Spring-声明式事务

一、概念的引出

        事务这个在哪里会使用到?当我们处理一个比较复杂的业务需求时,比如银行转账,比如淘宝网上支付,涉及到多个表的问题。甚至还会涉及到跨系统.
●编程式事务
在编码中直接控制,一般是硬编码.看一段示意代码.
Connection con = JdbcUtils.getConnectionO;
con.setAutocommit(false);
try{
//开始我们一系列的操作…
//..…
}catch(Exception e){  //只要报错,就将所有数据库操作回滚
    con.rollabck0;
}
con.
con.commitO;
特点:
1.比较直接,也比较好理解
2.灵活度不够,而且功能不够强大。(如果事务涉及多个Dao,那么就不好控制)

●声明式事务

即我们可以通过注解的方式来说明哪些方法是事务控制的,并且可以指定事务的隔离级别

二、事务的简单示例

首先,使用事务需要先添加依赖,并配置appliction.xml

1、@TRANSACTIONANL事务是基于AOP面向切面的,所以aop的包需要

2、maven依赖

<!--        事务@TRANSACTIONAL-->
<!--        代码生成库-->
        <dependency>
            <groupId>net.sourceforge.cglib</groupId>
            <artifactId>com.springsource.net.sf.cglib</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!--        事务@TRANSACTIONAL-->
<!--        aop面向切面编程相关-->
        <dependency>
            <groupId>org.aopalliance</groupId>
            <artifactId>com.springsource.org.aopalliance</artifactId>
            <version>1.0.0</version>
        </dependency>
   <!--        AOP切面编程框架 aspect-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.9</version>
        </dependency>

3、配置xml

<!--    配置事务管理器-->
<bean id="DataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--    开启基于注解的声明式事务功能-->
<tx:annotation-driven transaction-manager="DataSourceTransactionManager"/>

create table `user_account` (
								user_id int UNSIGNED PRIMARY KEY auto_increment,
								user_name varchar(32) not null DEFAULT '',
								money DOUBLE not null default 0.0
								)CHARSET=utf8 engine=innodb;
insert into `user_account` VALUES(null,'张三',1000),(null,'李四',2000);	
create table `goods` (
									goods_id int unsigned primary key auto_increment,
									goods_name varchar(32) not null default '',
									price DOUBLE not null DEFAULT 0.0
									)charset = utf8 engine=innodb;
insert into `goods` VALUES (null,'小风扇',10.00),
(null,'小台灯',12.00),(null,'可口可乐',3.00);
create table `goods_amount` (
								goods_id int UNSIGNED PRIMARY key auto_increment,
								goods_num int UNSIGNED DEFAULT 0
								)charset = utf8 engine= innodb;
insert into `goods_amount` VALUES(1,200),
(2,20),(3,15);


@Repository
public class GoodsDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    //根据商品获得价格
    public float queryById(int goodsId) {
        String sql = "SELECT price FROM goods WHERE goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, new Object[]{goodsId}, Float.class);
        return price;
    }
    //修改某个用户的余额
    public void updateBalance(int userId,float money) {
        String sql = "UPDATE user_account SET money = money - ? WHERE user_id = ?";
        jdbcTemplate.update(sql,money,userId);
    }
    //修改库存量
    public void updateAmount(int goodsId,int amount) {
        String sql = "UPDATE goods_amount SET goods_num = goods_num-? WHERE goods_id = ?";
        jdbcTemplate.update(sql,amount,goodsId);

    }
}

使用@TRANSACTIONAL注解 , 明显地 


@Service
public class GoodsService {
    @Autowired
    private GoodsDao goodsDao;
    @Transactional //事务注解!!!!
    public void buyGoods(int goodsId,int userId,int nums){
        //第一步查询价格
        float price = goodsDao.queryById(goodsId);
        float price2 = price*nums;
        //判断用户余额是否足够
        //更新余额
        goodsDao.updateBalance(goodsId,price2);
        //更新库存
        goodsDao.updateAmount(goodsId,nums);
    }
}

说明:

明显地,在buyGoods买东西的操作中,

如果不加入事务控制, 假如更新余额成功,而更新库存失败 , 那么就会发生超卖风险。

添加了@TRANSACTIONAL后,在GoodsService中,更新余额 操作, 与 更新库存操作 只要有一个抛异常, 那么两个操作在数据库都不会成功。 即是一个整体, 要么都成功, 要么都不成功。数据一致性。

请注意 @TRANSACTIONAL 一般修饰 service 层

三、声明式事务:事务传播机制

基本说明:

当有多个事务处理并存时,如何控制?比如用户去购买两次商品(使用不同的方法),每个方法都是一个事务,那么如何控制呢?=>这个就是事务的传播机制,看一个具体的案例:

当事务嵌套时,应该如何管理事务呢?事务传播机制

spring一共提供了7中事务传播机制,常用的是REQUIRED和REQUIRED_NEW事务传播机制.(描述看不懂,请继续往下看,我也看不懂哈哈哈)

使用说明


@Service
public class GoodsService {
    @Autowired
    private GoodsDao goodsDao;
    @Transactional(propagation = Propagation.REQUIRED)
    public void buyGoods(int goodsId,int userId,int nums){
        //第一步查询价格
        float price = goodsDao.queryById(goodsId);
        float price2 = price*nums;
        //判断用户余额是否足够
        //更新余额
        goodsDao.updateBalance(goodsId,price2);
        //更新库存
        goodsDao.updateAmount(goodsId,nums);
    }
}

常用的两个传播机制

请看截图解释,常用的两种传播机制

REQUIRED :  事务的默认传播机制就是REQUIRED 

 当事务嵌套时,如果全部事务(包括Tx1事务,嵌套事务方法1,方法2)的传播机制指定的都是REQUIRED, 那么可以看作Tx1方法中任意报错,那么整体回滚(即方法1,方法2也要回滚)。

 

 详细说明:

1、在buyGoods这个事务中,我们执行“更新余额”, “更新库存”这两个操作

@Service
public class GoodsService {
    @Autowired
    private GoodsDao goodsDao;
    
    public void buyGoods(int goodsId,int userId,int nums){
        //第一步查询价格
        float price = goodsDao.queryById(goodsId);
        float price2 = price*nums;
        //判断用户余额是否足够
        //更新余额
        goodsDao.updateBalance(goodsId,price2);
        //更新库存
        goodsDao.updateAmount(goodsId,nums);
    }

2、故意将“修改库存量”的sql写错。那么在没有事务的情况下,调用buyGoods(),明显地更新余额会成功而更新库存不会成功。 

3、现在我们在buyGoods上加了@TRANSACTIONAL,测试一下看看会有什么不同

@Service
public class GoodsService {
    @Autowired
    private GoodsDao goodsDao;
    @TRANSACTIONAL
    public void buyGoods(int goodsId,int userId,int nums){
        //第一步查询价格
        float price = goodsDao.queryById(goodsId);
        float price2 = price*nums;
        //判断用户余额是否足够
        //更新余额
        goodsDao.updateBalance(goodsId,price2);
        //更新库存
        goodsDao.updateAmount(goodsId,nums);
    }

 4、在测试之前,查询mysql,

5、测试下,发现商品1的数量并没有改变,说明事务发生了作用,进行了回滚。且默认传播机制就是REQUIRED 

   @Test
    public void test04() throws SQLException {
        GoodsService goodsService = (GoodsService) applicationContext.getBean("goodsService");
        goodsService.buyGoods(1, 1, 1);
    }

REQUIRED_NEW:

TX1  @Transactional(propagation = Propagation.REQUIRED)

TX2  @Transactional(propagation = Propagation.REQUIRED_NEW)

TX3  @Transactional(propagation = Propagation.REQUIRED_NEW)

当TX2开始时,TX1会挂起,直到TX2结束,TX1继续。以此类推

简单理解,即TX1,TX2,TX3这三个事务之间互不影响,  即使 方法1报错,也不影响 方法2的操作,也不影响tx1

详细说明:

1、重新再写一个dao,


@Repository
public class GoodsDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    //根据商品获得价格
    public float queryById(int goodsId) {
        String sql = "SELECT price FROM goods WHERE goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, new Object[]{goodsId}, Float.class);
        return price;
    }
    //修改某个用户的余额
    public void updateBalance(int userId,float money) {
        String sql = "UPDATE user_account SET money = money - ? WHERE user_id = ?";
        jdbcTemplate.update(sql,money,userId);
    }
    //修改库存量
    public void updateAmount(int goodsId,int amount) {
        String sql = "UPDATE goods_amount SET goods_num = goods_num-? WHERE goods_id = ?";
        jdbcTemplate.update(sql,amount,goodsId);
    }
    public float queryById2(int goodsId) {
        String sql = "SELECT price FROM goods WHERE goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, new Object[]{goodsId}, Float.class);
        return price;
    }
    //修改某个用户的余额
    public void updateBalance2(int userId,float money) {
        String sql = "UPDATE user_account SET money = money - ? WHERE user_id = ?";
        jdbcTemplate.update(sql,money,userId);
    }
    //修改库存量
    public void updateAmount2(int goodsId,int amount) {
        String sql = "UPDATE goods_amount SET1 goods_num = goods_num-? WHERE goods_id = ?";
        jdbcTemplate.update(sql,amount,goodsId);
    }
}

2、重新再写一个Service,buyGoods2全部用方法2,并设置好REQUIERS_NEW


@Service
public class GoodsService {
    @Autowired
    private GoodsDao goodsDao;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void buyGoods(int goodsId,int userId,int nums){
        //第一步查询价格
        float price = goodsDao.queryById(goodsId);
        float price2 = price*nums;
        //判断用户余额是否足够
        //更新余额
        goodsDao.updateBalance(goodsId,price2);
        //更新库存
        goodsDao.updateAmount(goodsId,nums);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void buyGoods2(int goodsId,int userId,int nums){
        //第一步查询价格
        float price = goodsDao.queryById2(goodsId);
        float price2 = price*nums;
        //判断用户余额是否足够
        //更新余额
        goodsDao.updateBalance2(goodsId,price2);
        //更新库存
        goodsDao.updateAmount2(goodsId,nums);
    }
}

3、 写一个multiService


@Service
public class MultiService {
    @Autowired
    private GoodsService goodsService;
    @Transactional
    public void multiBuyGoods () {
        goodsService.buyGoods(1,1,1);
        goodsService.buyGoods2(2,2,1);
    }
}

4、故意将buyGoods2的sql写错。 测试,看看会发生什么变化?会回滚吗?

先查mysql, 注意看张三的钱930

执行

   @Test
    public void test05() throws SQLException {
        MultiService multiService = (MultiService)applicationContext.getBean("multiService");
        multiService.multiBuyGoods();
    }

 测试后,查mysql ,发现张三的钱变了,李四没有变,

由此说明 REQUIRED_NEW修饰的话,就是你是你,我是我,事务间互不影响

四、声明式事务-隔离级别

mysql中的四种隔离级别(详见mysql篇。哦,还没补笔记,呵呵~)

细节:
1.如要看到隔离级别效果,设置隔离级别时,这里我们需要将其传播方式设置为REQUIRES_NEW
2.因为如果是REQUIRES 是不会开新事务,这样这个隔离级别就是hibernate默认的隔离级别(一般是repeatable read )

细节的解释:

当老师说需要将事务的传播机制设置成REQUIRES_NEW才能看到隔离级别的效果时,可能有以下原因:

  1. 确保新的事务总是开始一个新的事务。如果一个事务启动了另一个事务,那么新的事务将具有自己的隔离级别,而不会受到原始事务隔离级别的影响。这样可以更清晰地观察不同隔离级别之间的差异。
  2. 在一些数据库系统中,如MySQL,当使用InnoDB存储引擎时,默认的事务隔离级别是可重复读(REPEATABLE READ)。在这个隔离级别下,如果一个事务在执行过程中有其他事务尝试修改或删除其中的数据,将会看到不可重复读(Non-repeatable Read)现象,即同一行数据在不同的事务中可能表现出不同的状态。而将事务传播机制设置为REQUIRES_NEW可以确保每个事务都以自己的隔离级别执行,不受其他事务的影响。

测试事务隔离级别 

@Service
public class GoodsService {
    @Autowired
    private GoodsDao goodsDao;
    @Transactional(
            propagation = Propagation.REQUIRES_NEW
//            ,isolation = Isolation.READ_COMMITTED
    )
    public void buyGoods(int goodsId,int userId,int nums){
        //第一步查询价格
        float price = goodsDao.queryById(goodsId);
        float price2 = price*nums;
        //判断用户余额是否足够
        //更新余额
        goodsDao.updateBalance(goodsId,price2);
        //更新库存
        goodsDao.updateAmount(goodsId,nums);
        //第二次查询价格,测试事务隔离级别
        price = goodsDao.queryById(goodsId);
        System.out.println("goods_price"+price);
    }

 ​​​​​

 debug以下测试方法,程序在断点停滞,

  @Test
    public void test06() throws SQLException {
        GoodsService goodsService = (GoodsService) applicationContext.getBean("goodsService");
        goodsService.buyGoods(1, 1, 1);
    }

此时先查mysql,  id =1 的商品的价格是10

开启一个新事务并 修改price = 20, where goods_id = 1 , 然后commit

stepOver程序,发现控制台输出的仍然是 goods_price = 10

说明默认的@TRANSACTIONAL的事务隔离级别是 REPEATABLE READ 可重复读

那么现在来修改一下隔离级别

 @Transactional(
            propagation = Propagation.REQUIRES_NEW
           ,isolation = Isolation.READ_COMMITTED
    )
    public void buyGoods(int goodsId,int userId,int nums){
        //第一步查询价格

重复刚才得步骤

到断点时,查mysql, 价格是20

登录另一个mysql,开启事务,并修改price=30 where goods_id = 1, 然后commit提交;


继续debug,会看到什么呢?首先我们定义的是,isolation = Isolation.READ_COMMITTED读已提交

那么预计的控制台输出会发生不可重复读的错误(读已提交会发生幻读,不可重复读),即price= 30

结束

mysql默认的隔离级别是可重复读,那么什么时候需要改变事务的隔离级别呢?(以下是文心一言的回答,仅供参考,不保证对)

MySQL默认的隔离级别是“可重复读”(REPEATABLE READ),这个级别在大多数情况下都能够满足需求。然而,在某些特定场景下,你可能需要改变事务的隔离级别,以满足特定的需求或解决某些问题。

以下是一些可能需要改变事务隔离级别的情况:

  1. 读已提交(READ COMMITTED)隔离级别:如果你需要读取其他事务已经提交的更改,而不是等待当前事务提交后再读取,你可以将隔离级别设置为“读已提交”。这可以减少读取操作的延迟,但可能会增加脏读(读取到其他未提交事务的更改)的风险。
  2. 读未提交(READ UNCOMMITTED)隔离级别:在某些情况下,你可能需要读取其他事务尚未提交的更改。例如,你可能需要实时监控系统中的某些指标,而这些指标是由其他事务频繁更新的。在这种情况下,你可以将隔离级别设置为“读未提交”,以便读取最新的未提交数据。但是请注意,这种级别的脏读风险非常高。
  3. 序列化(SERIALIZABLE)隔离级别:如果你需要执行一系列严格按照顺序执行的操作,并且要求这些操作是原子的(即不可中断的),你可以将隔离级别设置为“序列化”。这将强制所有读写操作按照严格的顺序执行,避免了并发操作可能引起的冲突。但是,这种级别的性能开销较大,可能会降低系统的并发性能。

需要注意的是,改变事务的隔离级别可能会带来一些副作用。较低的隔离级别可能导致数据一致性问题(如脏读、不可重复读和幻读),而较高的隔离级别可能导致性能下降。因此,在决定改变事务隔离级别之前,你应该仔细评估系统的需求和性能要求,并进行充分的测试。

 声明式事务-事务的超时回滚

 基本介绍:

        如果一个事务执行的时间超过某个时间限制,就让该事务回滚。可以通过设置事务超时回顾来实现

测试执行,发现控制台报超时错误。 

声明式事务:事务的只读模式

基本介绍
        如果一个事务执行的操作都是读的操作,我们可以明确的指定该事务是readOnly,这样便于数据库底层对其操作进行优化处理

@Transactional(
            propagation = Propagation.REQUIRES_NEW
           ,isolation = Isolation.READ_COMMITTED, timeout = 2,
            readOnly = true
    )
    public void buyGoods(int goodsId,int userId,int nums){
        //timeout = 2 表示这个事务两秒钟没有执行完,就会自动回滚

 基于XML的声明式事务

基本介绍
除了通过注解来配置声明式事务,还可以通过xml的方式来配置事务。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值