SpringBoot声明式事务
事务的基本特征(ACID)
Atomic(原子性)
事务中包含的操作被看作一个整体的业务单元,这个业务单元中的操作要么全部成功,要么全部失败,不会出现部分失败和部分成功的场景
Consistency(一致性)
事务在完成时,必须使所有的数据都保持一直状态,在数据库中所有的修改都基于事务,保证了数据的完整性
Isolation(隔离性)
多个应用程序线程同时访问统一数据,这样数据库同样的数据就会在各个不同的事务中被访问,这样会产生丢失更新,为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制丢失更新的产生。因为互联网的应用常常面对高并发的场景,所以隔离性是需要掌握的重点内容
Durability(持久性)
事务结束后,所有的数据都会固化到一个地方,如保存磁盘当中,即时断电重启后也可以提供应用程序访问
为了压制丢失更新,数据库提出了隔离级别,在不同程度上压制更新
也许会有一个疑问,都全部消除丢失更新不就好了吗,为什么只是在不同的程度.上压制丢失更新呢?
其实这个问题是从两个角度去看的,一个是数据的一致性,另一个是性能。数据库现有的技术完全可以避免丢失更新,但是这样做的代价,就是付出锁的代价,在互联网中,系统不单单要考虑数据的-致性,还要考虑系统的性能。试想,在互联网中使用过多的锁,--旦出现商品抢购这样的场景,必然会导致大量的线程被挂起和恢复,因为使用了锁之后,一个时刻只能有一个线程访问数据,这样整个系统就会十分缓慢,当系统被数千甚至数万用户同时访问时,过多的锁就会引发宕机,大部分用户线程被挂起,等待持有锁事务的完成,这样用户体验就会十分糟糕。因为用户等待的时间会十分漫长,一般而言,互联网系统响应超过5秒,就会让用户觉得很不友好,进而引发用户忠诚度下降的问题。所以选择隔离级别的时候,既需要考虑数据的一致性避免脏数据,又要考虑系统性能的问题。因此数据库的规范就提出了4种隔离级别来在不同的程度上压制丢失更新。
隔离级别
未提交读
最低的隔离级别,含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,实际开发中应用不广
- 优点:并发能力高。适合那些对数据一致性没有要求而追求高并发的场景
- 缺点:出现脏读
读写提交
指一个事务只能读取一个事务已经提交的数据,不能读取未提交的数据
- 优点:克服脏读
- 缺点:出现不可重复读
可重复读
可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化,影响当前事务的执行
- 优点:克服不可重复读
- 缺点:出现幻读
串行化
数据库最高的隔离级别,它会要求所有的SQL都会按照顺序执行,这样就可以克服上诉隔离级别出现的各种问题,所以它能完全保证数据的一致性
追求更高的隔离级别,它能更好地保证数据的一致性,但是也要付出锁的代价。有了锁,就意味着性能的丢失,而且隔离级别越高,性能越是直线下降。
所以在选择隔离级别时,要考虑的不单单是数据一致性问题,还要考虑系统的性能问题
一般而言,选择隔离级别会以读写提交为主,它能防止脏读,而不能避免不可重复读和幻读,为了克服数据不一致性和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据库而使用其他手段
对于隔离级别,不同的数据库支持也是不一样的
- Oracle只支持读写提交和串行化,默认隔离级别为读写提交
- MySQL能支持4种,默认隔离级别为可重复读
传播行为
在Spring中,当一个方法调用另外一个方法,可以让事务采取不同的策略工作,如新建事务或挂起当前事务
在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,只是回滚出现异常的交易,而不是里整个批量任务,这样就能够是的那些没有问题的交易可以吮吸完成,而有问题的交易则不做任何事情
@Transcation
对于声明式事务,使用@Transaction进行标注,可标注在类活着方法上,当它标注在类上时,代表这个类所有公共(public)非静态的方法都将启用事务功能
默认配置下 Spring 只会回滚运行时、未检查异常(继承自 RuntimeException 的异常)或者 Error。
- 捕获RuntimeException
- 捕获Error
- 并不捕获Checked Exception
有了@Transcation,Spring就会知道从哪启动事务,约定流程:
当上下文开始调用被@Transcation标注的类或者方法时,Spring就会产生AOP的功能。请注意事务的底层需要启动AOP功能,这就是Spring事务的底层实现
如有一个保存用户的方法,加入 @Transactional 注解,使用默认配置,抛出异常之后,事务会自动回滚,数据不会插入到数据库。
@RestController public class HouseController { @Autowired private HouseRepository houseRepository; @GetMapping("/test1") public String test1(){ houseRepository.save(new House("house1", "100平方米")); houseRepository.save(new House("house2", "100平方米")); houseRepository.save(new House("house3", "100平方米")); houseRepository.save(new House("house444444444", "100平方米")); houseRepository.save(new House("house5", "100平方米")); return "success"; } @GetMapping("/test2") @Transactional public String test2(){ houseRepository.save(new House("house6", "100平方米")); houseRepository.save(new House("house7", "100平方米")); houseRepository.save(new House("house8", "100平方米")); houseRepository.save(new House("house999999999", "100平方米")); houseRepository.save(new House("house10", "100平方米")); return "success"; } }
test1方法没有加入事务,test2方法加入了事务注解。
test1:当输入http://localhost:8888/test1
数据库中插入了三条数据
因为第四条数据太长,所以插入失败,导致第五条正常数据插入失败,这并不是我们想要的,要么全成功,要么全失败
test2:输入http://localhost:8888/test2
数据库中数据不变依然是三条数据,插入失败,所以全部回滚
本文借鉴:《深入浅出Spring Boot 2.x》书中有更详细的例子