一、常用的两种存储引擎
1. MyISAM与InnoDB
- 是否支持行级锁
MyISAM只有表级锁,而InnoDB支持行级锁和表级锁,默认为行级锁。
- 是否支持事务
MyISAM不提供事务支持,InnoDB提供事务支持,具有提交(commit)和回滚(rollback)的事务能力。
- 是否支持外键
MyISAM不支持外键,InnoDB支持外键
- 是否支持数据库异常崩溃后的安全恢复
MyISAM不支持,InnoDB支持。
使用InnoDB的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于redo log。
InnoDB 使用redo log(重做日志)保证事务的持久性,使用undo log(回滚日志)来保证事务的原子性。
通过 锁机制,MVCC等手段来保证事务的隔离性(默认的隔离级别是REPEATABLE-READ)
保证了事务的持久性,原子性,隔离性之后,一致性才能得到保障
2. 锁机制与InnoDB锁算法
MyISAM和InnoDB存储引擎使用的锁:
- MyISAM采用表级锁。
- InnoDB支持行级锁和表级锁默认为行级锁。
表级锁和行级锁对比:
- 表级锁:MySQL中锁定粒度最大的一种锁,对当前操作的整张表加锁,实现简单,资源消耗少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低。
- 行级锁:MySQL中锁定粒度最小的一种锁,对当前操作的行加锁,行级锁能大大减少数据库操作的冲突,其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
InnoDB支持三种行锁定:
- 行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。
- 间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而设计的。
- 后码锁(Next-Key Lock):行锁和间隙锁组合起来就叫Next-Key Lock。
默认情况下,InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。
2.1 行锁(Record Lock)
- 当需要对表中的某条数据进行写操作(insert,update,delete,select for update)时,需要先获取记录的排他锁(X锁),这个就称为行锁。
1 | create table x(`id` int, `num` int, index `idx_id` (`id`)); |
- 针对InnoDB RR隔离级别,行锁的特点:“锁定特定行不允许进行修改”,但行锁是基于表索引的,如果where条件中用的是num字段(非索引列)将产生不一样的现象:
1 | -- 事务A |
2.2 Gap锁(Gap Lock)
在MySQL中select称为快照读,不需要锁,而insert、update、delete、select for update则称为当前读,需要给数据加锁,幻读中的“读”即是针对当前读。
RR事务隔离级别允许存在幻读,但InnoDB RR级别却通过Gap锁避免了幻读
产生间隙锁的条件(RR事务隔离级别下)
- 使用普通索引锁定
- 使用多列唯一索引
- 使用唯一索引锁定多行记录
唯一索引的间隙锁
数据表
1 | CREATE TABLE `test` ( |
数据
1 | INSERT INTO `test` VALUES ('1', '小罗'); |
只使用记录锁,不会产生间隙锁
1 | /* 开启事务1 */ |
以上,由于主键是唯一索引,而且是只使用一个索引查询,并且只锁定一条记录,所以,只会对id=5的数据加上记录锁,而不会产生间隙锁。
产生间隙锁
1 | /* 开启事务1 */ |
锁住不存在的数据
1 | /* 开启事务1 */ |
我们可以看出,指定查询某一条记录时,如果这条记录不存在,会产生间隙锁
结论
- 对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁,如:WHERE
id
= 5 FOR UPDATE; - 对于查找某一范围内的查询语句,会产生间隙锁,如:WHERE
id
BETWEEN 5 AND 7 FOR UPDATE;
普通索引的间隙锁
数据准备
创建test1表:
- number 不是唯一值
1 | CREATE TABLE `test1` ( |
id 是主键,number上建立了一个普通索引。先加一些数据:
1 | INSERT INTO `test1` VALUES (1, 1); |
test1表中 number 索引存在的隐藏间隙:
(-infinity, 1]
(1, 3]
(3, 8]
(8, 12]
(12, +infinity]
执行以下的事务(事务1最后提交)
1 | /* 开启事务1 */ |
这里可以看到,number (1 - 8) 的间隙中,插入语句都被阻塞了,而不在这个范围内的语句,正常执行,这就是因为有间隙锁的原因。
1 | /* 开启事务1 */ |
这里有一个奇怪的现象:
事务3添加 id = 6,number = 8 的数据,给阻塞了;
事务4添加 id = 8,number = 8 的数据,正常执行了。
事务7将 id = 11,number = 12 的数据修改为 id = 11, number = 5的操作,给阻塞了;
当 number 相同时,会根据主键 id 来排序,所以:
事务3添加的 id = 6,number = 8,这条数据是在 (3, 8) 的区间里边,所以会被阻塞;
事务4添加的 id = 8,number = 8,这条数据则是在(8, 12)区间里边,所以不会被阻塞;
事务7的修改语句相当于在 (3, 8) 的区间里边插入一条数据,所以也被阻塞了。
结论
- 在普通索引列上,不管是何种查询,只要加锁,都会产生间隙锁,这跟唯一索引不一样
- 在普通索引跟唯一索引中,数据间隙的分析,数据行是优先根据普通索引排序,再根据唯一索引排序
2.3 后码锁(Next-key Lock)
后码锁是记录锁与间隙锁的组合,也是为了避免幻读。如果把事务的隔离级别降级为RC,Next-key Lock 则也会失效。
总结
- 记录锁、间隙锁、后码锁,都属于排它锁;
- 记录锁就是锁住一行记录;
- 间隙锁只有在事务级别RR中才会产生;
- 唯一索引只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁,指定给某条存在的记录加锁的时候,只会加记录锁,不会产生间隙锁;
- 普通索引不管是锁住单条,还是多条记录,都会产生间隙锁;
- 间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其他事务在这个区域插入、修改、删除数据,这是为了防止出现幻读现象;
- 普通索引的间隙,优先以普通索引排序,然后再根据主键索引排序;
- 事务级别RC(读已提交)级别的话。间隙锁将会失效。