Mysql的InnoDB存储引擎锁机制 1. 事务 2. 锁的粒度和类型 3. 一致性的非锁定读 4. Select ... For Update 和 Select ... Lock In Share Mode 5. 锁的算法 6. 锁问题

Mysql的InnoDB存储引擎锁机制
1. 事务
2. 锁的粒度和类型
3. 一致性的非锁定读
4. Select ... For Update 和 Select ... Lock In Share Mode
5. 锁的算法
6. 锁问题

1.1 事务概述

  事务用来保证数据库的完整性——要么都修改,要么都不修改。事务必须满足ACID四个特性。

  • 原子性(atomicity),指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作执行都成功,才算整个事务成功。如果事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
  • 一致性(consistency),指事务将数据库从一种状态转变为下一种一致的状态,在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
  • 隔离性(isolation),一个事务的影响在该事务提交前对其他事务都不可见——这通过锁来实现。
  • 持久性(durability),事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。

1.2 事务隔离级别

  ISO和ANIS SQL标准制定了四种事务隔离级别的标准:

  • READ UNCOMMITEED,读未提交,会出现脏读问题。
  • READ COMMITTED,读已提交,会出现幻读问题。
  • REPEATABLE READ,可重复读(InnoDB存储引擎的默认隔离级别)
  • SERIALIZABLE,会给每一个读操作加一个共享锁,不支持一致性的非锁定读,隔离性最高。

2. 锁的粒度和类型

  InnoDB既支持行级锁,也支持表级锁,默认情况下是采用行级锁。
InnoDB存储引擎实现了两种标准的行级锁:

  • 共享锁(S Lock),允许事务读一行数据,不允许其他事务获取排他锁。
  • 排他锁(X Lock),允许事务删除或者更新一行数据,不允许其他事务获得共享锁或排他锁。
      InnoDB存储引擎支持多粒度锁定,允许行级锁和表级锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎提供了意向锁。意向锁是表级别的锁,其设计目的主要是为了在一个事务中揭示下一行将被请求的锁的类型。InnoDB存储引擎支持两种意向锁:
  • 意向共享锁(IS Lock),事务想要获得一个表中某几行的共享锁。
  • 意向排他锁(IX Lock),事务想要获得一个表中某几行的排他锁。
      意向锁不会阻塞除全表扫描以外的任何请求。

3. 一致性的非锁定读

  一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE、UPDATE操作,这是读取操作不会因此而等待行上锁的释放,相反,InnoDB存储引擎会去读取行的一个快照数据。如下图所示:
    Mysql的InnoDB存储引擎锁机制
1. 事务
2. 锁的粒度和类型
3. 一致性的非锁定读
4. Select ... For Update 和 Select ... Lock In Share Mode
5. 锁的算法
6. 锁问题

  上图可以看出,读取的数据是一份快照数据,快照数据是指执行当前更改操作前的数据,该实现是通过undo段来实现的,所以快照数据本身是没有额外的开销的。因为快照数据是一份历史数据,是只读的,所以不需要上锁。
  快照可能有多个版本,也就是说可能有多份不同的快照数据,这种技术称为行多版本技术,由此带来的并发控制称为多版本并发控制(Multi Version Concurrency Control, MVCC)
  一致性非锁定读是InnoDB存储引擎默认的读取方式,但是不同的事务隔离级别下,读取的方式不同,并不是每个事务隔离级别下读取的都是一致性读,即使都是一致性读,不同的事务隔离级别读取的快照数据也不同。例如Read Commited和Repeatable Read下,使用的都是一致性的非锁定读,但它们读取的是不同的快照数据。在Read Commited级别下,读取的总是被锁定行的最新一份快照数据,而在Repeatable Read级别下,读取的是事务开始时的第一份快照数据。

step 1. 初始化测试表
mysql> create table t (id int,
    -> primary key (id))
    -> engine innodb;
Query OK, 0 rows affected (0.62 sec)
mysql> insert into t values(1);
mysql> select * from t;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

step 2. 开启一个会话A,并在会话A中开启一个事务,查看下测试表的数据,但不提交事务
# Session A
mysql> begin;
Query OK, 0 rows affected (0.03 sec)

mysql> select * from t;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

step 3. 开启另一个会话B,模拟并发的情况,在会话B中开启事务,修改测试表中id为1的数据,但不提交
# Session B
mysql> begin ;
Query OK, 0 rows affected (0.00 sec)
mysql> update t set id = 3 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

step 4. 在会话A中查看测试表数据,发现还是修改之前的数据
# Session A
mysql> select * from t;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

setp 5. 在会话B中提交事务
# Session B
mysql> commit;
Query OK, 0 rows affected (0.04 sec)

step 5. 再在会话A中查看测试数据,在Read Commited和Repeatable Read级别下得到的结果就不一样了。对于Read Committed级别,它总是读取最新版本,所以它得到的结果是一个id为3的记录(幻读)。而Repeatable Read级别下,它总是读取事务开始时的行数据,所以它得到的结果仍然是一个id为1的记录。

4. Select ... For Update 和 Select ... Lock In Share Mode

  默认情况下,InnoDB存储引擎的Select操作使用的是一致性非锁定读,但是有些情况下,需要用户主动对读取操作进行加锁。InnoDB存储引擎对Select语句加锁有两种操作:

  • Select ... For Updata,对读取的行记录加一个X锁。其他事务想在这些行上加任何锁都会被阻塞。
  • Select ... Lock In Share Mode,对读取的行加一个S锁。其他事务可以向被锁定的行加S锁,但是如果加X锁会被阻塞。
      对于一致性非锁定读,即使读取的行已被使用Select ... For Update,也是可以读取的。Select ... For Update 和 Select ... Lock In Share Mode 必须在事务中使用,当事务提交了,锁也就释放了。

5. 锁的算法

  InnoDB存储引擎有3种行锁算法:

  • Record Lock,单个记录上的锁。Record Lock锁住的永远是索引而不是记录,如果在建表的时候没有设置索引,InnoDB存储引擎会使用隐式的主键来进行锁定。
  • Gap Lock,间隙锁,锁定一个范围,但不包含记录本身。Gap Lock主要是解决可重复度模式下的幻读问题。
  • Next-Key Lock,Gap Lock + Record Lock,锁定一个范围,并且包括记录本身。

6. 锁问题

6.1 丢失更新

  多个事务同时修改同一行记录,可能出现丢失更新的问题。

  1. 事务A查询了一行数据。
  2. 事务B也查询了这行数据。
  3. 事务A根据它的查询结果修改数据,并提交事务。
  4. 事务B根据它的查询结果修改数据,并提交事务。
      可以看出,事务1的更新操作丢失了。避免丢失更新的做法,就是事务在查询数据的时候加上排他锁。

6.2 脏读

  脏读是指事务A读到了事务B中还没有提交的更新(违反了数据库的隔离性)。脏读只在隔离级别为Read UnCommitted的情况下才会发生。避免脏读的办法,就是把事务的隔离级别至少设置成Read Committed。

6.3 幻读(不可重复读)

  幻读是指在一个事务中多次读同一数据,拿到的结果不同(违反了数据库的一致性)。例如,事务A读了一行数据,事务B修改了这行数据并提交了,事务A再读这行数据,拿到的结果与事务A前一次读取的结果不同。
  幻读一般是可以接收的,因为它读到的确实是其他事务已经提交的数据。
  InnoDB存储引擎中,通过使用Next-Key Lock算法来避免幻读问题。在Next-Key Lock算法下,对于索引的扫描,不仅仅锁住的是扫描到的索引,而且还锁住这些索引覆盖的范围(gap),因此在这个范围内的修改都是不允许的。