锁机制
共享排他锁
InnoDB实现了两种标准行级锁
- 共享锁(S)
持有S锁的事务可以读取行 - 排他锁(X)
持有X锁的事务可以修改或者删除行
共享锁与排他锁的兼容性:
S | X | |
---|---|---|
S | 兼容 | 互斥 |
X | 互斥 | 互斥 |
即:
- 多个事务可以同时获取S锁,读读可以并行
- 只有一个事务能够获取X锁,写写不能并行
- 一条记录要么是S锁要么是X锁,读写不能并行
意向锁
意向锁是表级锁,它表明一个事务需要在这个表的行上添加的锁类型。
意向锁有两种:
- 意向共享锁(IS)
表明一个事务有意向表中的行添加S锁,加锁方式SELECT …… LOCK IN SHARE MODE - 意向排他锁(IX)
表明一个事务有意向表中的行添加X锁,加锁方式 SELECT …… FOR UPDATE
意向锁的协议:
- 一个事务获取S锁之前,必须获取IS锁
- 一个事务获取X锁之前,必须获取IX锁
意向锁与共享排他锁的兼容性:
S | IS | X | IX | |
---|---|---|---|---|
S | 兼容 | 兼容 | 互斥 | 互斥 |
IS | 兼容 | 兼容 | 互斥 | 兼容 |
X | 互斥 | 互斥 | 互斥 | 互斥 |
IX | 互斥 | 兼容 | 互斥 | 兼容 |
记录锁、间隙锁、临键锁
假设有一张表:
id | name | no |
---|---|---|
1 | dxy1 | 10 |
2 | dxy2 | 20 |
3 | dxy3 | 30 |
PK:id
Key:name
Key:no
- 记录锁
记录锁是对记录行加锁,只对主键或者唯一索引生效,并且必须是精确匹配
例如:
事务A先执行,不提交:
select * from t where id = 1 for update;
事务B后执行:
select * from t where id = 1 for update;
如图,事务B阻塞,因为id=1的记录已经有了记录锁
- 间隙锁
间隙锁是一种开区间锁,用来解决插入的问题
例如:
事务A先执行,不提交:
select * from t where id between 1 and 10 for update;
事务B后执行:
insert into t values(8,'dxy8',8);
如图:事务B阻塞,因为区间(1,10)已经加锁
- 临键锁
临键锁是由记录锁以及索引记录之前的间隙组成
例如:
事务A先执行,不提交:
select * from t where no = 20 for update;
事务B后执行:
insert into t values(19,'dxy19',19);
如图:事务B被阻塞,因为区间(10,20]已经加锁
需要注意的是
- 如果no是唯一索引,那么事务A支持有id=20的记录锁
- 如果no是普通索引,那么会持有(10,20]的临键锁和(20,30)的间隙锁
插入意向锁
插入意向锁是一种特殊的间隙锁,由插入操作设置
多个事务插入记录,如果在同一区间内插入位置不冲突,那么不会相互阻塞
例如:
id | name | no |
---|---|---|
1 | dxy1 | 10 |
2 | dxy2 | 20 |
3 | dxy3 | 30 |
事务A先执行,不提交:
insert into t values(11,'dxy11',11);
事务B后执行:
insert into t values(12,'dxy12',12);
事务B不会被阻塞
但是如果事务B执行:
insert into t values(11,'dxy11',11);
如图:事务B依然会被阻塞,因为事务A占有记录锁
自增锁
自增锁是一种特殊的表级锁,是由向包含自增列的表中插入数据的事务获取的。
注意:自增锁的生命周期就是一条语句,而不是事务
例如:
事务A先执行,不提交:
insert into t(name,no) values('dxy11',11),('dxy12',12);
事务B后执行,不提交:
insert into t(name,no) values('dxy13',13);
事务A后执行,不提交:
insert into t(name,no) values('dxy14',14);
如图:事务A两条语句插入的记录,id是不连续的
可以通过innodb_autoinc_lock_mode
设置自增锁模式:
-
innodb_autoinc_lock_mode=0
(传统锁模型) -
innodb_autoinc_lock_mode=1
(连续锁模型) -
innodb_autoinc_lock_mode=2
(交叉锁模型)
更多关于自增锁模式的描述,参考官网文档
事务隔离性
我们知道事务有ACID四大特性,其中I就是代表的隔离性(isotation)
什么是事务的隔离性?
隔离性是指多个事务并发访问同一个表数据时,一个事务不会受到另一个事务对数据操作的影响。
并发事务之间的干扰存在哪些问题?
假设表t有数据:
id | name | no |
---|---|---|
1 | dxy1 | 10 |
- 场景1
事务A先执行,不提交:
insert into t values(4,'dxy11',11);
事务B后执行,不提交:
select * from t;
id | name | no |
---|---|---|
1 | dxy1 | 10 |
4 | dxy11 | 11 |
最后事务A回滚
此时,事务B就读取并处理了一条不存在的数据,这种情况就是脏读
- 场景2
事务A先执行,不提交:
select * from t where id >= 1;
id | name | no |
---|---|---|
1 | dxy1 | 10 |
事务B后执行,并提交:
update t set name = 'wuming' where id = 1;
事务A再次执行:
select * from t where id >= 1;
id | name | no |
---|---|---|
1 | wuming | 10 |
此时,事务A先后两次查询的记录数不一致,这种情况就是不可重复读
- 场景3
事务A先执行,不提交:
select * from t where id = 1;
id | name | no |
---|---|---|
1 | dxy1 | 10 |
事务B后执行,并提交:
insert into t values(4,'dxy11',11);
事务A再次执行:
select * from t where id = 1;
id | name | no |
---|---|---|
1 | dxy1 | 10 |
4 | dxy11 | 11 |
此时,事务A先后两次查询的记录数不一致,这种情况就是幻读
因此,并发事务可能导致
- 脏读:一个事务读取了另一个事务未提交的数据
- 不可重复读:一个事务读取了另一个事务修改并提交的数据
- 幻读:一个事务读取了另一个事务增加或删除并提交的数据
为了平衡并发事务的性能和数据一致性、可靠性、再现性,InnoDB支持了SQL:1992表准中全部四种隔离级别
读未提交(Read Uncommitted)
Select语句不加锁,事务可能读取其他事务未提交的数据,即脏读。因此,这种级别的并发性最好,一致性最差-
读已提交(Read Committed)
- 对于非加锁读,每次都读取最新的快照
- 对于加锁读、更新、删除,只对记录加锁而不对间隙加锁,即这种级别不支持间隙锁,因此会导致幻读
因此,RC的隔离级别会出现不可重复读和幻读
-
可重复读(Repeatable Read)(默认级别)
- 对于非加锁读,同一个事务只在第一次查询时生成快照。
- 对于加锁读、更新、删除存在两种情况
- 唯一索引精确匹配:只对数据加记录锁
- 范围查询或非唯一索引:使用间隙锁和临键锁
因此,RR级别下可以完全避免幻影行和不可重复读
序列化(Serializable)
最严格的隔离级别,所有select全部转换成 select …… lock in share mode。这就导致,如果其他事务修改记录,那么select会被阻塞。
全表扫描在RC和RR级别下的加锁策略是怎样的呢?
假设有一张表t:
id | name | no |
---|---|---|
1 | dxy1 | 10 |
2 | dxy2 | 20 |
- RR级别
事务A先执行,不提交:
select * from t where no = 10 for update;
事务B后执行:
select * from t where no = 20 for update
如图:事务B被阻塞,因为RR级别下会对所有扫描的数据加锁
- RC级别
事务A先执行,不提交:
select * from t where no = 10 for update;
事务B后执行:
select * from t where no = 20 for update
如图:事务B不会被阻塞,因为只会对no=10的行加锁