数据库加锁分析

"dead lock"

Posted by yueLng on 2019-12-13

数据库事务ACID

  1. 原子性(Atomicity): 一个事务必须被视为一个不可分割的最小工作单元,事务中的操作要么全部执行成功,要么全部失败会滚,不可能只执行其中的一部分。
  2. 一致性(consistency) 数据库总是要从一个一致的状态转换到另一个一致的状态。需要确保当从A减去100时若系统崩溃,账户不会损失100元。
  3. 隔离性(isolation)通常来说,一个事务在最终提交前对其他事务是不可见的。为满足不同的并发场景,定义了不同的隔离级别,从低到高是读未提交,读已提交,可重复读和序列化。序列化能够保证最强的一致性,其他隔离级别的一致性依次将此,并发性更好。读未提交会看到其他事务没有提交的操作,称为脏读。读已提交只能看到其他事务已经提交的修改,但是不能重复读,因为一个事务内两次读看到的结果不同。可重复读解决了不可重复读的问题,但是有幻读的问题,就是在读取某个范围的记录时,其他事务在该范围内插入了新的数据,两次读的结果不一致。InnoDB通过多版本并发控制(MVCC)解决了幻读的问题,可重复读是MySQL的默认隔离级别。序列化是最高的隔离级别,会让事务串行执行,在读取的每一行数据加锁,会造成大量的锁征用。
  4. 持久性(durability) 持久性要保证提交了事务后,即使系统崩溃,修改的数据也不会丢失。持久性也分为不同的级别,通过备份等手段可以提高持久性。

MVCC

MVCC其实很类似一个特殊的cas,它保证了涉及到跨多key修改的原子操作语义,这样也可以理解为什么MVCC可以把并发粒度控制得更好。

SQL分析

  1. SELECT … FROM 是一个快照读,通过读取数据库的一个快照,不会加任何锁,除非将隔离级别设置成了 SERIALIZABLE 。在 SERIALIZABLE 隔离级别下,如果索引是非唯一索引,那么将在相应的记录上加上一个共享的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。

  2. SELECT … FROM … LOCK IN SHARE MODE 语句在所有索引扫描范围的索引记录上加上共享的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。

  3. SELECT … FROM … FOR UPDATE 语句在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。这将堵塞其他会话利用SELECT … FROM … LOCK IN SHARE MODE 读取相同的记录,但是快照读将忽略记录上的锁。

  4. UPDATE … WHERE …语句在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。

当UPDATE 操作修改主键记录的时候,将在相应的二级索引上加上隐式的锁。当进行重复键检测的时候,将会在插入新的二级索引记录之前,在其二级索引上加上一把共享锁。

  1. DELETE FROM … WHERE … 语句在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。

  2. INSERT 语句将在插入的记录上加一把排他锁,这个锁是一个index-record lock,并不是next-key 锁,因此就没有gap 锁,他将不会阻止其他会话在该条记录之前的gap插入记录。

在插入记录之前,将会加上一种叫做 insert intention gap 的 gap 锁。这个 insert intention gap表示他有意向在这个index gap插入记录,如果其他会话在这个index gap中插入的位置不相同,那么将不需要等待。假设存在索引记录4和7,会话A要插入记录5,会话B要插入记录6,每个会话在插入记录之前都需要锁定4和7之间gap,但是他们彼此不会互相堵塞,因为插入的位置不相同。

如果出现了重复键错误,将在重复键上加一个共享锁。如果会话1插入一条记录,没有提交,他会在该记录上加上排他锁,会话2和会话3都尝试插入该重复记录,那么他们都会被堵塞,会话2和会话3将尝试在该记录上加一个共享锁。如果此时会话1回滚,将发生死锁。

为什么会发生死锁呢?当会话1进行回滚的时候,记录上的排他锁释放了,会话2和会话3都获得了共享锁。然后会话2和会话3都想要获得排他锁,进而发生了死锁。
会话1在该记录上拥有一把排他锁,会话2和会话3都碰到了重复记录,因此都在申请共享锁。当会话1提交之后,会话1释放了排他锁,之后的会话2会话3先后获得了共享锁,此时他们发生了死锁,因为会话2和会话3都无法或者排他锁,因为彼此都占用了该记录的共享锁。

  1. INSERT … ON DUPLICATE KEY UPDATE 和普通的INSERT并不相同。如果碰到重复键值,INSERT … ON DUPLICATE KEY UPDATE 将在记录上加排他的 next-key锁。

  2. REPLACE 在没有碰到重复键值的时候和普通的INSERT是一样的,如果碰到重复键,将在记录上加一个排他的 next-key锁。

  3. INSERT INTO T SELECT … FROM S WHERE … 语句在插入T表的每条记录上加上 index record lock 。如果隔离级别是 READ COMMITTED, 或者启用了 innodb_locks_unsafe_for_binlog 且事务隔离级别不是SERIALIZABLE,那么innodb将通过快照读取表S(no locks)。否则,innodb将在S的记录上加共享的next-key锁。

CREATE TABLE … SELECT … 和 INSERT INTO T SELECT … FROM S WHERE … 一样,在S上加共享的next-key锁或者进行快照读取((no locks)

  1. REPLACE INTO t SELECT … FROM s WHERE … 和 UPDATE t … WHERE col IN (SELECT … FROM s …) 中的select 部分将在表s上加共享的next-key锁。

  2. 当碰到有自增列的表的时候,innodb在自增列的索引最后面加上一个排他锁,叫AUTO-INC table lock 。AUTO-INC table lock会在语句执行完成后进行释放,而不是事务结束。如果AUTO-INC table lock被一个会话占有,那么其他会话将无法在该表中插入数据。innodb可以预先获取sql需要多少自增的大小,而不需要去申请锁,更多设置请参考参数innodb_autoinc_lock_mode.

12.如果一张表的外键约束被启用了,任何在该表上的插入、更新、删除都将需要加共享的 record-level locks来检查是否满足约束。如果约束检查失败,innodb也会加上共享的 record-level locks。

  1. lock tables 是用来加表级锁,但是是MySQL的server层来加这把锁的。当innodb_table_locks = 1 (the default) 以及 autocommit = 0的时候,innodb能够感知表锁,同时server层了解到innodb已经加了row-level locks。否则,innodb将无法自动检测到死锁,同时server无法确定是否有行级锁,导致当其他会话占用行级锁的时候还能获得表锁。

数据库死锁问题