试想,事务如果都是串行的,那么就不需要锁了,但是性能肯定没法接受。加锁只是为了提高事务并行度,并且解决并发事务执行过程中引起的脏写、脏读、不可重复读、幻读这些问题的一种解决方案(MVCC算是一种解决脏读、不可重复读、幻读这些问题的一种解决方案),一定要意识到加锁的出发点是为了解决这些问题,不同情景下要解决的问题不一样,才导致加的锁不一样。当然,有时候因为MySQL具体的实现而导致一些情景下的加锁有些不太好理解,这就得我们死记硬背了~
一、丢失修改(lost update)
丢失修改简单来说就是一个事务的更新操作会被另一个事务的更新操作所覆盖。从而导致数据的不一致。例如:事务A与事务B从数据库中读入同一数据并修改,事务A修改完提交事务,随后事务B也修改完提交事务,结果就是事务B提交的结果覆盖了事务A提交的结果。这既是丢失更新问题。
举例:比如就拿订票系统来说,现订票系统还有20张票,两个用户同时各自提交了一个事务。
第1步:事务A查看系统剩余票数还有20张。那么事务A做更新操作(买一张票,那么系统会把它读到的总票数减1,还剩19张)。此时还没有提交事务。
第2步:事务B查看系统剩余票数还有20张。那么事务B也做更新操作(买1张票,那么系统也会把它读到的总票数减1,也剩余19张)。此时还没有提交事务。
第3步:事务A提交事务。
第4步:事务B提交事务。
这个时候发生了什么情况?两个人一共买了两张票而数据库中的记录只是减掉了1张票,这个就是丢失修改问题。
但是,在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为,即使是READ UNCOMMITTED的事务隔离级别,对于行的DML操作,需要对行或其他粗粒度级别的对象加锁(U锁)。因此上面的事务B并不能对事务A正在操作的数据进行更新操作,其会被阻塞,直到事务A结束。
虽然数据库可以阻止丢失更新问题的产生,但是在生成应用中还有另一个逻辑意义的丢失更新问题,而导致该问题的并不是因为数据库本身的问题。实际上,在所有多用户计算机系统环境下都有可能产生这个问题。简单地来说,出现下面的情况时,就会发生丢失更新:
第一步:事务A查询一行数据,放入本地内存,并显示给一个终端用户UserA。
第二步:事务B也查询该行数据,并将取得的数据显示给终端用户UserB。
第三步:UserA修改这行记录,更新数据库并提交。
第四步:UserB修改这行记录,更新数据库并提交。
显然,这个过程中用户UserA的修改更新操作“丢失”了。这个结果对于银行系统或其他事务系统来说也是不可接受的。要避免丢失更新的发生,其实需要让这种情况下的事务变成串行操作,而不是并发的操作。即在上述四步操作时的第(1)步对用户读取的记录加上一个排他锁(FOR UPDATE),同样,发生第(2)步情况下的操作时,用户也需要加上一个排他锁(FOR UPDATE)。这样一来,第(2)步就必须等待第(1)、(3)步完成,最后完成第(4)步。
有的人可能会奇怪,在上述的例子中为什么不直接使用UPDATRE语句,而首先要进行SELECT的操作。的确,直接使用UPDATE可以避免丢失更新问题的产生,然而在实际应用中,应用程序可能需要首先检测用户的余额信息,查看是否可以进行转账操作,然后再进行最后的UPDATE操作,因此在SELECT与UPDATE操作之间可能还存在一些其他的SQL操作。
二、脏读(dirty read)
在理解脏读之前,需要理解脏数据的概念。但是别把脏数据和脏页混淆,脏页指的是在缓冲池中已经被修改的页,但是没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了redo 日志文件中。而所谓脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。
对于脏页的读取,是非常正常的。脏也是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终会达到一致性,即当脏页都刷回到磁盘)。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。
但脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另一个事务中未提交的数据,则显然违反了数据库的隔离性标准。简单来说“脏读”指的是一个事务可以读到另一个事务中未提交的数据。如,事务A修改某一数据并写回缓存池,然后事务B又读取该数据。事务A由于某种原因被撤销,数据恢复原值,从而导致事务B读取的数据为错误数据,也就是脏数据。一般出现“脏读”都是在Read Uncommitted 隔离级别下才会发生,也就是事务会读取到另一个事务没有提交的数据。但是MySQL 默认是RR 隔离级别,而Oracle 则在RC 隔离级别,所以基本不会出现“脏读”的问题。
三、不可重复读(non-repeatable read)
不可重复读是指在一个事务内相同查询语句多次查询时结果不同。比如,事务A读取了一行数据,事务B接着修改或者删除了这行数据,当事务A再次读取同一行数据的时候,读到的数据时修改之后的或者发现已经被删除。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为“不可重复读”。
不可重复读和脏读的区别是:脏读是读到未提交的数据,而“不可重复读”读到的确实是已提交的数据,但是其违反了ACID中的I,即隔离性。下面通过一个例子来观察不可重复读的情况,在《InnoDB行锁算法介绍》这篇博客中也详细介绍了“不可重复读”问题。所以目前不可重复读只有在RC隔离级别下才会发生。
如下实例:
# 事务A
mysql> set tx_isolation='read-committed';
mysql> begin;
mysql> select * from test.t;
+---+
| a |
+---+
| 1 |
+---+
1
2
3
4
5
6
7
8
mysql>settx_isolation='read-committed';
mysql>begin;
mysql>select*fromtest.t;
+---+
|a|
+---+
|1|
+---+
# 事务B
mysql> set tx_isolation='read-committed';
mysql> begin;
mysql> insert into test.t select 2;
mysql> commit;
1
2
3
4
mysql>settx_isolation='read-committed';
mysql>begin;
mysql>insertintotest.tselect2;
mysql>commit;
# 事务A
mysql> select * from test.t;
+---+
| a |
+---+
| 1 |
| 2 |
+---+
1
2
3
4
5
6
7
mysql>select*fromtest.t;
+---+
|a|
+---+
|1|
|2|
+---+
一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库厂商将其数据库事务的默认级别设置为“读已提交”,在这种隔离级别下允许不可重复读的现象。而MySQL使用的是“可重复读”隔离级别。
在InnoDB存储引擎中,通过使用Next-Key Lock算法来避免不可重复读的问题。在MySQL官方文档中将不可重复读的问题定义为Phantom Problem,即幻象问题。在Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(gap)。因此在这个范围的插入都是不允许的。这样就斌免了另外的事务在这个范围内插入数据导致的不可重复读的问题。因此,InnoDB存储引擎的默认事务隔离级别时REPEATABLE READ,采用Next-Key Lock算法,避免了不可重复读的问题。
四、幻读(Phantom)
幻读,是指当事务不是独立执行时发生的一种现象,事务A读取了满足某条件的一个数据集,事务B插入了一行或者多行数据满足了A的选择条件,导致事务A再次使用同样的选择条件读取的时候,得到了比第一次读取更多的数据集,就好象发生了幻觉一样。虽然 MySQL 在 Repeatable Read 隔离级别也解决了幻读问题,但也没有完全去解决。
我们在 MySQL 的 Repeatable Read 隔离级别下来复现一下这个没去完全解决的幻读问题。
事务A
事务B
begin;
select * from z
(没有数据)
insert into z select 1
update z set id=2
(影响一行)
select * from z
(查询一行)
对于“不可重复读”及“幻读”问题,有时候很容易搞混淆。但从定义上来看,“不可重复读”主要针对update操作,对上一次读到的数据再次读取时发生了改变;而“幻读”主要针对insert及delete操作,对上一次读取到的数据条数变多或变少了。
你如果去测试 PostgreSQL、SQL Server 可能得到的结果与 MySQL 不同,它们在 Repeatable Read 隔离级别不会产生幻读,在事务A更新数据的时候就不会看见事务B插入的数据。这主要与 MySQL 实现 Repeatable Read 隔离级别的方式有关,MySQL 中 DML 操作都是当前读,所以上面事务A才会更新了事务B的插入操作,那么也就把这一行的 trx_id 更新为事务A的 id 了。由于 MySQL 对记录可见性规则规定(涉及到 MVCC 的实现与机制),如果行的 trx_id 与当前事务 id 相同,那么就读,所以就发生了上面的幻读现象。
其实在 ANSI SQL 标准定义中,Repeatable Read 隔离级别不需要解决“幻读”,只解决“不可重复读”,Serializable 隔离级别解决幻读。所以 MySQL 这种实现方式是符合 ANSI SQL STANDARD,并非属于实现上的 BUG。
看一下 ANSI SQL STANDARD 对于各种隔离级别发生幻读的规定:
如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。