MySQL(InnoDB剖析):35---锁之(锁问题:脏读、不可重复读、可重复读、丢失更新(可串行化))

一、脏读(未提交读)

脏读不是脏页

  • 脏页在前面的文章中有介绍:https://blog.csdn.net/qq_41453285/article/details/104083744
  • 脏页是指在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中
  • 脏数据是指事物对缓冲池中行记录的修改,并且修改过的记录还没有被提交(commit)
  • 对于脏页的读取,是非常正常的。脏页时因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终回达到一致性,即当脏页都刷回到磁盘)。并且因为脏页的刷新时异步的,不影响数据库的可用性,因此可以带来性能的提高
  • 脏数据截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性
  • 脏读是指:在不同的事务下,当前事务可以读到另外事务未提交的数据。脏读也称为未提交读
  • 脏读发生在隔离级别“READ UNCOMMITTED”下,而目前InnoDB默认的事务隔离级别为“READ REPEATABLE”。SQL Server默认为“READ COMMITTED”,Oracle同样也是“READ COMMITTED”
  • 脏读隔离看似毫无用处,但是在一些比较特殊的情况下还是可以将事务的隔离级别设为“READ UNCOMMITTED”。例如replication环境中的slave节点,并且在该slave上的查询并不需要特别精确的返回值

演示案例

  • 下面是一个脏读的演示过程,我们将一步一步的展现出这个过程

  • 建立一张表,只有一个字段a,然后向表中插入一条记录1
create table t(
    a int not null
)ENGINE=INNODB;

insert into t select 1;

select * from t;

 

  • 会话A:将的隔离级别设置为“READ UNCOMMITTED”,然后开始事务
set @@tx_isolation='read-uncommitted';

begin;

 

  • 会话B:将的隔离级别设置为“READ UNCOMMITTED”,然后开始事务。第一次查询t表,其中的只有一条数据为1
set @@tx_isolation='read-uncommitted';

begin;

select * from t;

  • 会话A:此时向表中插入一条数据,此时会话A还未提交
insert into t select 2;

  • 会话B:此时再去读取表t中的数据,读取到了会话A未提交的数据(此处就是脏读)
select * from t;

  • 然后结束两个事务

二、不可重复读(提交读)

  • 不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务页访问该同一数据接。并做了一些DML操作。因此,在第一个事务中的两次读数据之前,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据时不一样的情况,这种情况称为“不可重复读”。也称为“提交读”
  • 不可重读读与脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的确实已经提交的数据,但是其违反了数据库事务一致性的要求
  • 在MySQL官方文档中将不可重复读的问题定义为“Phantom Problem”,即幻象问题(幻读问题)
  • 脏读发生在隔离级别“READ COMMITTED”下
  • 一般而言,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库厂商(Oracle、SQL Server)将其数据库事务的默认隔离级别设置为“READ COMMITTED”,在这种隔离级别下允许不可重复读的现象

演示案例

  • 下面是一个不可重复读的演示过程,我们将一步一步的展现出这个过程

  • 建立一张表,只有一个字段a,然后向表中插入一条记录1
create table t(
    a int not null
)ENGINE=INNODB;

insert into t select 1;

select * from t;

 

  • 会话A:将的隔离级别设置为“READ COMMITTED”,然后开始事务,查询记录,此时只有一条记录1,会话A此时不提交
set @@tx_isolation='read-committed';

begin;

select * from t;

 

  • 会话B:将的隔离级别设置为“READ COMMITTED”,然后开始事务。向表t中插入一条记录2,也不提交事务
set @@tx_isolation='read-committed';

begin;

insert into t select 2;

 

  • 会话A:此时再去读取记录,发现会话B插入的记录读取不到(此处可以看见“READ COMMITTED”隔离级别解决了脏读的问题) 
select * from t;

 

  • 会话B:此时提交事务
commit;

 

  • 会话A:此时再去查询表t中的内容,发现内容变为2条(此处就是不可重复读)
select * from t;

 

三、可重复读

可重复读解决了不可重复读中的幻读问题,但其还存在幻读问题

  • 在InnoDB中,通过使用Next-Key Lock算法来避免不可重复读的问题
  • 在Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(gap)。因此在这个范围内的插入都是不允许的。这样就避免了另外的事务在这个范围内插入数据导致的不可重复读的问题
  • 可重复读解决了不可重复读中的幻读问题,但是其本身还存在幻读问题
  • 因此,InnoDB的默认事务隔离级别是“READ REPEATABLE”,采用Next-Ket Lock算法,避免了不可重复读的现象

四、丢失更新(可串行化)

  • 丢失更新是指:另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致
  • 例如:
    • ①事务T1将行记录r更新为v1,但是事务T1未提交
    • ②与此同时,事务T2将行记录r更新为v2,事务T2未提交
    • ③事务T1提交
    • ④事务T2提交
  • 在当前的数据库任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为即使是“READ UNCOMMITTED”的事务隔离级别,对于行的DML操作,需要对行或者其他粗粒度级别的对象加锁。因此在上述步骤②中,事务T2并不能对行记录r进行更新操作,其会阻塞,直到事务T1提交
  • SERIALIZABLE(可序列化的)是最高的隔离级别
  • 它通过强制事务串行执行, 避免了前面说的幻读的问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题
  • 实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别

丢失更新案例

  • 虽然数据库能阻止丢失更新问题的产生,但是在生产应用中还有另一个逻辑意义的丢失更新问题,从而导致该问题的并不是因为数据库本身的问题
  • 实际上,在所有多用户计算机系统环境下都有可能产生这个问题。简单的说,出现下面的情况时,就会发生丢失更新:
    • ①事务T1查询一行数据,放入本地内存,并显示给一个终端用户User1
    • ②事务T2也查询该行数据,并将取得的数据显示给终端用户User2
    • ③User1修改这行记录,更新数据库并提交
    • ④User2修改这行记录,更新数据库并提交
  • 显然,这个过程中用户User1的修改更新操作“丢失”了
  • 而这可能会导致一个“恐怖”的结果。设想银行发生丢失更新现象,例如一个用户账号有10000元人民币,他用两个网上银行的客户端分别进行转账操作:
    • ①事务T1:第一次转账9000,因为网络和数据的关系,这时需要等待(此时余额为1000)
    • ②事务T2:但这时用户操作另一个网上银行客户端,转账1元(此时余额为9999)
    • ③事务T1:提交数据,此时将余额1000保存在数据库中
    • ④事务T2:提交数据,此时将余额9999保存在数据库中
  • 如果这两笔操作都成功了,用户的账号余额是9999,第一次转的9000人民币并没有得到更新。但是在转账的另一个账号却会收到这9000,这导致的结果就是钱变多,而账不平
  • 也许会有读者说,不对,我的网银是绑定USE Key的,不会发生这种情况。是的,通过USE Key登录也许可以解决这个问题,但是更重要的是在数据库层解决这个问题,避免任何可能发生丢失更新的情况

避免丢失更新

  • 要避免这种丢失更新的发生,需要让事务在这种情况下的操作变成串行化,而不是并行的操作:

    • 即在上述四个步骤中的①中,对用户读取的记录加上一个排他X锁

    • 同样,在步骤②的操作过程中,用户同样也需要加一个排他X锁

    • 通过这种方式,步骤②就必须等待步骤①和步骤③完成,最后完成步骤④

  • 下标展示了整个过程:

  • 有读者可能会问,在上述的例子中为什么不直接允许update语句,而首先要进行SELECT...FOR UPDATE的操作:
    • 的确,直接使用UPDATE可以避免丢失更新问题的产生
    • 然而在实际应用中,应用程序可能需要首先检测用户的余额信息,查看是否可以进行转账操作,然后再进行最后的UPDATE操作,因为在SELECT和UPDATE之间可能存在一些其他的SQL操作
  • 我发现,程序员可能在了解如何使用SELECT、INSERT、UPDATE、DELETE语句后就开始编写程序。因此,丢失更新时最容易犯得错误,也是最不容易发现的一个错误,因为这种现象只是随机的,不过其可能造成的后果却十分严重
发布了1462 篇原创文章 · 获赞 996 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104317721
今日推荐