Springboot数据库事务处理——Spring声明式事务

版权声明:版权没有,盗用不究 https://blog.csdn.net/liman65727/article/details/84204322

前言

Spring事务部分之前一直理解的模棱两可,在实际开发中这些都由架构师完成,底层程序员更多的只是编写业务,并没有实际接触这个事务的配置,在数据库中事务的配置却是非常重要的一环。在springboot学习过程中,正好遇到了spring事务管理这个章节,就借这个机会总结一下事务的处理部分。

在spring数据库事务中,可以使用编程式事务和声明式事务,但是编程式事务需要手动编写事务的提交和回滚,这样相关代码就会与业务代码耦合在一起,这种方式目前也已经看不到使用,所以这里主要总结声明式事务的编写方式。

本篇博客重点会梳理隔离级别和传播行为的概念,并附上相关实例。

spring声明式事务的使用

首先,需要明确的是声明式事务底层是通过AOP的方式实现的,往深处说应该是采用了动态代理模式。

声明式事务字面意思就是告诉spring在什么地方启用事务,spring就会通过AOP自动进行数据库的事务操作。@Transactional既是spring中事务申明的标签,这个标签可以放在类上,也可以放在方法上;放在类上,表示这个类中所有的公共非静态方法都将采用事务机制;放在方法上,表示这个方法将采用事务处理机制。

其实spring的事务使用,只是给@Transactional这个注解配置属性而已......这个已经easy爆了

spring事务管理的核心内容是隔离级别和传播行为,隔离级别本身是数据库的概念,不同层度的隔离级别可以从不同层度上压制数据丢失更新的问题。传播行为则是规定了方法之间调用事务采取的策略问题。这两个概念后面会详细讨论。

spring事务管理器

事务的打开,回滚和提交都是交由spring的事务管理器来完成,spring中事务管理器的顶层接口PlatformTransactionManager,提供了一个事务管理器的实现标准,其中有一个方法——getTransaction(TransactionDefinition definition),其中的TransactionDefinition这个变量依赖于我们配置的@Transactional的配置项生成。

隔离级别

数据库事务

这个概念其实已经烂熟了,提到数据库事务就会想到它的四个属性——A(Atomic 原子性)、C(Consistency 一致性)、I(Isolation 隔离性)、D(Durability 持久性)。

除了隔离性,其他三个属性非常好理解,A——原子性:事务要么全部成功,要么全部不成功。C——一致性:数据库事务操作前后数据必须不能出错。D——持久性,事务结束后,数据必须固化到一个地方(磁盘等存储介质)。

唯独这个隔离性,是在为了解决数据更新丢失的问题而提出的。所以这里先介绍数据更新丢失的两个场景

数据丢失更新

多个事务(或者线程,其实感觉多线程情况下也会有这类问题,思想都差不多)在同时访问或修改相关数据的时候会产生两类数据更新丢失的情况

第一类丢失更新

上述的图片其实就是第一类更新丢失的问题,在T5时刻,事务1回滚了库存数据,而事务2的操作已经卖了一件商品出去,库存总数依然是100,这个数据明显是有问题的。 这种由于一个事务回滚,另一个事务提交而引发的数据不一致的情况,就是第一类更新丢失。现在的数据库系统基本都已经通过版本控制解决了第一类丢失更新问题。

第二类丢失更新

 上述数据表示的是第二类数据更新丢失,在T5时刻,事务1提交数据,库存总数为99。但是事务2也买了一件商品,事务1也成功卖了一件商品,库存总数应该是98,而不是99,数据依旧不正确。这种由于多个事务都提交数据引发的丢失更新问题称为第二类丢失更新。为了克服这个问题,于是就有了事务隔离级别。

隔离级别详解

隔离级别有四种,这个也是烂熟的概念。1、读未提交,2、读写提交,3、可重复读,4、串行化

读未提交

这个是最低的隔离级别,从字面不难理解,其实就是允许一个事务读取另外一个事务没有提交的数据。

问题同样出现在T5时刻, 事务2将库存扣减,之后提交事务这个过程没有问题。问题在于事务2读取的是事务1未提交的数据,即读取的库存数为1。事务1在回滚的时候,这种其实就是第一类丢失更新问题,但是由于采用CAS算法解决了该问题,事务1发现自己的数据并不是最新的,因此会采用事务2提交的数据,最终库存数为0,这明显就不正确,只买了一件商品,库存却少了2。

这个就是脏读现象,读取了一个事务未提交的数据,这个数据并不一定会持久化到数据库中,称为脏数据。可见这个隔离级别对数据一致性影响非常大,因此实际中真正用到事务的地方并不会用这种隔离级别。

读写提交

在读未提交的基础上,更进一步,一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据,避免了脏读。

T4时刻,在事务2提交事务的时候,发现本身自己读取不到事务1提交的数据,因此不会将库存扣减,而是保存读取到的数据。这个结果是正确的。但是......如果事务2扣减库存发生在T5之后,就会产生以下问题:

在T5时刻,事务2提交数据的时候会出错,毕竟数据库如果有限制的话,商品库存这个字段的数据是不能小于0的。这个的根本原因就是事务2没有再次读取到事务1更新的数据,为了解决这个问题,数据库的隔离级别还提出了可重复读的隔离级别。

可重复读

事务2在T5时刻更新数据库之前重新读取了事务1提交的数据,因此这里直接会返回库存为0的数据。 

但是这样依旧会产生新的问题——幻读

针对多条数据的时候,会出现查询的记录与最终得到的记录不一致,上面的实例非常清楚,这个就不详细解释了。

串行化

串行化是数据隔离的终极级别,让所有的事务都排队处理,这样并不会产生数据问题,但是性能就非常低了。

合理使用隔离级别

上面这张表估计也是见过多次了,这次应该理解更加深刻了,在一定场景下选用不同级别的隔离模式也是一个综合考量,需要综合性能和数据需求考虑。

在实际中,选择隔离级别会以读写提交为主,能够防止脏读,但是不能避免不可重复读和幻读的问题,有时候为了解决数据库数据不一致和性能问题,会用到Redis等NoSQL数据库。同时对于隔离级别,不同的数据库的支持也是不同的。例如Oracle只支持读写提交和串行化,而MySQL全部支持,但是MySQL默认是可重复读,Oracle默认是读写提交。

传播行为

传播行为理解起来相对简单。

回到开头的前言部分,说过,传播行为是方法之间调用事务采取的策略问题。在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,我们是回滚这些发生异常的交易,还是全部批量回滚所有任务。这个就是传播行为来定义的。当然,更多的时候,我们只希望回滚出现异常的交易。

这个图只是一个简易的实例图,用大白话来解释就是,如果批量任务函数配置了事务,批量任务在调用 单个交易的时候,单个交易也配置了自己的事务,这两者的关系该如何处理,单个交易是用自己的事务还是用批量任务函数的事务?

上述表中的调用者就是对应前面实例中的批量任务,事务类型是配置在单个交易的逻辑代码上。

总结

写到这里,这篇文章算是结束了,本来想一并总结springboot中整合数据库访问层的操作,但是发现其实这个比较简单,网上一些大牛博客总结的已经很到位了,一些简单的实例一看就能明白,这里就不再这篇博客中总结,会单独在springboot的数据访问博客中去总结。

参考资料:

《深入浅出Spring Boot 2.X》 一本比较通俗的书,实例丰富,本文中的大部分图例都来自这本书,文字部分也只是在这本书中提取出主干内容。

实例代码(还不是很完善)地址:https://github.com/liman657/springbootlearn

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/84204322