概念
对于数据库中并发事务的 读-读 情况并不会引发什么问题。对于 写-写 、读-写 或 写-读 这些情况可能会引起一些问题,需要使用MVCC或者加锁的方式来解决问题。在使用加锁的方式解决问题时,由于既要允许 读-读 情况不受影响,又要使 写-写 、 读-写 或 写- 读 情况中的操作相互阻塞,所以MySQL实现一个由梁总类型的锁组成的锁系统来解决。这两种类型的锁通常被称为 共享锁 (Shared Lock , S Lock) 和 排他锁 (Exclusive Lock, X Lock)。
- 读锁:也称为共享锁、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会相互影响,相互不阻塞。
- 写锁:也称为排他锁、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的统一资源。
注意:对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。
X锁 | S锁 | |
---|---|---|
X锁 | 不兼容 | 不兼容 |
S锁 | 不兼容 | 兼容 |
1、锁定读
在采用 加锁 的方式解决 脏读、 不可重复读、幻读这些问题时,读取一条记录时需要获取该记录的 S 锁,其实是不严谨的,有时候需要在读取记录时就获取记录的 X锁,来禁止别的事务读写该记录,为此MySQL提出了两种比较特殊的select 语法:
对读取的记录加S锁:
SELECT .. LOCK IN SHARE MODE;
# 或
SELECT .. FOR SHARE; # 8.0新增语法
在普通的select语句后边加 LOCK IN SHARE MODE ,如果当前事务执行了该语句,那么它会读取到的记录都会加 S锁,允许别的事务继续获取这些记录的S锁(比方说别的事务也是用 SELECT … LOCK IN SHARE MODE 语句来读取这些记录),但是不能获取这些记录的 X锁(比如使用SELECT … FOR UODATE 语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的 X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的 S锁释放掉。
对读取的记录加X锁:
SELECT ... FOR UPDATE;
如果别的事务想要获取或修改这条记录,不管是X锁 还是 S锁,都会阻塞,直到当前事务提交之后,将这些记录上的X锁释放掉,才能操作这些记录。
测试 S锁
开启事务
在另外一个窗口,S锁查询
修改操作测试
阻塞中…在开辟一个窗口测试X锁
依然是阻塞中…我们 提交后,测试
超时等待异常,commit后修改数据成功!并且不会阻塞其他的查询!
测试 X锁
开启事务,提交查询语句
在另一个窗口下查询,结果阻塞中…最终超时。当然,你在本窗口查询,是能够查询出数据的。包括在同一事务中,你的任何CRUD都是可以执行,但是一旦在另一个窗口,相当于新的事务中,那么CRUD是无法进行的。
A窗口的修改
B窗口的修改
但是不会阻塞普通查询
MySQL8.0特性:
在5.7之前的版本,SELECT … FOR UPDATE,如果获取不到锁,会一直等待,直到 innodb_lock_wait_timeout超时。在8.0版本中,SELECT … FOR UPDATE,SELECT … FOR SHARE 添加了NOWAIT、SKIP LOCKED语法,跳过锁等待,或者跳过锁定。
这个跳过的语法,相当于本次查询跳过,不代表所有请求都跳过,依然这个事务还是没有结束,需要commit。
如果是NOWAIT执行,会出现下面的错误提示!
如果是SKIP LOCKED执行,会出现查询空的提示!
2、写操作
一定会加X锁。
平常用到 UPDATE、DELETE、INSERT
DELETE:
对一条记录做delete操作的过程,首先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,在执行这条记录的X锁,在执行delete mark操作。
UPDATE:
- 未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化
- 先在B+树中定位到这条记录的位置,然后在获取一下记录的X锁,最好在原纪录的位置进行修改操作。
- 未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发送变化
- 先在B+树中定位到这条记录的位置,然后获取该记录的X锁,将该记录彻底删除后(就是把记录彻底移入垃圾链表),最后在插入一条新纪录。
- 修改了该记录的键值