MySql--锁
摘要
-
MySql知识点介绍: 锁
-
本文基于
mysql-8.0.30,https://dev.mysql.com/doc/refman/8.0/en/
常见面试题
数据库锁
-
从性能上分为
乐观锁(用版本对比来实现)和悲观锁 -
从对数据库操作的类型分,分为
读锁和写锁(都属于悲观锁)
读锁(共享锁,S锁[Shared]):针对同一份数据,多个读操作可以同时进行而不会互相影响。读锁可以认为没有加锁,可读但不可写,当写锁锁住数据时,读锁也会不可获取。
写锁(排它锁,X锁[eXclusive]):当前写操作没有完成前,它会阻断其他写锁和读锁
1 | # 给表加读锁 |
对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻赛对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作
-
从对数据操作的粒度分,分为
表锁和行锁
1 | # 行锁for update,这样其他session只能读这行数据,修改则会被阻塞,直到锁定行的session提交 |
注意行锁的查询条件必须走索引,否则会升级为表锁
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
合理设计索引,尽量缩小锁的范围
尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
-
行锁分析
1 | mysql> show status like 'innodb_row_lock%'; |
-
MyISAM不支持事务且只支持表锁,在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
-
InnoDB支持事务和行锁,在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
-
读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
-
间隙锁(Gap Lock): 锁的就是两个值之间的空隙,用于解决幻读。间隙锁是在可重复读隔离级别下才会生效。
如account表的主键id是不连续的(1,2,3,10,20),那么间隙就有id为 (3,10),(10,20),(20,正无穷) 这三个区间
在Session_1下面执行update account set age = 10 where id > 8 and id <18;
则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在 (3,20]区间都无法修改数据,注意最后那个20也是包含在内的。
注意这里锁住的最后一条记录不是id=18,而是18所在的间隙区间都会锁住。
尽可能减少检索条件范围,避免间隙锁
-
临键锁(Next-key Locks): 是行锁与间隙锁的组合。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁。可重复读里当前读靠临键锁防幻读,和 MVCC 的关系见 MySql--MVCC。
死锁如何排查
先分清两件事,面试和线上都容易混:
| 死锁 Deadlock | 锁等待超时 Lock wait | |
|---|---|---|
| 现象 | 两个(或多个)事务互相等对方已经持有的锁,形成环 | 一方拿着锁不放,另一方干等 |
| InnoDB 怎么处理 | 立刻选一个事务回滚,打破环 | 等到 innodb_lock_wait_timeout(默认 50 秒,见 MySql单节点、主从、双主的构建方法)后抛错 |
| 错误码 | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
ERROR 1205 (HY000): Lock wait timeout exceeded |
| 现场还在不在 | 通常已经没了,被回滚的那个事务锁已释放 | 还能在 PROCESSLIST / data_lock_waits 里抓到阻塞链 |
死锁检测默认开着(innodb_deadlock_detect=ON),靠等待图找环。高并发下检测本身也吃 CPU,极少数场景会关掉它、改用超时兜底,一般不要关。
1. 把现场留下来
死锁发生时 InnoDB 已经自动回滚了其中一个事务,SHOW PROCESSLIST 经常什么都看不到。排查靠的是日志,不是盯着当前线程。
1 | -- 最近一次死锁的完整报告,重点看 LATEST DETECTED DEADLOCK |
innodb_print_all_deadlocks 建议写进配置持久化。只靠 INNODB STATUS 会被下一次死锁覆盖。
正在发生的锁等待(还没形成环、或已经超时前)用 8.0 的 performance_schema,老的 information_schema.INNODB_LOCKS 在 8.0 里删了:
1 | -- 谁持有锁、谁在等 |
线程状态、kill 长事务见 MySql--线程管理。Lock_time 很高的慢 SQL 见 MySql--慢查询。
2. 怎么读 LATEST DETECTED DEADLOCK
报告里通常有两个 TRANSACTION,按下面几行读就够:
-
*** (1) TRANSACTION/*** (2) TRANSACTION:两个事务各自正在执行的 SQL、已持有的锁 -
WAITING FOR THIS LOCK TO BE GRANTED:当前这条语句在等哪一把锁 -
HOLDS THE LOCK(S):它已经拿着、导致对方也在等的锁 -
lock mode:X排它、S共享;后面跟rec but not gap(纯行锁)、gap(间隙)、insert intention(插入意向)、不带修饰则是临键锁 -
index PRIMARY of table ...:锁在哪个索引上。锁在二级索引还是主键,决定交叉顺序 -
*** WE ROLL BACK TRANSACTION (1):牺牲者。InnoDB 一般回滚 undo 更少、回滚更便宜的那个
典型环可以画成:事务 A 持有行 1 的 X 锁,在等行 2;事务 B 持有行 2 的 X 锁,在等行 1。把两边 SQL 和 LOCK_DATA(主键值)对上,就能在代码里复现。
3. 常见成因
-
两个事务更新同一批行,加锁顺序相反。最经典:A 先改 id=1 再改 id=2,B 先 2 后 1。
-
唯一索引上的并发 INSERT:一个事务插入失败或等间隙,另一个插入意向锁和间隙锁互相等。RR 下更明显。
-
where 没走索引,行锁升级成表锁/锁住大量间隙,冲突面被放大。索引失效场景见 MySql索引。
-
事务太大、持锁时间太长:中间还打 RPC、循环里一条条
SELECT ... FOR UPDATE。 -
外键、级联更新把锁扩到父表/子表,表面上在改 A,实际还锁了 B。
4. 怎么处理
-
应用必须重试 (1213)。错误信息里已经写了
try restarting transaction,死锁被解开后重跑一遍通常就能成功。这是正规解法,不是权宜之计。 -
业务里约定相同的加锁顺序,例如一律按主键从小到大
FOR UPDATE。 -
缩小事务:能查的先查,锁语句放最后,尽快
COMMIT;不要在持锁时做远程调用。 -
让检索走索引,锁住真正要改的那几行,避免 RR 下大范围间隙锁。幻读不敏感的业务可以把隔离级别降到 RC,少掉间隙锁,见 MySql--事务。
-
热点行(库存、计数器)考虑排队、合并更新,而不是靠无限加大超时。
-
锁等待(1205)才去
kill阻塞会话;死锁已经被引擎回滚了,再 kill 没有现场。
MySql死锁问题如何排查?
- 先分清:死锁是环,InnoDB 立刻回滚一个事务,报
1213;锁等待是单边阻塞,等到innodb_lock_wait_timeout报1205。死锁发生后现场通常没了,不要只靠PROCESSLIST。 - 留证据:
SHOW ENGINE INNODB STATUS看LATEST DETECTED DEADLOCK;线上打开innodb_print_all_deadlocks打到 error log。正在等锁用sys.innodb_lock_waits/performance_schema.data_locks(8.0 已无INNODB_LOCKS)。 - 读报告:两个事务各自 HOLD 和 WAIT 的锁模式、索引、
LOCK_DATA,再看WE ROLL BACK TRANSACTION是哪一方。 - 成因多半是加锁顺序交叉、唯一键并发插入、没走索引锁范围过大、事务过长。
- 处理:应用捕获 1213 后重试;统一加锁顺序;缩小事务和锁范围;必要时 RR 改 RC。不要指望靠加大超时消灭死锁。