隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

不可重复读和幻读问题是指当事务不是独立执行时发生的一种现象。

不可重复读问题
不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。
这是由于查询时系统中其他事务修改的提交而引起的。
比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据
,便得到了不同的结果,称为不可重复读,即原始读取不可重复。

幻读的问题
事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。

**二、事务的并发问题
  1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
  3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
  小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。

  • 悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处 于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

  • 乐观锁

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如 果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。

三、MySQL事务隔离级别

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

mysql默认的事务隔离级别为repeatable-read

Mysql 的脏,幻,不可重复读 - 图1

读未提交

  1. ## 1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
  2. set session transaction isolation level read uncomitted;
  3. start transaction;
  4. select * from account;
  5. +-------+---------+---------+
  6. | id | Name | balance |
  7. +-------+---------+---------+
  8. | 1 | lilei | 450 |
  9. | 2 | hanmei | 16000 |
  10. | 3 | lucy | 2400 |
  11. +-------+---------+---------+
  12. ## 2)打开另一个客户端B,更新表account:
  13. set session transaction isolation level read uncomitted;
  14. start transaction;
  15. update account set balance = balance - 50 where id = 1;
  16. select * from account;
  17. +-------+---------+---------+
  18. | id | Name | balance |
  19. +-------+---------+---------+
  20. | 1 | lilei | 400 |
  21. | 2 | hanmei | 16000 |
  22. | 3 | lucy | 2400 |
  23. +-------+---------+---------+
  24. ## 3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
  25. +-------+---------+---------+
  26. | id | Name | balance |
  27. +-------+---------+---------+
  28. | 1 | lilei | 400 |
  29. | 2 | hanmei | 16000 |
  30. | 3 | lucy | 2400 |
  31. +-------+---------+---------+
  32. ## 4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据
  33. ## 5)在客户端A执行更新语句update account set balance = balance - 50 where id =1
  34. lileibalance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真
  35. ,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别

读已提交

  1. ## 1)打开一个客户端A
  2. set session transaction isolation level read committed;
  3. start transaction;
  4. select * from account;
  5. +-------+---------+---------+
  6. | id | Name | balance |
  7. +-------+---------+---------+
  8. | 1 | lilei | 450 |
  9. | 2 | hanmei | 16000 |
  10. | 3 | lucy | 2400 |
  11. +-------+---------+---------+
  12. ## 2)打开另一个客户端B,更新表account:
  13. set session transaction isolation level read uncomitted;
  14. start transaction;
  15. update account set balance = balance - 50 where id = 1;
  16. select * from account;
  17. +-------+---------+---------+
  18. | id | Name | balance |
  19. +-------+---------+---------+
  20. | 1 | lilei | 400 |
  21. | 2 | hanmei | 16000 |
  22. | 3 | lucy | 2400 |
  23. +-------+---------+---------+
  24. ## 3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
  25. select * from account;
  26. +-------+---------+---------+
  27. | id | Name | balance |
  28. +-------+---------+---------+
  29. | 1 | lilei | 450 |
  30. | 2 | hanmei | 16000 |
  31. | 3 | lucy | 2400 |
  32. +-------+---------+---------+
  33. ## 4)客户端B的事务提交
  34. commit;
  35. ## 5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
  36. select * from account;
  37. +-------+---------+---------+
  38. | id | Name | balance |
  39. +-------+---------+---------+
  40. | 1 | lilei | 450 |
  41. | 2 | hanmei | 16000 |
  42. | 3 | lucy | 2400 |
  43. +-------+---------+---------+
  44. select * from account;
  45. +-------+---------+---------+
  46. | id | Name | balance |
  47. +-------+---------+---------+
  48. | 1 | lilei | 400 |
  49. | 2 | hanmei | 16000 |
  50. | 3 | lucy | 2400 |
  51. +-------+---------+---------+

可重复读 — Mysql默认

  1. ## 1)打开一个客户端A
  2. set session transaction isolation level repeatable read;
  3. start transaction;
  4. select * from account;
  5. +-------+---------+---------+
  6. | id | Name | balance |
  7. +-------+---------+---------+
  8. | 1 | lilei | 400 |
  9. | 2 | hanmei | 16000 |
  10. | 3 | lucy | 2400 |
  11. +-------+---------+---------+
  12. ## 2) 打开另一个客户端B,更新表account:
  13. set session transaction isolation level read uncomitted;
  14. start transaction;
  15. update account set balance = balance - 50 where id = 1;
  16. select * from account;
  17. +-------+---------+---------+
  18. | id | Name | balance |
  19. +-------+---------+---------+
  20. | 1 | lilei | 350 |
  21. | 2 | hanmei | 16000 |
  22. | 3 | lucy | 2400 |
  23. +-------+---------+---------+
  24. commit;
  25. ## 3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题
  26. ## 4)在客户端A,接着执行update balance = balance - 50 where id = 1,
  27. balance没有变成400-50=350lileibalance值用的是步骤(2)中的350来算的,
  28. 所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,
  29. select操作不会更新版本号,是快照读(历史版本);
  30. insertupdatedelete会更新版本号,是当前读(当前版本)。
  31. select * from account;
  32. +-------+---------+---------+
  33. | id | Name | balance |
  34. +-------+---------+---------+
  35. | 1 | lilei | 400 |
  36. | 2 | hanmei | 16000 |
  37. | 3 | lucy | 2400 |
  38. +-------+---------+---------+
  39. update account set balance = balance - 50 where id = 1;
  40. select * from account;
  41. +-------+---------+---------+
  42. | id | Name | balance |
  43. +-------+---------+---------+
  44. | 1 | lilei | 300 |
  45. | 2 | hanmei | 16000 |
  46. | 3 | lucy | 2400 |
  47. +-------+---------+---------+
  48. ## 5) 重新打开客户端B,插入一条新数据后提交
  49. start transaction;
  50. insert into account values(4, 'lily', 700);
  51. commit;
  52. ## 6)在客户端A查询表account的所有记录,没有 查出 新增数据,所以没有出现幻读
  53. select * from account;
  54. +-------+---------+---------+
  55. | id | Name | balance |
  56. +-------+---------+---------+
  57. | 1 | lilei | 300 |
  58. | 2 | hanmei | 16000 |
  59. | 3 | lucy | 2400 |
  60. +-------+---------+---------+

串行化

  1. ## 1)打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值
  2. set session transaction isolation level serializable;
  3. start transaction;
  4. select * from account;
  5. +------+--------+---------+
  6. | id | name | balance |
  7. +------+--------+---------+
  8. | 1 | lilei | 10000 |
  9. | 2 | hanmei | 10000 |
  10. | 3 | lucy | 10000 |
  11. | 4 | lily | 10000 |
  12. +------+--------+---------+
  13. ## 2)打开一个客户端B,并设置当前事务模式为serializable,插入一条记录报错,表被锁了插入失败
  14. mysql中事务隔离级别为serializable时会锁表
  15. ,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
  16. set session transaction isolation level serializable;
  17. start transaction;
  18. insert into account values(5,'tom',0);
  19. ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

1、事务隔离级别为读提交时,写数据只会锁住相应的行
  2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
  3、事务隔离级别为串行化时,读写数据都会锁住整张表
   4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
   5、MYSQL MVCC实现机制参考链接:https://blog.csdn.net/whoamiyang/article/details/51901888
   6、关于next-key 锁可以参考链接:https://blog.csdn.net/bigtree_3721/article/details/73731377