latch 与 lock
latch 可以认为是应用程序中的锁, 可以称为闩锁 (轻量级的锁) 因为其要求锁定的时间必须要非常短, 若持续时间长, 则会导致应用性能非常差, 在 InnoDB 存储引擎中, latch 又可以分为 mutex(互斥锁) 和 rwlock(读写锁), 其目的用来保证并发线程操作临界资源的正确性, 并且没有死锁检测的机制
在 InnoDB 存储引擎中的 latch, 可以通过命令 SHOW ENGINE INNODB MUTEX 来进行查看
mysql > SHOW ENGINE INNODB MUTEX;
lock 可以认为是数据库提供的锁, 用来锁定的是数据库中的数据并且一般 lock 对象仅在事务 commit 或 rollback 后进行释放(不同事务隔离级别释放的时间可能不同),lock 是有死锁机制的
在 InnoDB 存储引擎中的 lock 可以通过 show engine innodb status,information_schema.INNODB_LOCKS,INNODB_TRX,INNODB_LOCK_WATIS 信息来查看
线程获取 lock 的流程:
在对数据加 lock 的时候会先对数据所在的页面添加 latch, 然后再对数据添加 lock, 添加完 lock 后再释放页面的 Latch
这种机制主要是为了保证线程获取的行数据的一致性和完整性.
如果 lock 被其他的线程占有, 线程先释放页面 latch, 等待 lock, 待获取 lock 后会再次对页面添加 latch, 查看页面数据是否有改动, 然后尝试再次获取对应的 lock
共享锁与排他锁
innodb 储存引擎提供了如下两种标准的行级锁
共享锁(S) 允许一个事务去读一行
排他锁(X) 允许获得排他锁的事务更新或删除数据
同时 innodb 储存引擎支持多粒度锁定, 为了支持在不同的粒度上进行加锁操作, innodb 支持另一种额外的锁方式, 称之为意向锁
意向共享锁(IS) 事务想要获得一张表中某几行的共享锁
意向排他锁 (IX) 事务想要获得一张表中某几行的排他锁
在行锁的实现上
mysql 提供了三种的行锁的算法
分别是
Record Lock 记录锁, 单个记录上的锁
Gap Lock 间隙锁, 锁定一个范围, 但不包含记录本身
Next-key Lock Gap Lock + Record Lock 锁定一个范围, 并且锁定记录本身
Mysql 是如何加锁的
非特殊注明 默认在 RR 隔离级别下进行讨论
InnoDb 的行锁是对索引加锁的, 对扫描的行边扫描边加锁, 如果走的是二级索引 (非聚簇索引) 除了需要对二级索引加锁外, 还需要根据二级索引里面的主键信息扫描主键的聚簇索引, 对主键加锁,
加锁的数据行数会受到 Mysql 是否支持 Index Condition PushDown 而影响(Mysql 5.6 支持 ICP), 加锁的数量可能远远大于满足条件的记录数量
这里需要加两次锁的原因是
如果
语句 A 使用二级索引对记录 X 进行更新操作,
语句 B 使用聚簇索引对记录 X 进行更新操作,
如果 A 仅对二级索引进行加锁, 那么并发的语句 B 将感受不到语句 A 的存在, 违背了同一条记录上的更新 / 删除必须串行执行的约束
select * from table where?
RC 级别下 : 无需加锁, 一致性非锁定读, 使用快照读, 读取被锁定行的最新一份数据, 因此会出现前后读取数据不一致的情况
RR 级别下: 无需加锁, 一致性非锁定读, 使用快照读, 读取事务开始时的行数据版本, 因此前后读到的数据是一样的
Serializable 级别下: 使用当前读, 需要加锁, innodb 内部将 select 语句转换为了 select lock in share mode
insert?
insert 会对插入成功的行加上记录锁, 不会阻止其他并发的事务往这条记录之前插入记录在插入之前, 会先在插入记录所在的间隙加上一个插入意向意向锁 (并发的事务可以对同一个间隙加插入意向锁锁) 如果 insert 的事务出现了 duplicate-key error , 事务会对 duplicate index record 的记录加共享锁这个共享锁在并发的情况下是会产生死锁的, 比如有两个并发的 insert 都对要对同一条记录加共享锁, 而此时这条记录又被其他事务加上了排它锁, 排它锁的事务将这条记录删除后, 两个并发的 insert 操作会发生死锁
delete?
delete 操作仅是将主键列中对对应的记录 delete flag 设置为 1, 记录并没有被删除, 还是存在于 B + 树中
真正的删除操作被延迟了, 最终在 purge 操作中完成
延迟到 purge 操作的原因是的 innodb 支持 mvcc 多版本控制, 所以记录不能在事务提交时立即进行删除, 只有当对应的行记录不被任何其他事务引用的时候, 才可以由 purge 进行真正的删除
delete 操作过程中:
找到满足条件的记录, 并且记录有效, 则对记录加 X 锁
找到满足条件的记录, 但是记录无效(标识为删除), 则对记录加 next key 锁;
未找到满足条件的记录, 则对第一个不满足条件的记录加 Gap 锁, 保证没有满足条件的记录插入;
update?
对满足条件的记录 next-key 锁, 如果是等值匹配并且使用唯一索引或是聚簇索引, 那么可以只添加记录锁
唯一索引中含 NULL 值的记录, 将不会添加记录锁, 转而为 next-key 锁 因为 NULL 不等于 NULL,NULL 和任何值比较均返回 NULL, 包括 NULL 本身, 但是 NULL is NULL
死锁案例分析
- create table `deadlocktest`
- (
- `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
- `a` bigint(20) unsigned NOT NULL,
- `b` bigint(20) unsigned NOT NULL,
- `c` bigint(20) unsigned NOT NULL,
- `d` bigint(20) unsigned NOT NULL,
- `e` bigint(20) unsigned NOT NULL,
- PRIMARY KEY(`id`),
- UNIQUE KEY `I_a`(`a`),
- KEY `I_b` (`b`),
- KEY `I_c` (`c`)
- )ENGINE=InnoDb ;
- insert into deadlocktest (a,b,c,d,e)values(1,999,3,4,5);
- insert into deadlocktest (a,b,c,d,e)values(2,998,4,5,6);
- insert into deadlocktest (a,b,c,d,e)values(3,997,4,5,6);
- insert into deadlocktest (a,b,c,d,e)values(4,996,3,4,5);
- ...
- insert into deadlocktest (a,b,c,d,e)values(1000,1,3,4,5);
3 个 insert 的死锁
事务 A | 事务 B | 事务 C |
begin; | begin; | begin; |
insert into deadlocktest (a,b,c,d,e)values(4,996,3,4,5); | ||
insert into deadlocktest (a,b,c,d,e)values(4,996,3,4,5); | ||
insert into deadlocktest (a,b,c,d,e)values(4,996,3,4,5); | ||
rollback; | ||
1 row affected | Deadlock found when trying to get lock; try restarting transaction |
事务 A 获得排他锁, 插入数据成功
事务 B 事务 C, 因为记录 duplicate-key error 转而持有行的共享锁
事务 A 回滚, 释放了持有的排他锁, 事务 B 和事务 C 需要获得该行的排他锁, 但是由于互相都持有对应行的共享锁, 互相等待, 造成死锁
2 个 update 的死锁
事务 A | 事务 B |
begin; | begin; |
update deadlocktest force index(I_b) set e = sleep(5) where b>0; | |
update deadlocktest force index(I_c) set e = sleep(5) where c>2; | |
Deadlock found when trying to get lock; try restarting transaction | Rows matched: 4 Changed: 4 Warnings: 0 |
两个 update 事务, 加锁顺序不一样导致的死锁
InnoDb 的行锁是对索引加锁的, 对扫描的行边扫描边加锁, 如果走的是二级索引 (非聚簇索引) 除了需要对二级索引加锁外, 还需要根据二级索引里面的主键信息扫描主键的聚簇索引, 对主键加锁
3 个以上 delete 的死锁
delete from deadlocktest where a=550事务 A | 事务 B | 事务 B |
begin; | begin; | begin |
delete from deadlocktest where a=550 | ||
delete from deadlocktest where a=550 | ||
commit; | 0 rows affected | Deadlock found when trying to get lock; try restarting transaction |
delete 操作仅是将主键列中对对应的记录 delete flag 设置为 1, 实际的删除延迟到 purge 中
delete 删除时如果找到满足条件的记录, 但是记录无效(标识为删除), 则对记录加 next key 锁;
死锁日志
3 个 delete 的死锁比较难以复现, 我是利用如下脚本完成的
- MY_DB="mysql -hxxx -Pxxx -uxxx -pxxx"
- while :
- do
- echo "use test;begin; delete from deadlocktest where a=499;rollback;" | $MY_DB
- done
该类 delete 死锁的出现条件
1 针对唯一索引上等值查询的删除
2 有 3 个以上并发删除操作
3 事务的隔离级别是 RR
4INNODB 储存引擎
参考文献
- https://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html
- http://hedengcheng.com/?p=771#_Toc374698320
- http://hedengcheng.com/?p=844
来源: https://www.cnblogs.com/magicsoar/p/8414638.html