乐观锁和悲观锁
悲观锁:
-
假设会发生冲突,因此在操作数据之前就对数据加锁,确保其他事务无法访问该数据。常见于对数据一致性要求较高的场景。
-
实现方式 :使用行级锁或表级锁 ,例如可以使用
SELECT ... FOR UPDATE或UPDATE语句来加锁。 -
使用场景 :悲观锁适合并发冲突多,写多读少 的场景。通过每次加锁的形式来确保数据的安全性,吞吐量较低 。但是可能死锁
sql
-- 【读取数据并加锁 FOR UPDATE】
SELECT id, name FROM users WHERE id = 1 FOR UPDATE;
-- 【更新】操作
UPDATE users SET name = 'new_name' WHERE id = 1;
乐观锁:
-
假设不会发生冲突,因此在操作数据时不加锁,而是在更新数据时进行版本控制或校验。如果发现数据被其他事务修改,则会拒绝当前事务的修改,需重新尝试。
-
实现方式 :通常通过版本号或时间戳来实现,每次更新时检查版本号或时间戳是否一致。
-
使用场景 :乐观锁适合并发冲突少,读多写少 的场景,不用通过加锁只需通过比较字段版本号(或时间戳)是否发生改变的形式,无 锁操作,吞吐量较高。
sql
-- 假设有一张用户表 users,包含 id、name 和 version 字段
-- 【查询】数据
SELECT id, name, version FROM users WHERE id = 1;
-- 【更新】数据时检查版本号
UPDATE users
SET name = 'new_name', version = version + 1
WHERE id = 1 AND version = current_version;
死锁
死锁产生
一、事务加锁顺序不一致
这是最常见的死锁原因。多个事务对不同资源的加锁顺序相反,导致循环等待。
示例:
-
事务 A:先更新表 A 的行 1 → 再更新表 B 的行 2
-
事务 B:先更新表 B 的行 2 → 再更新表 A 的行 1
此时事务 A 持有表 A 行 1 的锁,等待表 B 行 2 的锁;事务 B 持有表 B 行 2 的锁,等待表 A 行 1 的锁,形成死锁。
二、锁的粒度选择不当
-
行锁升级为表锁 :如果事务操作的数据范围过大(如未命中索引的
UPDATE/DELETE),InnoDB 会将行锁升级为表锁,增大锁冲突概率,进而引发死锁。 -
间隙锁(Gap Lock)冲突:InnoDB 在可重复读隔离级别下会使用间隙锁防止幻读,若多个事务在同一间隙内插入数据,可能因间隙锁冲突导致死锁。
三、(大)事务持有锁时间过长
事务执行时间过长(如包含大量操作、等待用户输入等),会长时间持有锁,增加与其他事务的锁冲突概率,最终触发死锁。
四、并发修改相同资源的不同维度
多个事务从不同维度(如主键、唯一索引)修改同一批数据,可能因加锁顺序不同导致死锁。
示例:
-
事务 A:按主键 ID=1 更新 → 按唯一索引 name=' 张三 ' 更新
-
事务 B:按唯一索引 name=' 张三 ' 更新 → 按主键 ID=1 更新
五、外键约束触发的隐式锁
外键约束会导致 InnoDB 自动加锁校验关联数据,若多个事务同时操作存在外键关联的表,可能因隐式锁的顺序问题引发死锁。
六、批量操作的锁竞争
批量插入 / 更新(如INSERT ... SELECT、UPDATE ... WHERE)时,若多个事务同时操作重叠的数据范围,容易因锁竞争形成循环等待。
解决死锁
自动检测与回滚:
-
MySQL自带死锁检测机制 ,当检测到死锁时,数据库会自动回滚其中一个事务,以解除死锁。通常会回滚事务中持有最少资源的那个。
-
还可以设置锁等待超时的参数,当获取锁的等待时间超过阈值时,就释放锁进行回滚。
此外还有以下操作:
-
手动kill发生死锁的语句 :可以通过命令,手动快速地找出被阻塞的事务及其线程ID,然后手动
kill它,及时释放资源。 -
避免大事务长时间占据锁。将大事务拆分成多个小事务快速释放锁,可降低死锁产生的概率和避免冲突。
-
调整锁的顺序,大范围修改的优先。在更新数据的时候要保证获得足够的锁,先获取影响范围大的锁;或者固定顺序也可以。
-
更改数据库隔离级别。可重复读比读已提交多了间隙锁和临键锁,降低为读已提交级别可以降低死锁的情况。
-
合理建立索引,减少加锁范围。如果命中索引,则会锁对应的行,避免全表扫描。
排查语句
使用 SHOW ENGINE INNODB STATUS 来获取死锁的日志信息,从而定位到死锁发生的原因。