MySQL锁机制!
MySQL锁机制!
月伴飞鱼锁结构
当一个事务想对这条记录做改动时,会看看内存中有没有与这条记录关联的锁结构。
- 当没有的时候就会在内存中生成一个锁结构与之关联。
锁结构
信息:
- trx信息:这个锁结构是哪个事务生成的。
- is_waiting : 当前事务是否在等待。
锁分类
读锁、写锁:
读锁:
- 共享锁,用S表示。
- 针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
写锁:
- 排他锁,用X表示。
- 当前写操作没有完成前,它会阻断其他写锁和读锁。
- 这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。
对读取的记录加S锁:
1 | SELECT ... LOCK IN SHARE MODE; |
对读取的记录加X锁:
1 | SELECT ... FOR UPFATE; |
表锁:
LOCK TABLES t READ
:
- 对表t加表级别的
S锁
。
LOCK TABLES t WRITE
:
- 对表t加表级别的
X锁
。
意向锁:
有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排它锁:
- 如果没有意向锁,T2就需要去检查各个页或行是否存在锁。
- 如果存在意向锁,那么此时就会受到由T1控制的表级别意向锁的阻塞。
如果事务想要获得数据表中某些记录的共享锁,需要在数据表上添加
意向共享锁
。如果事务想要获得数据表中某些记录的排他锁,需要在数据表上添加
意向排他锁
。
- 意向锁会告诉其他事务已经有人锁定了表中的某些记录。
在为数据行加共享/排他锁之前,InooDB会先获取该数据行所在数据表的对应意向锁。
元数据锁(MDL锁):
MDL 的作用是 保证读写的正确性。
- 如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,增加了一列,那么查询线程拿到的结果跟表结构对不上。
当对一个表做增删改查操作的时候,加 MDL读锁。
当要对表做结构变更操作的时候,加 MDL 写锁。
行锁:
行锁(Row Lock)也称为记录锁,就是锁住某一行(某条记录Row)。
间隙锁(Gap Locks
):
MySQL
解决幻读问题方案有两种:
- 使用
MVCC
。- 采用加锁(
Gap Locks
)。图中ID值为8的记录加了
Gap
锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录
。
- ID列的值
(3, 8)
这个区间的新记录是不允许立即插入的。
间隙锁的实现:
当一个事务对索引范围内的记录执行查询时,
MySQL
会在查询的范围内对记录和记录之间的间隙进行加锁。对于范围查询,
MySQL
会在范围内的记录和记录之间的间隙加锁,以确保其他事务无法在这个范围内插入新记录。间隙锁是
Next-Key Locks
的一种特殊形式,同时锁定记录和记录之间的间隙。
- 防止幻读和不可重复读等并发问题的发生。
间隙锁可能会导致一些并发性能问题,特别是在高并发写入场景下。
- 因此,在使用间隙锁时需要谨慎评估并发控制的成本和性能影响。
临键锁(Next-Key Locks):
既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,InnoDB就提出了Next-Key Locks。
- 它是在 可重复读 的情况下使用的数据库锁,Innodb默认的锁就是
Next-Key Locks
。
1、update 命令会施加一个 X 型记录锁,X 型记录锁是写写互斥的。
- 如果 A 事务对 goods 表中 id = 1 的记录行加了记录锁,B 事务想要对这行记录加记录锁就会被阻塞。
2、insert 命令会施加一个插入意向锁,但插入意向锁是互相兼容的。
- 如果 A 事务向 order 表 insert 一条记录,不会影响 B 事务 insert 一条记录。
select……for update锁表还是锁行
如果查询条件用了索引/主键,
select ..... for update
会进行行锁。如果是普通字段(没有索引/主键),
select ..... for update
会进行锁表。
如何有效的避免死锁的发生
设置事务等待锁的超时时间:
- 当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。
- 在 InnoDB 中,参数
innodb_lock_wait_timeout
是用来设置超时时间的,默认值时 50 秒。开启主动死锁检测:
- 主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
- 将参数
innodb_deadlock_detect
设置为 on,表示开启这个逻辑,默认就开启。修改数据库隔离级别为RC:
- MySql默认级别为RR,RC没有间隙锁
Gap Lock
和组合锁Next-Key Lock
,能一定程度的避免死锁的发生。尽量少使用当前读
for update
,数据更新时尽量使用主键。
悲观锁
当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势。
- 因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势。
- 因为乐观锁在执行更新时频繁失败,需要不断重试,浪费
CPU
资源。
乐观锁
乐观锁假设认为数据一般情况下不会造成冲突。
- 所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测。
如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
使用版本号实现乐观锁
- 版本号的实现方式有两种,一个是数据版本机制,一个是时间戳机制。
使用数据版本(
Version
)记录机制实现:
为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的
version
字段来实现。当读取数据时,将
version
字段的值一同读出,数据每更新一次,对此version
值加一。当提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的
version
值进行比对:
- 如果数据库表当前版本号与第一次取出来的
version
值相等,则予以更新,否则认为是过期数据。这种版本号的方法并不是适用于所有的乐观锁场景。
当电商抢购活动时,大量并发进入,如果仅仅使用版本号或者时间戳,就会出现大量的用户查询出库存存在。
- 但是却在扣减库存时失败了,而这个时候库存是确实存在的。
1 | update t_goods |
使用条件限制实现乐观锁
这个适用于做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高。
更新库存操作如下:
- 注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表。
1 | UPDATE t_goods |
MySQL那些场景下会造成死锁?
1、批量更新id没排序,一个update 1,2,另外一个update 2,1,方案是id排序。
2、update 条件不一样导致死锁,尽量主键更新。
1 | update my_table set name = 'test',age = 22 where name = "ischuang"; |
3、插入间隙锁导致的死锁。
4、并发插入唯一索引导致的死锁。
解决方案:
InnoDB 会选择资源最小的事务进行回滚,另一个事务执行成功。
可采取以下措施:
- 尽量避免大事务,降低锁冲突的可能性。
- 死锁回滚后,记录原始 SQL,手动处理。
1 | try { |