MyBatis的事务管理:关于事务的基础概念介绍
本文更新日期:2019年10月28日
事务问题
本文主要是讲解一下MyBatis的事务管理,在了解MyBatis的事务管理之前,先看一位网友提出的问题:
我们系统采用SpringMVC+MyBatis的架构,数据使用的是MySQL,数据库的隔离级别是默认的:REPEATABLE-READ。现在发现一个事务并发控制锁定问题。
我们系统有一个业务逻辑,每个人只能执行一次,所以开启事务的时候,我们使用悲观锁进行控制:
select * from table where type = 1 and id = XXX for update;
如果用户点击非常快,点击两次的话,就会启动两个事务进行控制,暂定A和B两个事务。假如A事务先获得锁,因为只有A事务执行完成之后B事务才能获得锁,所以此时B事务就会处于等待状态。A事务在处理完成之后,就会把类型type修改了,改成了其他的值,比如说:2。但是,B事务在获得了锁之后,即本语句:select * from table where type = 1 and id = XXX for update;依然能够执行通过,此时事务B认为该用户的type还是1,即还是A事务修改之前的数值。
针对上面问题,对Repeatable Read事务隔离简要分析如下:
时刻 | 事务A | 事务B | 说明 |
T1 | 查询:type=1 | 查询:type=1 | A和B都是读事务,可并行 |
T2 | 更新:type=2 | 事务A更新,事务B堵塞 | |
T3 | 更新:type=2 | 事务B更新 |
针对上面问题的解决方法有多种,后续文章会详细介绍,敬请期待。请先熟悉一下数据库中事务的相关知识,如下文所述。
数据库隔离级别
数据库和程序一样,也有并发的问题,在同时存在两个或者两个以上的数据库事务环境中,同一条记录甚至是不同记录都会由于SQL在不同时刻的执行产生不同的结果,甚至产生错误。于是便有了隔离级别这样的数据库的概念。
隔离级别 | 脏读(Dirty Read) | 写覆盖(Write Cover) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
读未提交(Read uncommitted) | 可能 | 可能 | 可能 | 可能 |
读已提交(Read committed) | 不可能 | 可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 | |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
数据库并发异常情况介绍
1.脏读。
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。如下面的例子:
1.Mary的原工资为1000, 财务人员将Mary的工资改为了8000(但未提交事务)
2.Mary读取自己的工资 ,发现自己的工资变为了8000,欢天喜地!
3.而财务发现操作有误,回滚了事务,Mary的工资又变为了1000
像这样,Mary记取的工资数8000是一个脏数据。
2.不可重复读。
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。如下面的例子:
1.在事务1中,Mary读取了自己的工资为1000,操作并没有完成。
2.在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务。
3.在事务1中,Mary再次读取自己的工资时,工资变为了2000
3.幻读。
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。如下面的例子:
目前工资为1000的员工有10人。
1.事务1,读取所有工资为1000的员工。
2.这时事务2向employee表插入了一条员工记录,工资也为1000。
3.事务1再次读取所有工资为1000的员工共读取到了11条记录,
数据库隔离级别原理
1.读取未提交(Read Uncommitted)
这是最低的事务隔离级别,读事务不会阻塞读事务和写事务,写事务也不会阻塞读事务,但是会阻塞写事务。这样造成的一个结果就是当一个写事务没有提交的时候,读事务照样可以读取,那么造成了脏读的现象,例如下面的例子:
2.读取已提交 (Read Committed)
采用此种隔离界别的时候,写事务就会阻塞读事务和写事务,但是读事务不会阻塞读事务和写事务,这样因为写事务会阻塞读取事务,那么从而读取事务就不能读到脏数据,如下面的例子:
但是因为读事务不会阻塞其它的事务,这样还是会造成不可重复读的问题。如同下面这个尴尬的例子:
这里我们看到,对于余额而言,在T4时刻买单失败了。因为在T3时刻老婆提交了消费800 元的事务,这时老公可要出洋相了。为了避免这个问题,我们可以使用可重复读的策略,这样就消除了老公无钱买单的尴尬场景。
3.可重复读( Repeatable Read)
采用此种隔离级别,读事务会阻塞写事务 ,但是读事务不会阻塞读事务,但是写事务会阻塞写事务和读事务 。因为读事务阻塞了写事务,这样以来就不会造成不可重复读的问题,但是这样还是不能避免幻影读问题。可重复读是针对于同一条记录而言的,对于不同的记录会发生下面这样的场景:
我们看到老婆在查询之后,老公启动了消费,并先于老婆之前打印账单记录, 所以在T4时刻,打印了1800 元11条记录,这个时候老婆就会去质疑这800 元是不是幻读的。上面和不可重复读很接近,但是我们需要注意的是,不可重复读是针对同一条记录,而幻读是针对删除和插入记录的。为了避免服这个问题我们可以采用序列化的隔离层。序列化就意味着所有的操作都会按顺序执行,不会出现脏读、不可重读和幻读的情况。
4.序列化( serializable)
此种隔离级别是最严格的隔离级别,如果设置成这个级别,那么就不会出现以上所有的问题(脏读,不可重复读,幻影读)。但是这样以来会极大的影响到我们系统的性能,因此我们应该避免设置成为这种隔离级别。
注意:在实践中,我们一般采用读取已提交或者更低的事务隔离级别,配合各种并发访问控制策略来达到并发事务控制的目的。