事务及Spring事务

1.事务的特性

事务是数据库的执行单位。事务具有ACID四个特性。

• 原子性(Atomicity):即事务中的一组操作,要么全执行,要么全部不执行

• 一致性(Consistency):事务前数据是一致,事务后数据也是保持一致的

• 隔离性(Isolation):多个并发事务执行时,要和事务串行执行的结果是一样的

• 持久性(Durability):数据库数据是持久化的,在数据库关闭或故障是可以恢复的

在事务的特性中,C即一致性是追求的根本,而对于一致性的破坏主要是两个方面,事务的并发执行,事务故障或系统故障。

(1) 事务的并发执行,主要涉及到事务的隔离性,隔离性的根本原则也是为了保障一致性。

(2) 事务故障,执行过程中任一操作出现异常,即要保障整个事务的失败,也就是回滚操作,保障了事务的原子性,原子性也是为了保障事务的一致性。

(3) 系统故障,执行中系统出现故障,此时日志恢复技术即可对已经提交对数据库的修改不会因为系统崩溃而丢失,保障了事务的持久性,原子性,从而保障一致性。

2.并发异常

2.1常见的并发异常

并发异常无非两种,并发读,并发写。

•     脏写:脏写是指事务回滚了其他事务对数据项的已提交修改,比如下面这种情况

• 丢失更新:丢失更新是指事务覆盖了其他事务对数据的已提交修改,导致这些修改好像丢失了一样。

• 脏读:脏读是指一个事务读取了另一个事务未提交的数据

• 不可重复读:不可重复读是指一个事务对同一数据的读取结果前后不一致。脏读和不可重复读的区别在于:前者读取的是事务未提交的脏数据,后者读取的是事务已经提交的数据,只不过因为数据被其他事务修改过导致前后两次读取的结果不一样,比如下面这种情况

• 幻读:幻读是指事务读取某个范围的数据时,因为其他事务的操作导致前后两次读取的结果不一致。幻读和不可重复读的区别在于,不可重复读是针对确定的某一行数据而言,而幻读是针对不确定的多行数据。因而幻读通常出现在带有查询条件的范围查询中,比如下面这种情况

2.2事务的隔离级别

事务的隔离级别主要是数据库提供的一种将并发事务分隔加锁的一种方式,不同级别,设置锁的方式不同。

• 读未提交(READ UNCOMMITTED)

事务和事务间可以相互读到未提交的数据,还是会出现脏读,不可重复读,幻读,丢失更新的问题。

• 读已提交(READ COMMITTED)

事务A可以读到事务B已经提交的数据,还是会出现不可重复读,幻读,丢失更新的问题。

• 可重复读(REPEATABLE READ)

事务A和事务B完全隔离,类似于快照,开启事务时A,B修改查询的仅仅是自己的快照,两个事务相互不影响,但是同时改表会有死锁。还是可能会出现幻读,丢失更新的问题。

• 串行化(SERIALIZABLE)

事务A和事务B顺序执行,解决了所有的并发异常。

2.3并发控制-事务的隔离性

基于快照隔离的并发控制

• 串行化(SERIALIZABLE)

事务顺序执行。没有特殊处理

• 读未提交

事务简单隔离,没有特殊处理

• 读已提交,可重复读

通过MVCC(多版本并发控制)来实现事务隔离

MVCC本质就是MySQL通过undolog存储了多个版本的历史数据,根据规则读取某一历史版本的数据,这样就可以在无锁的情况下实现读写并行,提高数据库性能。

对于使用InnoDB存储引擎的表来说,聚集索引记录中都包含下面2个必要的隐藏列

trx_id:一个事务每次对某条聚集索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列

roll_pointer:每次对某条聚集索引记录进行改动时,都会把旧的版本写入undo日志中。这个隐藏列就相当于一个指针,通过他找到该记录修改前的信息

为了判断版本链中哪个版本对当前事务是可见的,MySQL设计出了ReadView的概念。4个重要的内容如下

m_ids:在生成ReadView时,当前系统中活跃的事务id列表

min_trx_id:在生成ReadView时,当前系统中活跃的最小的事务id,也就是m_ids中的最小值

max_trx_id:在生成ReadView时,系统应该分配给下一个事务的事务id值

creator_trx_id:生成该ReadView的事务的事务id

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。

  • 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。

  • 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
     

那针对于read-commited和repeatable-read在使用MVCC版本控制时,是控制了生成readview的生成时机不同,而产生不同的隔离性。

read-commited:

    在每次语句执行的过程中,都关闭read_view, 重新在row_search_for_mysql函数中创建当前的一份read_view。这样就会产生不可重复读现象发生。

 repeatable read:

    在repeatable read的隔离级别下,创建事务trx结构的时候,就生成了当前的global read view。使用trx_assign_read_view函数创建,一直维持到事务结束。在事务结束这段时间内 每一次查询都不会重新重建Read View , 从而实现了可重复读。

基于锁的并发控制

锁通常分为共享锁和排他锁两种类型

• 1.共享锁(S):事务T对数据A加共享锁,其他事务只能对A加共享锁但不能加排他锁。

• 2.排他锁(X):事务T对数据A加排他锁,其他事务对A既不能加共享锁也不能加排他锁

update,delete,insert 都会自动给涉及到的数据加上排他锁,select 语句默认不会加任何锁

基于锁的并发控制流程:

1. 事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁)

2. 申请锁的请求被发送给锁管理器。锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。

3. 若锁被授予,则申请锁的事务可以继续执行;若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。

可能出现的问题:

• 死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。

• 饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。

对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。

在RR级别和RC级别加锁方式:

RR级别引入新的锁模式,间隙锁,于是,在使用非唯一索引时,lock_mode为:X,将会存在上界虚数据锁lock_data为:supremum pseudo-record,类似于锁表。在使用唯一索引时上行锁。

RC级别时,lock_mode为:X,REC_NOT_GAP(X锁,记录锁)也就是行锁,无论使用不使用索引,都将会在mysql server层将未命中的索引上的锁释放。

由上述加锁将可能会在RR级别下引发的问题,更新或删除表数据后,多线程插入数据将会产生锁超时的问题。

解决丢失更新的问题

由上面的隔离性可以看出,除了串行化能够解决丢失更新的问题,其他都不能,那么该如何解决这个问题呢。

悲观锁:对于要修改的行数据加行锁,读取时就使用select   for update,这样就可以使所有的并发事务读到的都是已经提交的最新数据,因为加排它锁后当前查询其他的并发仅能在当前事务提交后才能进行查询。

乐观锁:CAS技术,基于版本的比较更新,是一种较为高效的锁模式。

分析上述情景的方案

设置数据库隔离级别

set global transaction isolation level ***;

set session transaction isolation level ***;

查询数据库隔离级别

Mysql8

select @@transaction_isolation;

mysql5.7

select @@global.tx_isolation,@@tx_isolation;

查看数据库是否自动提交,mysql默认自动提交

show variables like 'autocommit';

设置不自动提交

set @@autocommit = 0; (0否1是)

开启事务:

start transaction;

提交事务:

Commit;

查询当前事务:

SELECT * FROM information_schema.INNODB_TRX;

给select加锁

1. select … lock in share mode,对读取的记录加S锁

2. select … for update ,对读取的记录加X锁

3.Spring事务配置方式

分别为编程式事务管理,声明式事务管理。

1.编程式事务管理

编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。

2.声明式事务管理

声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
编程式事务每次实现都要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置

4.事务的传播机制

事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
常用的事务传播机制如下:

• PROPAGATION_REQUIRED
Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入 到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行

• PROPAGATION_REQUES_NEW
该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可

• PROPAGATION_SUPPORT
如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务

• PROPAGATION_NOT_SUPPORT
该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码

• PROPAGATION_NEVER
该传播机制不支持外层事务,即如果外层有事务就抛出异常

• PROPAGATION_MANDATORY
与NEVER相反,如果外层没有事务,则抛出异常

• PROPAGATION_NESTED
该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。

传播规则回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。

5.事务回滚机制

在默认设置下,事务仅在出现运行时异常时进行回滚,而在出现受检异常时不回滚,这样设置的目的是为了,程序在出现受检异常时,我们主动处理异常,并进行主动回滚,或者在遇到运行时异常,我们无法预知异常类型,所以无法针对性的处理异常,于是,事务将会捕获到异常并进行回滚,可是由于我们的编码规范,以及代码架构来看,我们需要捕获程序中的所有异常,并针对于执行时异常进行统一处理,那么这样会导致,事务无法捕获到异常,也就无法回滚。

例如

@Transactional(rollbackFor = Exception.class)

Function A(){

Try{

    代码段;

}catch{

    捕获代码段中所有异常

}

}

上述代码中事务注解将会失效因为对于监听事务注解的处理是通过catch异常来进行回滚的。

Spring事务源码

当前类实现了MethodInterceptor切面执行接口

重写了Invoke方法

invokeWithinTransaction

具体的事务执行处理方法

主要看下这段有事务的时候

1开启事务,2执行方法,3捕获异常后回滚,4 线程池中清除当前事务线程,5 提交事务

这个 1,4 我不太清楚。但是大概就是这个意思,我们主要了解2,3,5即可。

由 2,3,5代码可以看出,如果方法执行时,方法内捕获了异常并未抛出就会导致,事务这里捕获不到异常,也就不会进行事务回滚。

当存在事务嵌套时,我们常用的事务传播为默认配置,也就是当前存在事务时,加入当前事务那这个时候很容易出现嵌套事务的问题,例如,内外事务状态不一致。

这种场景出现在

@Transactional(rollbackFor = Exception.class)

Function A(){

Try{

调用方法B(存在事务)

}catch{

}

}

当B事务存在异常需要进行回滚时进行,但是A事务捕获了B的异常catch并未再向外抛出异常时将会报错: Transaction rolled back because it has been marked as rollback-only

源码为:

当执行方法过程中,会给一个事务状态,内层方法事务需要回滚,则状态为回滚,但是外层事务要提交事务,在提交时检测到当前事务状态不正确,则回滚并抛出异常。

由源码也能看出,事务管理主要是实现了Spring方法拦截器MethodInterceptor

监听来实现的,所以必须在代理调用时才能被监听,因为本质上spring方法拦截器是通过代理工厂设置代理对象,拦截代理方法,如果是this调用,则注解失效。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值