我们知道,当并发事务访问同一资源的时候由于并发的次序访问不可控可能会导致数据不一致,对此我们常常采用加锁的方式来解决并发同步问题,保证数据一致性,在MySQL中也是如此。
MySQL 中锁的类型
MySQL 中锁的类型有很多且锁的类型与具体引擎实现以及事务隔离级别有关,从大体上来看,MySQL 中的锁可以分为两大类:
- 乐观锁
- 悲观锁
下图是 MySQL 中锁的一个概览。
乐观锁
MySQL 中的乐观锁主要是应用程序级别自行实现的。主要思路为通过为每一行数据增加版本号的方式来处理。比如我们常常会设计这样的一张订单表:
CREATE TABLE `t_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`no` varchar(36) NOT NULL DEFAULT '',
`status` tinyint(2) NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL,
...
`version` int(11) NOT NULL DEFAULT '1'
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
订单表往往有状态,同时可能有多个线程在修改状态。此时如果不加以控制,状态可能会产生状态紊乱,因此我们常常会采取版本号的方式加以控制。在修改数据的时候同时需要比较上次数据的版本号,版本号不一致则不能够进行修改。
update t_order set status = 1,version=version+1 where id = 1 and version=#{lastVersion}
悲观锁
我们常常说到的 MySQL 中的锁如表锁、行锁、页锁等等都是悲观锁。不同的引擎使用到的锁以及加锁机制不同,对于高并发下的数据库访问表现形式不同,因此我们对于其中采取的加锁原理需要有全面的了解。
在MySQL 中,按照粒度来划分共存在三种粒度的锁:表锁、行锁、页锁。其中BDB 引擎采用的是页锁, InnoDB 采用的是行锁,除此之外其余引擎采用的都是表锁。
MyISAM 中的锁
MySQL 中绝大部分引擎如MyISAM,Memory等采用的都是表锁的机制。由于 MyISAM 在使用上比较广泛一些,因此在此采用 MyISAM 来看看表锁。
表锁顾名思义是表级别上的锁,对于同一个表中的数据进行操作的时候,如果当前有一条数据在更新,那么其余更新请求不管是不是对于同一条数据更新的都需要等待锁释放后才能进行下一步操作。
从上面我们可以看出,表锁的级别还是比较重的,同一时刻只能有一个线程对同一表进行处理,其余线程需要干等着。为了提高新能 MySQL 中的表锁又分了 读锁 和 写锁 两种类型。
- 读锁,表级共享读锁,不会阻塞其他线程对于同一表的读请求,但会阻塞其他线程对同一表的写请求。
- 写锁,表级独占写锁,会阻塞其他线程对于同一表的读写请求。
表锁兼容性如下:
读锁 | 写锁 | |
---|---|---|
读锁 | 兼容 | 冲突 |
写锁 | 冲突 | 冲突 |
加锁方式
MyISAM,默认 SELECT 是会对涉及到的所有表加读锁,更新操作则会自动加写锁。也可以手动为查询或修改加读锁或写锁,加锁语句如下:
#加读锁
lock tables table_name read;
# 加写锁
lock tables table_name write;
#取消加锁
unlock tables;
BDB 中的锁
BDB 引擎中采用的页锁的机制,页锁指的是对于连续相邻的一组数据进行加锁因此锁的粒度介于表锁和行锁之间。
InnoDB 中的锁
InnoDB 是MySQL 中唯一实现了事务的引擎,出于提升数据库的并发度、提高数据库性能考虑,InnoDB 使用了行锁,即锁住某一条数据。InnoDB 中为了实现高并发以及数据隔离级别,引入了多种类型的锁,共有如下几种类型:记录锁、意向锁、区间锁、临键锁、插入意向锁、自增锁、空间索引谓词锁。
记录锁(行级锁)
共享锁(S)与排他锁(X)是InnoDB 中标准行锁的实现机制。对于InnoDB 数据结构基于B+ Tree的聚集索引数据结构,因此共享锁与排他锁是加载索引上的。
- 共享锁(S),允许事务读取一行数据。共享锁不会阻塞其他事务对于同一行的读请求,但会阻塞其他事务对同一行的写请求。
- 排他锁(X),允许事务更新或删除一行数据,如果某一行数据被其他事务获取到了共享锁或排他锁,则需要等其他事务将该行锁全部释放后才能进行下一步更新操作。
意向锁(表级锁)
InnoDB 允许行锁与表级锁的共存。如果一个线程需要判断能否与获取某个表的锁,由于行锁的存在,除了要判断当前表是否有其他线程获取表锁外还需要对该表的所有数据进行一次遍历看是否能够获取锁。这样做的效率其实是很低的,为了弥补行锁与表锁之间的差异,InnoDB 引入了意向锁来处理,意向锁表明该事务后续想要申请某一行数据的共享锁或排他锁的意图。即:先需要向表申请意向锁,再申请某一行的行锁。
意向锁分为意向共享锁(IS)和意向排他锁(IX)。
- 意向共享锁(IS),表明一个事务意图获取一行数据的共享锁。
- 意向排他锁(IX),表明一个事务意图获取一行数据的排他锁。
意向锁与行锁的兼容性:
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果某一事务申请的锁与现有锁兼容,则可以获取到该锁,否则需要等到锁被释放才能被授予。手动加锁方式:
#加意向共享锁
select ... lock in share mode;
#加意向排他锁
select ... for update;
区间锁(行级锁)
区间锁指的是锁住两个索引之间区间的锁,作用在索引上不允许其他线程操作锁住区域。考虑如下情形,假设我们有一表test,表中有以下数据:
id | name |
---|---|
1 | A |
3 | C |
7 | G |
并发事务时序如下:
时序 | 事务1 | 事务2 |
---|---|---|
1 | start transaction; | |
2 | start transaction; | |
3 | insert into test(id,name) values(2,'B'); | |
4 | commit; | |
5 | delete from test where id < 3; | |
6 | commit; |
如若没有相关的机制,删除操作将把新插入的id=2的数据一并删除掉。区间锁正是为了解决这一问题而引入。我们知道 Read Committed
事务隔离级别下存在不可重复读的现象,在Read Repeatable
级别下,引入了间隙锁来解决这一问题。由于间隙锁锁住区域其余事务不能够进行操作,因此 InnoDB 的Read Repeatable
其实是已经解决了幻读的现象。因此间隙锁是需要在Read Repeatable
级别才有,加锁语句如下:
select id,name from test where id between 1 and 3 for update;
临键锁(行级锁)
临键锁是 Gap锁 + 记录锁的一种组合形式,作用于索引,加锁时会锁住索引及索引之间的间隙,是InnoDB 中的 Read Repeatable
默认加锁方式。以上面的test表为例,如若加 next-key 锁,可能产生的区间为:
(负无穷,1]
(1,3]
(3,7]
(7,正无穷)
插入意向锁(行级锁)
InnoDB 在执行行插入操作之前会对相应的索引区间加间隙锁。该锁表示了事务在该处的插入意图。如果多个事务没有在同一位置进行插入,那么它们是可以并发执行的毋需彼此等待。
以上述test
表为例,
假设现在有两个事务,需要插入两条数据(4,D),(5,E):
时序 | 事务1 | 事务2 |
---|---|---|
1 | start transaction; | |
2 | start transaction; | |
3 | insert into test(id,name) values(4,'D'); | |
4 | insert into test(id,name) values(5,'E'); | |
5 | commit; | |
6 | commit; |
如若按照Next-Key
的方式进行加锁,由于事务1需要在(3,7
]的区间进行插入,则需要对该区域进行加锁,因此事务1进行的时候事务2的插入需要等待事务1完成释放锁之后才可以进行插入,而使用插入意向锁由于两者虽都是在(3,7)这个区间内进行插入,但是插入的位置并不冲突,所以不会产生锁阻塞,这进一步提高了并发的性能。
自增锁(表级锁)
自增锁是一种特殊的表级锁,用以同一事务保持自增主键的连续性。自增锁一共有三种模式,由innodb_autoinc_lock_mode
控制:
- innodb_autoinc_lock_mode = 0 传统模式,所有的插入语句在开始的时候都需要先获取自增锁,语句结束之后才释放自增自增锁,最安全但并发性最差。
- innodb_autoinc_lock_mode = 1 连续模式,InnoDB 中默认的方式,该模式对于可预测插入行数的插入进行了优化,一次可以批量生成连续的值。
- innodb_autoinc_lock_mode = 2 交错模式,在这种锁定模式下,没有使用表级的自增锁,因此它的速度是最快的。但是该模式下并不能保证生成的值是连续,因此在主从复制或数据恢复的时候,主键可能与之前产生的不一致。
空间索引谓词锁
在多维空间数据中,没有绝对排序的概念,因此之前引入的间隙锁机制不能有效的处理空间数据的数据隔离。为此 InnoDB 中引入了空间索引谓词锁的机制,空间索引采用的是R-Tree 数据结构实现,空间索引包含了最小矩形边界的数据(MBR),因此 InnoDB 可以通过在 MBR 上加谓词锁来保证一致性读。