【MySQL 进阶】InnoDB 存储引擎中的锁

推荐阅读


开发者在开发应用的时候,最大的难点是: 一方面要最大的利用数据库的并发访问,另外一方面也会确保每个用户能够一致性的读取数据和修改数据。为此就有了锁(locking)机制。InnoDB的锁相比较于其他存储引擎的锁有着非常的优势,类似于Oracle数据库,所以只有正确的认识到InnoDB的锁,才能更好地发挥其性能。


众所周知,InnoDB 存储引擎的锁是行级锁,事实上,InnoDB 不仅仅在数据上有锁的踪迹,在缓存数据中 LRU 操作也必然有锁的参与。


1、Lock 与 Latch


Lock和Latch 都被称之为锁,但在InnoDB 存储引擎中,Latch一般称之为轻量级的锁,他要求锁的时间必须非常短否则应用的性能可能会非常差。

  • Latch的对象是临界资源,比如行记录,缓存等,其分为 Mutex (互斥量)和 RwLock(读写锁),通常情况 Latch并没有死锁的检测机制。
  • Lock的对象是事务,用来锁定的是数据库中的对象,一般情况下Lock会在事务提交或者回滚之后被释放。


对于MySQL 中 Latch 可以通过命令 SHOW ENGINE INNODBMUTEX 来查看信息。 相对而言,Latch 的信息是比较底层,对于开发人员而言,可以通过 SELECT * FROM information_schema.INNODB_TRX 查看锁的信息。




2、共享锁与排它锁


InnoDB 存储引擎实现以下两种行级锁。

  • 共享锁(S Lock) 允许事务读取一行记录
  • **排它锁(X Lock) 允许事务删除或者更新一行记录 **


如果事务T1 已经获取了R行记录的S锁,那么事务T2也可以立即获取到R行记录的S锁,这种情况称之为锁兼容。但是如果事务T3向获取R行的X锁,那么必须等待T1和T2事务均释放其S锁。下表展示共享锁和排它锁的兼容关系。

X 锁S 锁
X 锁不兼容不兼容
S 锁不兼容兼容


MySQL 中使用 `FOR UPDATE` 添加X锁,使用 `LOCK IN SHARE MODE` 添加S锁,比如
-- 为某些记录添加 X 锁

SELECT * FROM XXX WHERE id < 4 FOR UPDATE;


-- 为某些行记录添加S锁
SELECT * FROM XXX WHERE LOCK IN SHARE MODE;

值得注意的是 ,上述代码添加X锁,其锁定的并不只是 ID小于 4 中已存在的行记录,而是锁定所有小于4的记录,即使该ID不存在


此外,InnoDB存储引擎还支持多粒度的锁机制,它能够允许行记录锁,页记录锁与表锁共存,这种锁称之为 意向锁。 同样的意向锁一分为IX锁,IS锁,意向锁将锁定的对象划分为多个层次,意向锁意味着事务希望对更细的粒度上锁。对于下图而言,事务如果想要获取记录1 的X锁,那么其首先需要获取数据表A 的意向写锁(IX),然后在获取数据页1 的 IX锁,然后在获取记录1 的X锁。如果存在锁不兼容的情况,则必须阻塞的等待。
image.png


3、LOCK WAIT 的场景

为了更好地了解锁的功能以及特性,这里演示下在行记录已经存在X锁的情况下,其他事务是不能够更新该行记录。

  1. 连接1 开启事务1,并为 id = 1 的行记录添加了X锁
mysql root@localhost:test> start transaction;
Query OK, 0 rows affected
Time: 0.003s
mysql root@localhost:test> select * from user_innodb where id = 1 for update;
+----+------+-----+
| id | name | age |
+----+------+-----+
| 1  | one2 | 2   |
+----+------+-----+
1 row in set
Time: 0.017s
  1. 连接2开启事务2后,对ID=1的行记录记性更新操作,由于更新操作需要获取该行的X锁,此时该行记录的X锁已经被事务1持有,所以无法获取X锁,进入等待状态
mysql root@localhost:test> start transaction;
Query OK, 0 rows affected
Time: 0.002s
mysql root@localhost:test> update user_innodb set name = 'aaa233312' where id = 1;
  1. 创建连接3 通过SQL SELECT * FROM information_schema.INNODB_TRX\G; 查看连接中的锁
SELECT * FROM information_schema.INNODB_TRX\G;


-- 查询结果(由于篇幅问题删除了部分字段)
***************************[ 1. row ]***************************
trx_id                     | 5919
trx_state                  | LOCK WAIT
trx_started                | 2020-10-31 13:17:17
trx_requested_lock_id      | 139644687678928:40:4:15:139644620569816
trx_wait_started           | 2020-10-31 13:17:17
trx_weight                 | 2
trx_mysql_thread_id        | 34
trx_query                  | update user_innodb set name = 'aaa233312' where id = 1
// 忽略部分字段
***************************[ 2. row ]***************************
trx_id                     | 5918
trx_state                  | RUNNING
trx_started                | 2020-10-31 13:17:03
trx_requested_lock_id      | <null>
trx_wait_started           | <null>
trx_weight                 | 2
trx_mysql_thread_id        | 22
trx_query                  | <null>
// 忽略部分字段

  • trx_id 表示的事务ID
  • trx_state 表示的事务的状态, RUNING 标识事务正在运行 ,LOCK WAIT表示正在等待锁释放
  • trx_started 表示事务开启时间
  • trx_requested_lock_id 表示锁定的资源ID,如果事务不在LOCK WAIT 状态下,那么其值为NULL
  • trx_wait_started 表示事务等待的时间
  • trx_weight 标识事务的权重,在发生死锁的时候会选择其值较小的值进行事务回滚
  • trx_mysql_thread_id MySQL 的线程ID
  • trx_query 事务锁定的SQL语句


所以可以看到,事务5918(也就是上文中的事务1)正在执行,然后事务5919(也就是上文中的事务2)正在等待事务释放锁,其状态为 LOCK WAIT, 所以验证X锁相互之间是不兼容的。


接口提交或者回滚事务1 ,可以看到事务2 执行结果,耗时约为 27S,其中大部分是因为获取X锁时候的阻塞

mysql root@localhost:test> update user_innodb set name = 'aaa233312' where id = 1;
Query OK, 1 row affected
Time: 27.861s


此时是事务1已经提交,事务2还未提交,执行查看事务的操作,可以看到事务2的状态已经不再是LOCK WAIT 而是 RUNNING。

SELECT * FROM information_schema.INNODB_TRX\G;

-- 省略部分字段
***************************[ 1. row ]***************************
trx_id                     | 5919
trx_state                  | RUNNING
trx_started                | 2020-10-31 13:17:17
trx_requested_lock_id      | <null>
trx_wait_started           | <null>
trx_weight                 | 3
trx_mysql_thread_id        | 34
trx_query                  | <null>

4、一致性非锁定读

一致性非锁定读指的是InnoDB 通过多版本控制的方式,来读取行记录,当该行记录存在 X 锁的时候,其不会等待X锁的释放。相反的InnoDB 会读取一个快照版本,如下图所示


image.png


上图直观的展示什么是一致性非锁定读,之所以称其为非锁定读,因为其不需要等待X锁的释放。快照数据SnapshotData 是该行记录以前的版本,其是通过undo日志实现的,而undo日志则保存的是数据修改前的记录,因此一致性非锁定读没有任何额外的开销。此外,读取快照数据是需要上锁的,因为没有操作对历史数据进行修改。这种方式大大提高了MySQL 的并发性能,在InnoDB 存储引擎下,这是默认的方式,即读取不会等待表上的锁释放。不同的事务隔离级别下读取的方式不同并不是每个隔离级别均采用这种方式实现。即使均采用多版本快照的方式实现,那么每个隔离级别对于快照版本的定义也不同。


在事务隔离级别 READ COMMITED(读已提交)和REPEARABLE READ(可重复度)中,采用的是一致性非锁定读。READ COMMITED 读取的快照版本则是最新的快照数据,对于REPEATABLE READ 读取的则是事务开启时候的快照数据。这也是两个事务隔离级别实现的方式。


**如上图可知快照数据其实就是行的历史版本,这种技术称之为多版本控制,由多版本控制出现多版本并发控制即称之为(Multi Version Concurrecy Controller) MVCC. **

5、一致性锁定读

相对于自动的一致性非锁定读而言,一致性锁定读则就需要手动操作实现,上文中提到 使用的 FOR UPDATE 对于行记录加上X锁,由于X锁和S锁不兼容,所以S锁所在的事务进入阻塞状态,等待X锁释放,这就是一致性锁定读。


其一般用在防止更新丢失场景中,比如假设用户A原账户100元,在事务A中用户A向用户B转账100元的的过程中,用户A减少100,用户B增加100,但是在用户A减少100元之后,B账户并未添加100元的时候 提交的时候,在事务2中用户A又向用户C转账50元,然后转账成功,接着用户A继续向用户B转账,此时由于事务的隔离级别的问题,事务A认为此时账户是100元,转账100,更新为0元,那么账户A就能够转出去150元,而实际情况下其只有100元的月,导致账户出现异常。


此时一致性锁定读则可以解决这个问题,在事务A中读取A账户的时候手动添加X锁,在其他事务想要读取的时候也添加X锁,由于X锁相互之间不兼容,那么后者会进入阻塞状态等待前者执行完成在进行读取。

6、主键自增长ID与锁的实现

自增长是数据库中一种非常常见的属性。也是很多DBA或者开发人员将自增长字段作为主键的方式。对于含有自增字段的表,其内会维护一个增长计数器,获取的时候添加X锁,插入的时候回自动增长1,其实这也是一种特殊的表级锁,特殊点在于其锁的释放不用等到事务提交后,而是完成其自增长插入操作后立即释放,这种方式称之为 `AUTO-INC Locking, 这也是为什么事务回滚之后,自增ID却不会回滚的原因**。


但是这种方式有一定的性能问题,一个INSERT 操作都需要等到上一个INSERT完成才能进行,即使不用等到事务完成**。**针对批量INSERT操作的性能问题,从MySQL 5.1.22 开始,MySQL 对于自增长ID进行了一定的优化,它将自增ID分为四类:

  1. insert-like 指的是所有的INSERTREPLACEINSERT..SELECT, LOAD DATA 等INSERTT操作
  2. simple inserts 指的是插入前就知道插入数量的语句,比如 INSERT...VALUES... ,但不包含 INSERT....SELECRT
  3. bulk inserts 值得是插入之前不知道具体行数的语句,比如 REPLACEINSERT...VALUES 等等
  4. mixed inserts 指的是一部分是自增的,一部分是指定ID的,比如 INSERT...VALUES 中插入三条数据,其中两个ID为NULL,一个ID为具体的数字。


针对不同的方式,采用不同的自增方式。对于指导行数的操作,使用的是互斥量(Mutex) 操作,对于 bulk inserts 则采用的常规的 AUTO-INC Locking 机制实现。

7、外键FK与锁的实现

对于外键,其作用主要是检查数据完整性。在InnoDB 存储引擎中父表的参照列(引用父表的列这里称之为参照列),如果没有添加索引,InnoDB 会自动添加一个索引,这样可以避免锁表操作。而Oracle则不会自动添加索引,必须手动添加索引,这也是Oracle 产生死锁的一个原因。


对字表的更新或者插入都必须 SELECT 父表 ,其并不是使用的一致性非锁定读的方式,而是使用 LOCK IN SHARE MODE为父表添加S锁。这是为了防止数据不一致的情况出现,即在添加X锁的情况,再次添加S锁会进入阻塞状态,字表的操作也会进入阻塞状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值