希望通过本篇回答以下问题:
- 不同锁的适用场景,加锁、释放锁的方式?
- 不同锁在具体情况下会锁定哪些数据?
- 什么样的生产环境下会产生死锁,如何解决?
引子
锁的引入通常是为了处理并发问题,保证数据安全。
MySQL中有哪些锁? 需要分不同维度来回答。
- 按照使用方式上:排它锁、共享锁
- 按照加锁粒度上:全局锁、表级锁、行级锁
- 按照思想上:乐观锁、悲观锁
按使用方式划分
共享锁
概念 Share lock,也叫读锁。当对象被锁定时,允许其他事务读取该对象,也允许其他事务从该对象上获取共享锁,但是不能写入。
适用场景 说是优势更合适:允许多个事务同时读数据,提高系统的并发读能力。
加锁、释放锁方式
-- 加锁的两种方式
-- 方式1
select ... lock in share mode;
-- 方式2
select ... for share;
-- 释放锁的方式
-- 1. 事务提交 commit;
-- 2. 事务回滚 rollback;
-- 3. 断开连接,会释放该连接中所有的锁;
共享锁和排它锁不讨论其实际会锁定那些行,这个放到粒度里去讨论。
排它锁
概念 Extention Lock,写锁、独占锁。阻塞其他事务对当前对象的读写操作。
适用场景
- 防止并发修改
- 保证唯一性约束
- 避免幻读
加锁、释放锁方式
-- 加锁
select ... for update;
-- 释放锁的方式
-- 1. 事务提交 commit;
-- 2. 事务回滚 rollback;
-- 3. 断开连接,会释放该连接中所有的锁;
按粒度划分
全局锁
概念 对整个数据库实例加锁,最大粒度的锁。
适用场景 谨慎使用,必须绝对必要才使用,以下情况不一定非要用全局锁,除非你需要确保完全无外界写入操作干扰。
- 全库备份
- 全库导出
加锁、释放锁方式
-- 方式1
-- 加锁
flush tables with read lock; -- FYWRL 执行完毕后,整个数据库就处于只读状态了。
-- 释放锁
unlock tables;
-- 方式2
-- 加锁
SET GLOBAL READ_ONLY = 1;
-- 释放锁
SET GLOBAL READ_ONLY = 0;
锁定的数据 锁定数据库内所有的表,上的是读锁。
表级锁
包含两种:表锁和元数据锁。
表锁
概念 对整张表上锁,包括读锁和写锁,需要显式加锁和释放。
适用场景
- 并发要求低的场景,表锁相对来说开销小。
- 批量数据写入时,也可以使用表锁保证数据的一致性和完整性。
- 防止死锁
- 高并发读的场景
加锁、释放锁方式
--默认加写锁
lock tables tablenmae;
-- 加写锁
lock tables tablename write;
-- 加读锁
lock tables tablename read;
-- 释放锁
unlock tables;
锁定的数据 锁定表内的所有行。
MDL元数据锁
Metadata Lock,简称MDL,为了防止对一个正在使用的数据库对象(例如,表和触发器)进行结构性更改。 上MDL读锁时,其他线程能够进行增删改查。 上MDL写锁(DDL操作时触发)时,对读锁和写锁都互斥,用来保证变更表结构操作的安全性。 但是,DDL语句未必一定阻塞DML语句,这里不展开说明。
加锁、释放锁方式 并非显式加锁和释放锁,而是在访问表时自动加上。
锁定的数据 一个数据库对象:一张表、视图、存储过程或其他数据库对象。
意向锁
概念 Innodb引擎支持多粒度锁定,允许行锁和表锁共存,为了快速判断表中是否存在行锁,InnoDB推出了意向锁。意向锁本身是一种表锁。 通常有两种类型:
- 意向排他锁(IX):接下来要在表中某些行上加排它锁
- 意向共享锁(IS):接下来要在表中某些行上加共享锁
适用场景 实际是好处:
- 减少锁冲突:在事务开始时声明其意向,帮助系统判断哪些事务可能产生锁冲突。
- 优化锁定检查:在判断是否可以安全为一个事务分配锁时,可以快速检查表级意向锁,而无需扫描所有行锁。
为什么意向锁可以有以上好处?
加锁、释放锁方式
Innodb自动加上。 加锁需要满足:
- 获取表中行的共享锁之前,必须先获得表上的IS锁,或是更强的锁。
- 获取表中行的排他锁之前,必须先获取表中的IX锁。
AUTO-INC锁
概念 特殊的表级锁,在表中有自增列时,如果向该表插入数据,Innodb会首先获取这张表的AUTO-INC锁,等待插入完成后释放。用于保护自增列的生成值,保证唯一性和防止冲突。 可以使用Innodb_autoinc_lock_mode变量来配置自增锁的算法。
value |
含义 |
0 |
传统锁模式,使用AUTO-INC锁 |
1 |
连续锁模式,采用轻量级锁 |
2 |
交错锁模式(MySQL8默认),AUTO-INC和轻量级锁之间灵活切换 |
如果在insert执行期间都持有锁,会降低并发度,这是传统锁模式。 可以在生成id后,但是insert未执行完成前,立刻释放锁,使得其他事务可以去生成新的自增id,这是轻量级锁。但是如果批量插入时,可能会导致批量插入的数据id并不自增。 在单行insert语句使用轻量级锁,在批量插入语句使用AUTO-INC锁,这是交错锁模式。
行锁
MySQL行锁在引擎层实现,不是所有引擎都支持行锁,Innodb指出,MyISAM不支持。 行锁主要有四类:
- Record lock
- Gap lock
- Next-key lock
- 插入意向锁
Record lock
概念 记录锁,针对索引记录的锁,锁定索引记录。
锁定范围 这要分情况讨论: 以以下语句为例:
SELECT * FROM user WHERE username='qxy' FOR UPDATE;
- 如果有索引:
- 且该索引唯一,那么直接在该索引上Record lock。
- 如果索引不唯一,可能会增加间隙锁,确保事务处理期间,不会插入新的username='qxy'
- 如果没有索引:
- 升级锁的粒度,如表锁
Gap Lock
概念 间隙锁,锁定两个索引记录之间的间隙上,但是不包含该范围内的实际记录。
适用场景 用于防止其他事务插入记录到被锁定的范围内。
加锁、释放锁方式 Innodb隐式添加。
锁定范围 分情况讨论: 对于以下语句:
SELECT * FROM employees WHERE salary BETWEEN 50000 AND 100000 FOR UPDATE;
- 有索引
- 锁定索引记录(Record Lock),锁定索引之间的间隙
- 无索引
- 锁定全表扫描过程中的每一个间隙,锁定符合条件的记录。(可能会升级为表锁)
对于特殊的场景
-- id主键自增,且最大为40
SELECT * FROM employees WHERE id > 50 FOR UPDATE;
不会上Record Lock,会对>50的范围上锁,阻止其他事务在这个范围内插入。
Next-key Lock
概念 临键锁,Record Lock + Gap Lock,用于锁定一个范围和记录本身,左开右闭。
锁定范围 以以下为例:
-- id有索引
SELECT * FROM table_name WHERE id = 5 FOR UPDATE;
会锁定id=5记录本身,和id<5的间隙,如果id<5的最大记录是4,则会锁定(4,5),最终的效果是锁定(4,5]
乐观锁&悲观锁
概念上的抽象,与java中思想一致。 乐观锁:认为不会出现竞争,不上锁,但是通过CAS+版本号等对数据进行确认。 悲观锁:认为一定会发生竞争,对数据修改前,上排它锁。
死锁的发生和处理
死锁发生的场景 典型的是:AB商品上单修改库存的场景,且两个商品有优惠折扣绑定,需要在同时发生或者均失败,如果userA的顺序是先扣除A的库存,后扣除B的库存,userB则相反。在并发执行下可能导致死锁。 如果是在外贸检索平台的场景下,可能有多个程序需要去对商家的信息进行批量更新,考虑两个服务:1. 批量将商家升级为高级用户 2. 批量降低一些降低一些商家的活跃度指数。 将场景简化为就两个商家。
-- 服务1
start transaction;
update suppliers set grade = "Premium" where sid = 1;-- 执行顺序 1
update suppliers set grade = "Premium" where sid = 2;-- 执行顺序 3
commit;
-- 服务2
start transaction;
update suppliers set active_level = 2 where sid = 2;-- 执行顺序 2
update suppliers set active_level = 2 where sid = 1;-- 执行顺序 4
commit;
如果交替请求资源,发生持有并请求,则会发生死锁。
解决方法 以下是一些依赖数据库管理系统自动检测和解决死锁的方法:
- 设置超时时间innodb_lock_wait_timeout,默认是50s,当超时后,陷入死锁的事务回滚。
SET SESSION innodb_lock_wait_timeout = 10;
- 发起死锁检测,主动回滚死锁链表中的一个事务,让其它事务可以继续执行,需要将innodb_deadlock_detect设置为on。