1、什么是锁
锁机制用于管理对共享资源的并发访问。
lock与latch
- latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
- lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放。不同事务隔离级别释放的时间可能不同,所以分析lock问题,要先看事务隔离级别(select @@tx_isolation;)。
2、MyISAM存储引擎中的锁
MyISAM存储引擎只支持表锁。
MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。
但它认为写锁的优先级比读锁高,所以即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!
这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。
可以通过一些设置来调节MyISAM的调度行为。
3、InnoDB存储引擎中的锁
3.1 锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁:
- 共享锁(S Lock),允许事务读一行数据。
- 排他锁(X Lock),允许事务删除或更新一行数据。
X锁与任何的锁都不兼容,而S锁仅和S锁兼容,S和X锁都是行锁,兼容是指对同一记录(row)锁的兼容性情况。
意向锁设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。
如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。
两种意向锁:
- 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
- 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁
意向锁其实不会阻塞除全表扫以外的任何请求。
锁的兼容性情况
如果一个事务请求的锁模式与当前的锁兼容,InnoDB 就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等等锁释放。
3.2 一致性非锁定读
一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。
InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。
在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
3.3 一致性锁定读
InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作:
- SELECT…FOR UPDATE
- SELECT…LOCK IN SHARE MODE
3.4 自增长与锁
AUTO-INC Locking,这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。
3.5 外键和锁
在InnoDB存储引擎中,对于一个外键列,如果没有显式地对这个列加索引,InnoDB存储引擎自动对其加一个索引,因为这样可以避免表锁。
4、锁的算法
4.1 InnoDB存储引擎有3种行锁的算法
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身,是一个左开右闭的空间。如索引值有1,3,5,8,GAP的区间:(-∞,1],(1,3],(3,5],(5,8],(8,+∞)。GAP Lock的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
- Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。
4.2 解决Phantom Problem(幻像问题)
在默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem(幻像问题)。
Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。
InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ,在该隔离级别下,其采用Next-Key Locking的方式来加锁。而在事务隔离级别READ COMMITTED下,其仅采用Record Lock。
5、锁的问题
5.1 幻读
在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的,这个不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。需要将事务串行化,才能避免幻读。
5.2 脏读
脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。
脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED。
违反了事务的隔离性。
脏读隔离看似毫无用处,但在一些比较特殊的情况下还是可以将事务的隔离级别设置为READ UNCOMMITTED。例如replication环境中的slave节点,并且在该slave上的查询并不需要特别精确的返回值。
5.3 不可重复读
在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。
不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。
InnoDB存储引擎的默认事务隔离级别是READ REPEATABLE,采用Next-Key Lock算法,避免了不可重复读的现象。
5.4 丢失更新
一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。
要避免丢失更新发生,需要让事务在这种情况下的操作变成串行化,而不是并行的操作。
6、死锁
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。
MyISAM总是一次获得SQL语句所需要的全部锁。这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。
产生死锁的必要条件
- 多个并发事务
- 每个事务都持有锁
- 每个事务都需要再持有锁
- 事务之间产生加锁的循环等待,形成死锁
死锁检测
1.InnoDB的初始化一个事务,当事务尝试申请加一个锁,并且需要等待时(wait_lock),innodb会开始进行死锁检测(deadlock_mark)
2.进入到lock_deadlock_check_and_resolve()函数进行检测死锁和解决死锁。
3.检测死锁过程中,是有计数器来进行限制的,在等待wait-for graph 检测过程中遇到超时或者超过阈值,则停止检测。
4.死锁检测的逻辑之一是等待图的处理过程,如果通过锁的信息和事务等待链构造出一个图,如果图中出现回路,就认为发生了死锁。
5.死锁的回滚,内部代码的处理逻辑之一是比较undo的数量,回滚undo数量少的事务。
死锁日志
update `xxx` set xxx where xxx = xxx
RECORD LOCKS space id 123 page no 13726 n bits 248 index idx_xxx of table `xxx` trx id 123456 lock_mode X locks rec but not gap
Record lock, heap no 123 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
update `xxx` set xxx where xxx = xxx
RECORD LOCKS space id 123 page no 123456 n bits 128 index PRIMARY of table `xxx` trx id 123456 lock_mode X locks rec but not gap
Record lock, heap no 456 PHYSICAL RECORD: n_fields 10; compact format; info bits 0
上面的日志,精简了很多日志,只保留了部分重要信息,从死锁日志中,可以看出,执行哪条SQL,哪个页,哪个索引,锁的模式、锁的属性。
锁的属性
- LOCK_REC_NOT_GAP (锁记录)
- LOCK_GAP (锁记录前的GAP)
- LOCK_ORDINARY (同时锁记录+记录前的GAP 。传说中的Next Key锁)
- LOCK_INSERT_INTENTION(插入意向锁,其实是特殊的GAP锁)
死锁案例分析,最简单的、最经典的死锁案例,加锁顺序不一致导致死锁。
session1:
begin;
select * from user where id = 3 for update;
select * from user where id = 5 for update;
commit;
session2:
begin;
select * from user where id = 5 for update;
select * from user where id = 3 for update;
commit;
参考资料
- MySQL技术内幕——InnoDB存储引擎(姜承尧)
- 深入浅出MySQL——数据库开发、优化与管理维护(唐汉明 / 翟振兴 / 关宝军 / 王洪权)
- https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-transaction-model.html
- MySQL锁机制
- 杨奇龙的漫谈死锁