事务并发的可能问题与其解决方案

时间:2024-03-29 21:55:49

一、事务并发的问题

这些问题可以归结为5类,包括3类数据读问题( 脏读、 不可重复读和 幻象读)以及2类数据更新问题( 第一类丢失更新和 第二类丢失更新)

脏读(dirty read) 

A事务读取B事务尚未提交的更改数据,并在这个数据的基础上操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的。来看取款事务和转账事务并发时引发的脏读场景:

事务并发的可能问题与其解决方案

在这个场景中,B希望取款500元而后又撤销了动作,而A往相同的账户中转账100元,就因为A事务读取了B事务尚未提交的数据,因而造成账户白白丢失了500元。在Oracle数据库中,不会发生脏读的情况。  

不可重复读(unrepeatable read) 

不可重复读是指 A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致: 

事务并发的可能问题与其解决方案

这个就我自己觉得问题不大,好像也并没有影响什么。

幻象读(phantom read) 

A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。幻象读一般发生在计算统计数据的事务中,举一个例子,假设银行系统在同一个事务中,两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时,两次统计的总金额将不一致:  

事务并发的可能问题与其解决方案

如果新增数据刚好满足事务的查询条件,这个新数据就进入了事务的视野,因而产生了两个统计不一致的情况。 

不可重复读 和 幻读, 这两者确实非常相似。不可重复读 主要是说多次读取一条记录, 发现该记录中某些列值被修改过。而幻读 主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。

第一类丢失更新

A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来: 

事务并发的可能问题与其解决方案

第二类丢失更新 

A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:  

事务并发的可能问题与其解决方案

 

二、事务隔离级别

为了解决多个事务并发会引发的问题,进行并发控制。数据库系统提供了四种事务隔离级别供用户选择。

1.Read Uncommitted(读未提交): 一个事务不能修改其他事务正在修改的数据,但可以读取到其他事务中尚未提交的修改,这些修改如果未被提交,将会成为脏数据,Oracle有Ver控制,不会有脏读。

2.Read committed(读已提交):只允许读取已经被提交的数据,反过来讲,如果一个事务修改了某行数据且尚未提交,而第二个事务要读取这行数据的话,那么是不允许的。在MySql的InnoDB下,虽然这种操作不被允许,但MySQL不会阻塞住数据的查询操作,而是会查询出数据被修改之前的备份,返回给客户端。MySQL的这种机制称为MVCC(多版本并发控制),就是说数据库在事务并发的过程中对数据维护多个版本,使得不同的事务对不同的数据版本进行读写(MVCC的实现参见引用中的文章)。这样的机制反映在应用中就是,在任何时候对数据库查询总是可以得到数据库中最近提交的数据。为被提交的脏数据被隔离起来,无法被查询到,即防止脏读发生。

3.Repeat Read(可重复读): Repeat Read又比Read Committed更加严格一点,但仍然是在二级*协议的范畴,只是读取过程受到更多MVCC的影响。在Read Committed下,允许一个事务中多次相同查询得到不同的结果,就是所谓的不可重复读问题。这在一些应用中是允许的,所以oracle、SQL server上默认这一隔离级别,但MySQL没有,它默认Repeat Read级别。在这一级别下,有赖于MVCC,同一个事务中的查询只能查到版本号不高于当前事务版本的数据,即事务只能看到该事务开始前或者被该事物影响的数据。反过来说,这一级别下,不允许事务读取在该事务开始后新提交的数据。即防止了不可重复读的发生。

依靠上面的机制,已经做到了在事务内数据内容的不变,但是不能保证多次查询得到的数据数量一致。因为在一个事务执行的过程中别的事务完全可以执行数据插入,当插入了刚好符合查询条件的数据时,就会引发数据查询结果集增加,引发幻读。还有一种情况就是,如果一个事务想插入一条数据,而另一个事务已经插入了含有相同主键的数据,那么当前事务也会被阻塞,并最终执行失败,虽然当前事务根本无法查询到这一条数据,这也是一种幻读。

4.Serializable(可串行化): 最强事务隔离机制Serializable,它遵循三级*协议,使得所有的事务必须串行化执行,只要有事务在对表进行查询,那么在此事务提交前,任何其他事务的修改都会被阻塞。这解决了一切并发问题,但会造成大量的等待、阻塞甚至死锁,使系统性能降低,一般采用Repeat Read和数据库锁相结合方式来替代它。

 

读未提交

事务读不阻塞其他事务读和写,事务写阻塞其他事务写但不阻塞读。

可以通过写操作加“持续-X锁”实现。

读已提交

事务读不会阻塞其他事务读和写,事务写会阻塞其他事务读和写。

可以通过写操作加“持续-X”锁,读操作加“临时-S锁”实现。

可重复读

事务读会阻塞其他事务事务写但不阻塞读,事务写会阻塞其他事务读和写。

可以通过写操作加“持续-X”锁,读操作加“持续-S锁”实现。

串行化

“行级锁”做不到,需使用“表级锁”。

可串行化

如果一个并行调度的结果等价于某一个串行调度的结果,那么这个并行调度是可串行化的。

区分事务隔离级别是为了解决脏读、不可重复读和幻读三个问题的。

事务隔离级别 回滚覆盖 脏读 不可重复读 提交覆盖 幻读
读未提交 x 可能发生 可能发生 可能发生 可能发生
读已提交 x x 可能发生 可能发生 可能发生
可重复读 x x x x 可能发生
串行化 x x x x x

 

三、常用的解决方案

1. 三级加锁协议

一级*协议:事务在修改数据前必须加X锁,直到事务结束(提交或终止)才可释放;如果仅仅是读数据,不需要加锁。。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 一级*协议保证事务T是可恢复的,并且可以防止两个事务同时操作(增,删,改)同一数据问题。在一级*协议中,如果仅仅是读数据不会加锁的,所以它不能保证可重复读和不读“脏”数据。

二级*协议:一级*协议加上事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。 二级*协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级*协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。

三级*协议 :一级*协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。 三级*协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。

2. 两段锁协议(2-phase locking)

加锁阶段:事务在读数据前加S锁,写数据前加X锁,加锁不成功则等待。

解锁阶段:一旦开始释放锁,就不允许再加锁了。

若并发执行的所有事务均遵守两段锁协议,则对这些事务的任何并发调度策略都是可串行化的。

遵循两段锁协议的事务调度处理的结果是可串行化的充分条件,但是可串行化并不一定遵循两段锁协议。

 

 

四、不同的事务隔离级别与其对应可选择的加锁协议

事务隔离级别

加锁协议

读未提交

一级加锁协议

读已提交

二级加锁协议

可重复读

三级加锁协议

串行化

两段锁协议

参考文章:

https://www.cnblogs.com/jmcui/p/9812679.html

https://blog.csdn.net/dingguanyi/article/details/80888441