springboot 【数据库事务处理】

一、1.1 声明式事务使用

      对于事务,需要通过标注告诉 Spring 在什么地方启用数据库事务功能 。 对于声明式事务,是使用@Transactional 进行标注的。这个注解可以标注在类或者方法上,当它标注在类上时,代表这个类所有公共非静态的方法都将启用事务功能。在@Transactional 中,还允许配置许多的属性,如事务的隔离级别和传播行为,核心内容 :如异常类型 ,从而确定方法发生什么异常下回滚事务或者发生什么异常下不回滚事务等。这些配置内容,是在 Spring IoC 容器在加载时就会将这些配置信息解析出来,然后把这些信息存到事务定义器( TransactionDefinition 接口的实现类〉里 , 并且记录哪些类或者方法需要启动事务功能,采取什么策略去执行事务。这个过程中,我们所需要做的只是给需要事务的类或者方法标注@Transactional 和配置其属性而己,并不是很复杂。

     Spring 通过对注解@Transactional 属性配置去设置数据库事务 , 跟着 Spring 就会开始调用开发者编写 的业务代码 。 执行开发者 的业务代码,可能发生异常,也可能不发生异常 。 在Spring 数据库事务 的流程 中,它会根据是否发生异常采取不同的策略。 Spring 事务拦截器根据@Transactional 配置的内容来完成的。

来看一下@Transactional 源码分析

package org . springfrarnework.transaction . annotation ;
/**imports ****/
@Target({ElernentType . METHOD, ElernentType . TYPE})
@Retention(RetentionPolicy . RUNTIME)
@Inherited
@Documented
public @interface Transactional {
//通过 be an name 指定事务管理器
@Alias For (” transactionManager” )
Stri ng value() defaul t ””;
//同 value 属性
@Alias For (” value ” )
Str 工 n g tran sactionManager () defau l t n ” ;
//指定传播行为
Propagation propagation() default Propagation . REQUIRED ;
//指定隔离级别
Isolation isolation () default Isola tion . DEFAULT;
//指定超时时间(单位秒)
int timeout() default TransactionDefinition . TIMEOUT DEFAULT ;
//是否只读事务
boolean readOnly() default false ;
//方法在发生指定异常时回滚,默认是所有异常都回滚
Class< ? extends Throwable > [] rollbackFor( ) default {);
//方法在发生指定异常名称时回滚 , 默认是所有异常都回滚
String[] rollbackForClassNarne() default {) ;
//方法在发生指定异常时不回滚 , 默认是所有异常都回滚
Class< ? extends Throwable> [] noRollbackFor () default {} ;
//方法在发生指定异常名称时不回滚,默认是所有异常都回滚
String[] noRollbackForClassNarne() default {} ;

1.2 spring 事务管理器

在 Spring Boot 中,当你依赖于 mybatis-spring-boot-starter 之后 , 它会自动创建一个 DataSourceTransactionManager 对象 ,作为事务管理器 ,如果依赖于 spring-boot-starter-data-j pa ,则它会自动创建JpaTransactionManager 对象作为事务管理器 ,所以我们一般不需要自己创建事务管理器而直接使用它们即可。

二、隔离级别

2.1 数据库事务具有以下 4 个基本特征 , 也就是著名的 ACID 。
• Atomic (原子性): 事务中包含的操作被看作一个整体的业务单元 , 这个业务单元中的操作  要么全部成功,要么全部失败,不会出现部分失败、部分成功的场景。6.3 隔离级别 119


Consistency (一致性):事务在完成时,必须使所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性。


Isolation (隔离性): 这是我们讨论的核心内容,正如上述,可能多个应用程序线程同时访问同一数据,这样数据库同样的数据就会在各个不同的事务中被访问,这样会产生丢失更新。为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制丢失更新的发生。因为互联网的应用常常面对高并发的场景,所以隔离性是需要掌握的重点内容。


Durability (持久性):事务结束后,所有的数据会固化到一个地方,如保存到磁盘当中,即使断电重启后也可以提供给应用程序访问。

2.2 详解隔离级别

为了压制丢失更新,数据库标准提出了 4 类隔离级别,在不同的程度上压制丢失更新,这 4 类隔离级别是未提交读读写提交可重复读串行化,它们会在不同的程度上压制丢失更新的情景。也许你会有一个疑 问, 都全部消除丢失更新不就好了吗 , 为什么只是在不 同的程度上压制丢失更新呢?其实这个 问题是从两个角度去看的, 一个是数据的一致性 , 另一个是性能。数据库现有的技术完全可以避免丢失更新,但是这样做的代价 , 就是付出锁的代价,在互联网中 , 系统不单单要
考虑数据的一致性,还要考虑系统的性能 。 试想,在互联网中使用过多 的锁, 一旦 出 现商品抢购这样的场景 , 必然会导致大量的线程被挂起和恢复,因为使用了锁之后, 一个时刻只能有一个线程访问数据 ,这样整个系统就会十分缓慢 ,当系统被数千甚至数万用户同时访问时,过多的锁就会引发岩机,大部分用户线程被挂起 , 等待持有锁事务的完成 , 这样用户体验就会十分糟糕。因为用户等待 的时 间会十分漫长 , 一般而言 , 互联网 系统响应超过 5 秒 , 就会让用户觉得很不友好 , 进而引发用户忠诚度下降的 问题 。 所 以选择隔离级别的时候 , 既需要考虑数据的一致性避免脏数据,又要考虑系统性能的 问题 。 因此数据库的规范就提出了 4 种隔离级别来在不同的程度上压制丢失更新 。 下面我们通过商品抢购的场景来讲述这 4 种隔离级别 的区别 


l. 未提交读
未提交读( read uncommitted )是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,所以一般在我们实际的开发中应用不广 , 但是它的优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景 ,它的最大坏处是出现脏读 。 让我们看看可能发生的脏读场景


表中的 T3 时刻,因为采用未提交读,所 以事务 2 可以读取事务 l 未提交的库存数据为 1,这里当它扣减库存后则数据为 0 , 然后它提交了事务 , 库存就变为了 0,而事务 l 在 T5 时刻 回滚事务 ,因为第一类丢失更新已经被克服,所以它不会将库存回滚到 2 ,那么最后 的结果就变为了 0 , 这样就出现了错误。脏读一般是 比较危险的隔离级别 , 在我们实际应用中采用得不多 。 


2. 读写提交
读写提交( read committed )隔离级别, 是指一个事务只能读取另外一个事务已经提交的数据 ,不能读取未提交的数据 。表 6-4 克服脏读

在 T3 时刻,由于采用了读写提交的隔离级别 , 因此事务 2 不能读取到事务 l 中未提交的库存 I,所以扣减库存的结果依 旧为 1 , 然后它提交事务,则库存在 T4 时刻就变为了 l 。 TS 时刻, 事务 l 回滚 , 因为第一类丢失更新己经克服,所以最后结果库存为 1 , 这是一个正确 的结果。 但是读写提交也会产生下面的问题

在 T3 时刻事务 2 读取库存的时候 , 因为事务 l 未提交事务,所以读出的库存为 l , 于是事务 2认为当前可扣减库存 ; 在 T4 时刻,事务 l 己经提交事务,所以在 TS 时刻 , 它扣减库存的时候就发现库存为 0,于是就无法扣减库存了 。 这里的 问题在于事务 2 之前认为可以扣减,而到扣减那一步却发现已经不可以扣减,于是库存对于事务 2 而言是一个可变化的值,这样的现象我们称为不可重复读 , 这就是读写提交的一个不足 。 为了克服这个不足,数据库的隔离级别还提出了可重复读的隔离
级别 ,它能够消除不可重读的 问题 。


3 . 可重复读
可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化, 影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出 了可重复读的隔离级别 。表 6-6 克服不可重读



可以看到,事务 2 在 T3 时刻尝试读取库存,但是此时这个库存己经被事务 1 事先读取,所以这个时候数据库就阻塞它的读取, 直至事务 1 提交,事务 2 才能读取库存的值 。 此时己经是 T5时刻,而读取到的值为 0,这时就已经无法扣减了,显然在读写提交中出现的不可重复i卖的场景被消除了 。但是这样也会引发新的问题的出现,这就是幻读。假设现在商品交易正在进行中,而后台有人也在进行查询分析和打印的业务,让我们看看如表 6-7 所示可能发生的场景。表 6-7 幻读

这便是幻读现象。 可重复读和幻读,是比较难以理解的内容 ,这里稍微论述一下 。 首先这里的笔数不是数据库存储的值,而是一个统计值,商品库存则是数据库存储的值,这一点是要注意的 。 也就是幻读不是针对一条数据库记录而言,而是多条记录 ,例如, 这 51 笔交易笔数就是多条数据库记录统计出来的 。 而可重复读是针对数据库 的单一条记录,例如,商品的库存是 以数据库里面的一条记录存储的,它可以产生可重复读,而不能产生幻读 。


4. 串行化
串行化 ( Serializabl巳)是数据库最高的隔离级别,它会要求所有的 SQL 都会按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性 。但是全部请求都将串行化,导致执行效率低等情况。

这是总结得一张表。

作为互联网开发人员,在开发高并发业务时需要时刻记住隔离级别可能发生的各种概念和相关的现象,这是数据库事务的核心内容之一 ,也是互联网企业关注的重要内容之一。追求更高的隔离级别,它能更好地保证了数据的一致性,但是也要付出锁的代价 。 有了锁,就意味着性能的丢失,而且隔离级别越高,性能就越是直线地下降。所以我们在选择隔离级别时,要考虑的不单单是数据一致性的问题,还要考虑系统性能的问题。例如,一个高并发抢购的场景,如果采用串行化隔离级别,能够有效避免数据的不一致性,但是这样会使得并发的各个线程挂起,因为只有一个线程可以操作数据,这样就会出现大量的线程挂起和恢复,导致系统缓慢。而后续的用户要得到系统响应就需要等待很长的时间,最终因为响应缓慢,而影响他们的忠诚度。所以在现实中一般而言,选择隔离级别会以读写提交为主,它能够防止脏读,而不能避免不可重复读和幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据库而使用其他的手段。例如,使用 Redis 作为数据载体。对于隔离级别,不同的数据库的支持也是不一样的。例如, Oracle 只能支持读写提交和串行化,而 MySQL 则能够支持 4 种,对于 Oracle 默认的隔离级别为读写提交, MySQL 则是可重复读,这些需要根据具体数据库来决定。只要掌握了隔离级别的含义,使用隔离级别就很简单,只需要在@Transactional 配置对应即可

@Transactional (isolation= Isolation.SERIALIZABLE)

springboot可以通过配置文件设置默认得隔离级别,如果需要修改可以再配置文件中加上默认得配置

#隔离级别数字配置的含义 ·
#-1 数据库默认隔离级别
#1 未提交读
#2 读写提交
#4 可重复读
#8 串行化
#dbcp2 数据库连接池默认隔离级别

spring.datasource.druid.default-transaction-isolation=2

三、@Transactional失效的问题

事务其实现原理是 AOP , 而 AOP 的原理是动态代理 , 在自调用的过程中 , 是类自身的调用 ,而不是代理对象去调用, 那么就不会产生 AOP , 这样 Spring就不能把你的代码织入到约定的流程中 , 于是就产生了现在看到的失败场景。

详情请看我的另一篇博客 https://mp.csdn.net/postedit/85336514 有详细的介绍。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值