1. 行锁
InnoDB存储引擎实现了如下两种标准的行级锁:
❑共享锁(S Lock),允许事务读一行数据。
❑排他锁(X Lock),允许事务删除或更新一行数据。
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁——这种情况称为锁不兼容。下表显示了共享锁和排他锁的兼容性。
对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。
1.1 共享锁
select * from table_name where .....lock in share mode`
事务1: select * from table_1 where id=1 lock in share mode;
事务2: select * from table_1 where id=1 lock in share mode;
事务1: update table_1 set age=10 where id=1;
事务1: 事务1更新时发现此行锁被其他事务享用,等待
事务2: update table_1 set age=12 where id=1;
事务2: 事务2更新时发现此行锁被其他事务享用,也等待,导致死锁
1.2 排他锁
select * from table_name where .....for update
事务1: select * from table_1 where id=1 for update;
事务2: select * from table_1 where id=1 for update;
事务2: 等待...
事务1: update table_1 set age=10 where id=1;
事务1: 更新完后释放锁
事务2: 获得锁后,得到其他事务提交的记录
1.3 行锁的特性
- innodb 的行锁是在有索引的情况下,没有索引的表是锁定全表的。下面是一个示例,其中id是主键:
id | name |
---|---|
1 | 1 |
2 | 2 |
3 | 3 |
事务1update第一条id=1的数据,事务不提交;事务2接着update第二条id=2的数据的时候等待,原因是id没有加上索引,导致事务1锁的是表锁而不是行锁。
2. 意向锁
InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁,如图所示。
若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。例如图6-3,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,之后事务需要对记录r在表1上加上IX,由于不兼容,所以该事务需要等待表锁操作的完成。
InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:
1)意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
2)意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如表所示。
3.快照读
快照读又称一致性非锁定读。即读取到的数据始终是快照数据,而不是最新数据。
一致性非锁定读是指InnoDB存储引擎通过多版本并发控制技术来读取当前数据库的数据。如果当前读取的行正在执行delete或者update操作,这时读取操作不会等行锁的释放,而是去读取行的快照数据。快照数据是指改行之前版本的数据,该实现是通过undo段来实现的,而undo段用来在事务中保存回滚数据,因此使用快照没有增加额外的开销。 这是InnoDB存储引擎的默认读取方式,即默认的事务隔离级别为REPEATABLE READ下的读取方式。由于读到的不是最新数据,某些情况下可能会有问题。
3.1 注意
不同的事务隔离级别下读取的方式不同,并不是每个事务隔离级别下都是采用非锁定的一致性读。四种隔离级别中,READ COMMITTED和REPEATABLE READ这两种隔离级别使用非锁定的一致性读。
-
不同的事务即使都使用非锁定的一致性读,但是对于快照数据的定义也各不相同
READ COMMITTED级别下非锁定读总是读取锁定行的最新一份快照数据,即事务开始后不止一个快照,有很多版本的数据,取的是最新的数据。
REPEATABLE READ级别下非锁定读总是读取事务开始后第一次执行select操作的数据版本(重点在select操作在哪里调用,取的就是哪个时间的数据)。
3.2 例子
事务A | 事务B |
---|---|
select * from table where id='1'; | |
. | update table set id =3 where id=1; |
select * from table where id='1'; | |
. | commit; |
select * from table where id='1'; |
- 上述例子中事务B update以后,事务A第一次select的时候RC级别和RR级别获取的结果都是id=1的那一条数据;第二次select的时候,由于事务B已经提交,RC级别select的结果就是事务Bcommit后更新的最新数据:id=3,而RR级别读取的是事务A开始时的数据,id=1。
4. 当前读
当前读又称一致性锁定读,即不管什么时候读,读到的数据都是最新的数据,而不是事务开始时的快照数据。
默认配置下事务的隔离级别为REPEATABLE READ,select操作为一致性非锁定读,但某些情况下需要对数据库读取操作进行加锁保证数据的一致性。select 有两种一致的锁定读:
select ... for update
select ... lock in share mode
5. 自增长与锁
InnoDB存储引擎内部对每个含有自增长列的表有一个自增长计数器,当进行insert操作时,首先获取计数器的最大值,加1后进行insert操作。这个操作会加一个特殊的表锁,AUTO-INC LOCK。这个锁并不是在事务提交后才释放,而是在insert语句执行完后释放。
5.1 缺点
虽然是insert后就释放锁,不是事务提交后才释放,但是必须等前一个insert的完成才能进行下一次insert,性能较差。
5.2 改进
InnoDB存储引擎中提供了一种轻量级互斥量的自增长实现机制,大大提高了自增长值插入的性能。
6. 外键与锁
在对外键值进行update和insert操作时首先需要查询父表的记录,即select父表,这个select操作不是使用一致性非锁定读,因为会发生数据不一致的问题,为此需要使用一致性锁定读,这时使用的select ... lock in share mode方式。当父表对应记录加X锁后,子表的操作将会阻塞。
7. 锁的算法
InnoDB存储引擎有三种行锁的算法
Record Lock:单行记录上锁
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
Next-Key Lock:相当于Record Lock+Gap Lock,锁定一个范围,并且锁定记录本身
7.1 示例
如果一个索引有10,11,13,20这四个值,那么该索引可能被Next-Key Lock分为如下几个区间:
(-∞,10],
(10,11],
(11,13],
(13,20],
(20,+∞)
除了Next-Key Lock外,还有previous-key lock技术,可锁定的范围为:
(-∞,10),
[10,11),
[11,13),
[13,20),
[20,+∞)
然而,当索引为唯一索引时,InnoDB存储引擎会对Next-key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。示例如下:
事务A | 事务B |
---|---|
Begin | |
select * from t where a=5 for update; | |
Begin | |
Insert into t select 4; | |
Commit; |
该示例中a字段先设置为唯一索引,当事务a执行for update后,事务b插入4的时候,由于锁定的是5这一行,而不是范围,所以事务B不会阻塞。
问题一:什么时候会用到next-key锁?
- 在出现幻读的时候,为解决该问题,需要锁定一个范围,而不是固定一行,这个时候就会用到next-key锁了。
- mysql四个事务隔离级别下,READ UNCOMMITTED和READ COMMITTED这两个事务隔离级别是不支持next-key锁的;REPEATABLE READ 和 SERIALZABLE这两种事务隔离级别是支持next-key锁的。
问题二:Next-key锁只存在于查询条件为区间的时候吗?
- 答案不是的,如果查询条件指定了某一行,但是查询条件的字段是非唯一索引或不是索引的当前读下,也会使用next-key锁。
8. 锁问题
8.1 脏读
- 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做“脏读”。
- 当事务的隔离级别为READ UNCOMMITTED会产生脏读现象,其他隔离级别则不会。
8.2 不可重复读
- 不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。
- 不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。
- 事务隔离级别为READ UNCOMMITTED和READ COMMITTED会产生不可重复读现象,为解决此问题,可将事务隔离级别设置为REPEATABLE READ,该级别默认实现为Next-key Lock锁技术,避免了不可重复读的现象。当然光设置事务隔离级别为REPEATABLE READ还不行,还得使用
select … for update
或者select … lock in share mode
语句才行。
8.3 幻读
- 幻读指的是一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
- 幻读和不可重复读的区别在于不可重复读注重的是同一条记录前后多次读取出现结果不一致的问题,而幻读注重的是范围内增加了以前没有的新数据而导致总的记录数发生变化的问题。
8.4 丢失更新
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了其他事务所做的更新。
比如:
事务A | 事务B |
---|---|
Begin | Begin |
select * from t where a=5; | |
... | select * from t where a= 5; |
update t set b='1' where a=5; | ... |
update t set b='2' where a=5; |
上述两个事务互相不知道对方也在同时执行,并且都先进行了select操作,后进行了update操作,但是最后事务B的结果覆盖了事务A的结果,即丢失了A更新的结果。为避免这个问题应该在事务隔离级别采用REPEATABLE READ的前提上,select 查询语句都加上for update就可以了。
9. 阻塞
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。
在InnoDB存储引擎中:
- 参数innodb_lock_wait_timeout用来控制等待的时间(默认是50秒)
- innodb_rollback_on_timeout用来设定是否在等待超时时对进行中的事务进行回滚操作(默认是OFF,代表不回滚)。
参数innodb_lock_wait_timeout是动态的,可以在MySQL数据库运行时进行调整,而innodb_rollback_on_timeout是静态的,不可在启动时进行修改。