前言
数据库作为一个数据存取的服务器,往往需要对接多个客户端,在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改一部分数据时,MySQL会通过锁定 防止其他用户读取同一数据。
在处理并发读或者写时,通过实现一个由两种类型的锁组成的锁系统来解决问题。两种锁通常被称为共享锁(shared lock)和排他锁(exclusive lock),也叫读锁(read lock)和写锁(write lock)。
读锁是共享的,是互相不阻塞的。多个客户端在同一时刻可以同时读取同一个资源,而不互相干扰。写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,这是出于安全策略的考虑,只有这样才能 确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。
MySQL锁分类
按锁粒度分:
- 全局锁:锁整Database,由MySQL的SQL layer层实现。
- 表锁:锁某Table,由MySQL的SQL layer层实现
- 行锁:锁某Row的索引,也可锁定行索引之间的间隙,由存储引擎实现【InnoDB】
- 记录锁
- 间隙锁
- 临键锁
- 插入意向锁
按锁功能分:
- 共享锁Shared Locks(S锁,也叫读锁):
- 加了读锁的记录,允许其他事务再加读锁
- 加锁方式:select…lock in share mode
- 排他锁Exclusive Locks(X锁,也叫写锁)
- 加了写锁的记录,不允许其他事务再加读锁或者写锁
- 加锁方式:select…for update
全局锁
全局锁是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句, 已经更新操作的事务提交语句都将被阻塞。其典型的使用场景是做全库的逻辑备份,对所有的表进行锁 定,从而获取一致性视图,保证数据的完整性。
加全局锁的命令为:
flush tables with read lock;
释放锁的命令为:
unlock tables;
或者断开加锁session的连接,自动释放全局锁。
全局锁用于备份这个事情,还是很危险的。因为如果在主库上加全局锁,则整个数据库将不能写 入,备份期间影响业务运行,如果在从库上加全局锁,则会导致不能执行主库同步过来的操作,造成主 从延迟。
对于innodb这种支持事务的引擎,使用mysqldump备份时可以使用--single-transaction参数,利用 mvcc提供一致性视图,而不使用全局锁,不会影响业务的正常运行。而对于有MyISAM这种不支持事务 的表,就只能通过全局锁获得一致性视图,对应的mysqldump参数为--lock-all-tables。
# 提交请求锁定所有数据库中的所有表,以保证数据的一致性,全局读锁【LBCC】
mysqldump -uroot -p --host=localhost --all-databases --lock-all-tables > /root/db.sql
# 一致性视图【MVCC】
mysqldump -uroot -p --host=localhost --all-databases --single-transaction > /root/db.sql
表锁
MySQL的表级锁有四种:
- 表读锁(Table Read Lock)
lock table t read; # 为表t加写锁
unlock tables; # 释放锁
- 表写锁(Table Write Lock)
lock table t write; # 为表t加写锁
unlock tables; # 释放锁
- 元数据锁(meta data lock,MDL) , 当对表结构锁调整时,如alter操作会加元数据表锁。
- 自增锁(AUTO-INC Locks),AUTO-INC锁是一种特殊的表级锁,发生涉及AUTO_INCREMENT列的事务性插入操作时产生。
行锁
MySQL的行级锁,是由存储引擎来实现的,这里我们默认说的是InnoDB的行级锁。InnoDB行锁是通过给索引上的索引项加锁来实现的,因此InnoDB这种行锁实现特点:只有通过索引条件检索的数据, InnoDB才使用行级锁,否则,InnoDB将使用表锁!
InnoDB的行级锁,按照锁定范围来说,分为四种:
- 记录锁(Record Locks):锁定索引中一条记录。
- 间隙锁(Gap Locks):要么锁住索引记录中间的值,要么锁住第一个索引记录前面的值或者 最后一个索引记录后面的值。
- 临键锁(Next-Key Locks):是索引记录上的记录锁和在索引记录之前的间隙锁的组合(间隙锁 + 记录锁)。
- 插入意向锁(Insert Intention Locks):做insert操作时添加的对记录id的锁。
InnoDB的行级锁,按照功能来说,分为两种:
- 读锁:允许一个事务去读一行,阻止其他事务更新目标行数据。同时阻止其他事务加写锁,但不阻止其他事务加读锁。
- 写锁:允许获得排他锁的事务更新数据,阻止其他事务获取或修改数据。同时阻止其他事务加读锁和写锁。
如何加行级锁?
- 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加写锁;
- 对于普通SELECT语句,InnoDB不会加任何锁。
- 事务可以通过以下语句手动给记录集加共享锁或排他锁:
- 添加读锁:select * from t1_simple where id = 4 lock in share mode;
- 添加写锁:select * from t1_simple where id = 4 for update;
主键id索引的行锁区间划分图:
记录锁
记录锁(Record Locks)仅仅锁住索引记录的一行,在单条索引记录上加锁。记录锁锁住的永远是索引,而非记录本身,即使该表上没有任何显示索引,那么innodb会在后台创建一个隐藏的聚簇索引索引,那么锁住的就是这个隐藏的聚簇索引索引。
-- 加记录读锁
select * from t1_simple where id = 1 lock in share mode;
-- 加记录写锁
select * from t1_simple where id = 1 for update;
-- 新增,修改,删除加记录写锁
insert into t1_simple values (1, 22);
update t1_simple set pubtime=33 where id =1;
delete from t1_simple where id =1;
间隙锁
- 间隙锁(Gap Locks),仅仅锁住一个索引区间(开区间,不包括双端端点)。
- 在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录 本身。
- 间隙锁可用于防止幻读,保证索引间隙不会被插入数据。
- 在可重复读(REPEATABLE READ)这个隔离级别下生效。
临键锁
- 临键锁(Next-Key Locks)相当于记录锁 + 间隙锁【左开右闭区间】。
- 默认情况下,innodb使用临键锁来锁定记录,但在不同的场景中会退化。
- 当查询的索引含有唯一属性的时候,临键锁会进行优化,将其降级为记录锁,即仅锁住索引本身,不是范围。
| 场景 | 退化的锁类型 |
|---|---|
| 使用Unique index 精确匹配【=】,且记录存在 | 记录锁 |
| 使用Unique index 精确匹配【=】,且记录不存在 | 间隙锁 |
| 使用Unique index 范围匹配【<和>】 | 临键锁 |
插入意向锁
按照之前学习的关于 间隙锁 的知识分析,此时间隙锁的范围是 (11,99),意思是这个范围的 id 都不可以 插入。如果是这样的话数据插入效率太低,锁范围比较大,很容易发生锁冲突怎么办? 插入意向锁就是用来解决这个问题的!
什么是插入意向锁?
- 插入意向锁(Insert Intention Locks)是一种在INSERT操作之前设置的一种特殊的间隙锁。
- 插入意向锁表示了一种插入意图,即当多个不同的事务,同时往同一个索引的同一个间隙中插入数据的时候,它们互相之间无需等待,即不会阻塞。
- 插入意向锁不会阻止插入意向锁,但是插入意向锁会阻止其他间隙写锁(排他锁)、记录锁。
注意, 插入意向锁和InnoDB表意向锁是不同的锁:
InnoDB也实现了表级锁,也就是意向锁【Intention Locks】。意向锁是MySQL内部使用的,不需要用 户干预。意向锁和行锁可以共存,意向锁的主要作用是为了全表更新数据时的提升性能。否则在全表更 新数据时,需要先检索该范是否某些记录上面有行锁。那么将是一件非常繁琐且耗时操作。举例说明:
事务A修改user表的记录r,会给记录r上一把行级的写锁,同时会给user表上一把意向写锁(IX),这时 事务B要给user表上一个表级的写锁就会被阻塞。意向锁通过这种方式实现了行锁和表锁共存,且满足 事务隔离性的要求。
加锁的规则
主键索引
- 等值条件,命中,加记录锁。
- 等值条件,未命中,加间隙锁。
- 范围条件,命中,包含where条件的临键区间,加临键锁。
- 范围条件,没有命中,加间隙锁。
辅助索引
- 等值条件,命中,命中记录的辅助索引项 + 主键索引项加记录锁,辅助索引项两侧加间隙锁。
- 等值条件,未命中,加间隙锁。
- 范围条件,命中,包含where条件的临键区间加临键锁。命中记录的id索引项加记录锁。
- 范围条件,没有命中,加间隙锁。
死锁
深入理解MySQL如何加锁,有两个比较重要的作用:
- 可以根据MySQL的加锁规则,写出不会发生死锁的SQL;
- 可以根据MySQL的加锁规则,定位出线上产生死锁的原因;
- 可以根据MySQL的加锁规则,透过现象看本质,理解数据库层面阻塞执行的根本原因。
死锁如何产生的?
举例说明,准备数据:
CREATE TABLE `t1_deadlock` (
`id` int(11) NOT NULL, `name` varchar(100) DEFAULT NULL,
`age` int(11) NOT NULL,
`address` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`) USING BTREE,
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Insert into t1_deadlock(id,name,age,address) values (1,'刘备',18,'蜀国');
Insert into t1_deadlock(id,name,age,address) values (2,'关羽',17,'蜀国');
Insert into t1_deadlock(id,name,age,address) values (3,'张飞',16,'蜀国');
Insert into t1_deadlock(id,name,age,address) values (4,'关羽',16,'蜀国');
Insert into t1_deadlock(id,name,age,address) values (5,'诸葛亮',35,'蜀国');
Insert into t1_deadlock(id,name,age,address) values (6,'曹孟德',32,'魏国');
下面,来看看两个死锁的例子 一个是两个Session的两条SQL产生死锁;另一个是两个Session的一条 SQL,产生死锁:
结论: 死锁的发生与否,并不在于事务中有多少条SQL语句,【死锁的关键在于】:两个(或以上)的Session 【加锁的顺序】不一致。
如何避免死锁?
MySQL默认会主动探知死锁,并回滚某一个影响最小的事务。等另一事务执行完成之后,再重新执行该事务。
- 注意程序的逻辑:根本的原因是程序逻辑的顺序交叠,最常见的是交差更新。
- 保持事务的轻量:越是轻量的事务,占有越少的锁资源,这样发生死锁的几率就越小。
- 提高运行的速度:避免使用子查询,尽量使用主键等等。
- 尽量快提交事务,减少持有锁的时间:越早提交事务,锁就越早释放。