【详细版】Spring 事务


一个程序中不可能没有事务,而 Spring 中,事务的实现方式分为两种:编程式事务(手动写代码操作事务)和声明式事务(利用注解⾃动开启和提交事务)。因为编程式事务实现相对麻烦,而声明式事务实现极其简单,接下来,我们介绍声明式事务。

1. Spring 声明式事务@Transactional使用

声明式事务的实现很简单, 只需要在需要事务的方法上添加 @Transactional 注解就可以实现了。@Transactional注解的作用是:无需手动开启事务和提交事务, 进入方法时自动开启事务, 方法执行完会自动提交事务, 如果中途发生了没有处理的异常会自动回滚事务。
@Transactional可以用来修饰类或者方法:

  • 修饰方法时: 只有修饰public 方法时才⽣效(修饰其他方法时不会报错, 也不生效)
  • 修饰类时: 对 @Transactional 修饰的类中所有的 public 方法都生效

方法/类被 @Transactional 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结束之后, 自动提交事务。
代码实现:

@Slf4j
@RestController
@RequestMapping("/trans")
public class TransactionalController {
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/r1")
    public String r1(String userName, String password) {
        Integer result = userService.insertUser(userName, password);
        log.info("数据插入成功,result:"+result);
        return "注册成功";
    }
}
@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    public Integer insertUser(String userName, String password) {
        return userInfoMapper.insertUser(userName, password);
    }
}
@Mapper
public interface UserInfoMapper {
    @Insert("insert into user_info(user_name, `password`) values(#{userName},#{password})")
    Integer insertUser(String userName, String password);
}

执行代码之前,数据库中的数据如下:
image.png
运行程序,访问http://127.0.0.1:8080/trans/r1?userName=lisi&password=123,响应结果如下:
image.png
观察日志,事务提交成功:
image.png
查看数据库,数据插入成功:
image.png
接下来,我们修改程序, 使之出现异常
image.png
运行程序,访问:http://127.0.0.1:8080/trans/r1?userName=lisi&password=1234,响应结果:
image.png
观察后端日志,虽然日志显示数据插入成功,但事务并没有进行提交,进行了回滚
image.png
查看数据库,并没有插入数据:
image.png
如果我们不使用@Transactional注解,同样进行上述操作:
image.png
运行程序,访问:http://127.0.0.1:8080/trans/r1?userName=lisi&password=1234,响应结果:
image.png
观察后端日志,此时没有事务相关的日志,并且日志上显示数据插入成功:
image.png
查看数据库,发现数据插入成功了:
image.png
这就是使用事务和不使用事务的区别,当方法发生异常时,如果使用了事务,事务会进行回滚,数据不会插入成功;而不使用事务,即使发生异常,已经插入的数据并不会进行回滚。

2. @Transactional详解

通过上⾯的代码, 我们学习了 @Transactional 的基本使⽤。接下来我们学习 @Transactional 注解的使用细节,
@Transactional 注解当中的三个常见属性:

  1. rollbackFor: 异常回滚属性。指定能够触发事务回滚的异常类型,可以指定多个异常类型
  2. Isolation: 事务的隔离级别。默认值为 Isolation.DEFAULT
  3. propagation: 事务的传播机制。默认值为 Propagation.REQUIRED

2.1. rollbackFor

接下来,我们模拟几种异常情况,来演示事务回滚。

  1. 修改代码:

事务回滚
运行程序,访问http://127.0.0.1:8080/trans/r2?userName=lisi11&password=12345,响应结果:
image.png
观察后端日志,事务并没有提交成功,而是进行了回滚
image.png
查看数据库,并没有插入新的数据:
image.png

  1. 接下来,我们继续修改代码

事务提交
运行程序,访问http://127.0.0.1:8080/trans/r3?userName=lisi11&password=12345,响应结果:
image.png
观察后端日志,事务提交成功:
image.png
查看数据库,数据插入成功:
image.png
为什么同样是异常,有的异常,事务进行了回滚,而有的异常,事务并没有回滚?
其实,异常的事务回滚是有条件的,@Transactional 默认只在遇到运行时异常Error时才会回滚, 非运行时异常不回滚,即Exception的子类中, 除了RuntimeException及其⼦类。
image.png
我们希望当发生非运行时异常时,事务也进行回滚,应该怎么做呢?
如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚。

  1. 修改代码:

事务回滚
运行程序,访问http://127.0.0.1:8080/trans/r4?userName=lisi11&password=12345,响应结果:
image.png
观察后端日志,事务没有提交,事务进行了回滚
image.png
查看数据库,并没有新的数据插入:
image.png

  1. 对比r1,继续修改代码,针对异常进行try catch

事务回滚
事务提交
运行程序,访问http://127.0.0.1:8080/trans/r5?userName=lisi11&password=12345,响应结果:
image.png
观察后端日志,事务提交成功:
image.png
查看数据库,新的数据插入成功:
image.png
总结:如果我们对异常进行了捕获,事务就进行提交。
如果发生异常,我们对异常进行了捕获,也希望事务进行回滚,应该怎么做呢?
两种做法

  1. 继续把异常抛出去
  2. 手动设置回滚

演示做法1:
事务回滚
程序运行前,数据库的数据如下:
image.png
运行程序,访问http://127.0.0.1:8080/trans/r6?userName=lisi11&password=1111,响应结果:
image.png
观察后端日志,事务进行了回滚
image.png
查看数据库,没有新的数据插入:
image.png
演示做法2:
手动设置事务回滚:使用 TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并使用 setRollbackOnly 设置 setRollbackOnly
事务回滚
运行程序,访问http://127.0.0.1:8080/trans/r7?userName=lisi11&password=1111,响应结果:
image.png
观察后端日志,事务并没有提交,事务进行了回滚
image.png
查看数据库,没有新的数据插入:
image.png
总结@Transactional注解:

  1. 方法执行前,开启事务
  2. 执行方法
  3. 方法正常运行,事务进行提交。方法发生运行时异常且没有进行捕获或者发生Error,事务进行回滚。方法发生运行时异常且程序进行了捕获,事务提交;如果既希望捕获又不希望事务提交,继续抛出异常或者手动设置事务回滚其余异常,方法不回滚,如果需要回滚,设置rollbbackFor

3. 事务的隔离级别

3.1. MySQL事务的隔离级别

SQL 标准定义了四种隔离级别,MySQL 全都⽀持。这四种隔离级别分别是:

  1. READ-UNCOMMITTED(读未提交) :最低的隔离级别,该隔离级别的事务可以读到其他事务中未提交的数据,可能会导致脏读、幻读或不可重复读。

因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数
据称之为脏数据, 这个问题称之为脏读

  1. READ-COMMITTED(读已提交) :该隔离级别的事务允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。

该隔离级别不会有脏读的问题.但由于在事务的执行中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读

  1. REPEATABLE-READ(可重复读) : 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交。也就可以确保同⼀事务多次查询的结果⼀致, 可以阻止脏读和不可重复读。但是对其他事务新插⼊的数据, 是可以感知到的。这也就引发了幻读问题。可重复读, 是 MySQL 的默认事务隔离级别。

比如此级别的事务正在执⾏时, 另⼀个事务成功的插⼊了某条数据, 但因为它每次查询的结果都是⼀样的, 所以会导致查询不到这条数据, 自己重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但自己就是插⼊不进去, 这种现象叫做幻读

  1. SERIALIZABLE(串行化) :事务最高的隔离级别。它会强制所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,该级别可以防止脏读、不可重复读以及幻读。

image.png

3.2. Spring事务的隔离级别

Spring 定义了一个枚举类:Isolation

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

Spring 中事务隔离级别有5 种:

  1. Isolation.DEFAULT: 以连接的数据库的事务隔离级别为主,MySQL 默认采用的REPEATABLE_READ 隔离级别, Oracle 默认采用的 READ_COMMITTED 隔离级别。
  2. Isolation.READ_UNCOMMITTED: 最低的隔离级别,读未提交, 对应SQL标准中 READ UNCOMMITTED
  3. Isolation.READ_COMMITTED: 读已提交,允许读取并发事务已经提交的数据,对应SQL标准中 READ COMMITTED
  4. Isolation.REPEATABLE_READ: 可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,对应SQL标准中 REPEATABLE READ
  5. Isolation.SERIALIZABLE:串行化,最高的隔离级别,它会强制所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,对应SQL标准中 SERIALIZABLE

Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置:

@Transactional(isolation = Isolation.DEFAULT)
@RequestMapping("/r7")
public String r7(String userName, String password) {
    // 代码略
    return "注册成功";
}
  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值