什么是锁
锁机制用于管理对共享资源的并发访问。
lock与latch
latch一般称为闩锁(轻量级的锁)因为其要求锁定的时间必须非常短。若持续的时间长则应用的性能会非常差。在InnoDB存储引擎中latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性并且通常没有死锁检测的机制。
lock的对象是事务用来锁定的是数据库中的对象如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放。不同事务隔离级别释放的时间可能不同所以分析lock问题要先看事务隔离级别(select tx_isolation;)。
MyISAM存储引擎中的锁
MyISAM存储引擎只支持表锁。
MyISAM存储引擎的读锁和写锁是互斥的读写操作是串行的。
但它认为写锁的优先级比读锁高所以即使读请求先到锁等待队列写请求后到写锁也会插到读锁请求之前
这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因因为大量的更新操作会造成查询操作很难获得读锁从而可能永远阻塞。
可以通过一些设置来调节MyISAM的调度行为。
InnoDB存储引擎中的锁
锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁
共享锁(S Lock)允许事务读一行数据。
排他锁(X Lock)允许事务删除或更新一行数据。
X锁与任何的锁都不兼容而S锁仅和S锁兼容S和X锁都是行锁兼容是指对同一记录(row)锁的兼容性情况。
意向锁设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。
如果需要对页上的记录r进行上X锁那么分别需要对数据库A、表、页上意向锁IX最后对记录r上X锁。若其中任何一个部分导致等待那么该操作需要等待粗粒度锁的完成。
两种意向锁
意向共享锁(IS Lock)事务想要获得一张表中某几行的共享锁
意向排他锁(IX Lock)事务想要获得一张表中某几行的排他锁
意向锁其实不会阻塞除全表扫以外的任何请求。
锁的兼容性情况
type
IS
IX
S
X
IS
兼容
兼容
兼容
不兼容
IX
兼容
兼容
不兼容
不兼容
S
兼容
不兼容
兼容
不兼容
X
不兼容
不兼容
不兼容
不兼容
如果一个事务请求的锁模式与当前的锁兼容InnoDB 就将请求的锁授予该事务反之如果两者不兼容该事务就要等等锁释放。
一致性非锁定读
一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作这时读取操作不会因此去等待行上锁的释放。相反地InnoDB存储引擎会去读取行的一个快照数据。
InnoDB存储引擎的默认设置下这是默认的读取方式即读取不会占用和等待表上的锁。
在不同事务隔离级别下读取的方式不同并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外即使都是使用非锁定的一致性读但是对于快照数据的定义也各不相同。
一致性锁定读
InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作
SELECT…FOR UPDATE
SELECT…LOCK IN SHARE MODE
自增长与锁
AUTO-INC Locking这种锁其实是采用一种特殊的表锁机制为了提高插入的性能锁不是在一个事务完成后才释放而是在完成对自增长值插入的SQL语句后立即释放。
外键和锁
在InnoDB存储引擎中对于一个外键列如果没有显式地对这个列加索引InnoDB存储引擎自动对其加一个索引因为这样可以避免表锁。
锁的算法
InnoDB存储引擎有3种行锁的算法
Record Lock单个行记录上的锁
Gap Lock间隙锁锁定一个范围但不包含记录本身是一个左开右闭的空间。如索引值有1358GAP的区间(-∞,1](1,3](3,5](5,8](8,∞)。GAP Lock的目的是为了防止同一事务的两次当前读出现幻读的情况。
Next-Key Lock∶Gap LockRecord Lock锁定一个范围并且锁定记录本身。对于行的查询都是采用该方法主要目的是解决幻读的问题。
解决Phantom Problem(幻像问题)
在默认的事务隔离级别下即REPEATABLE READ下InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem(幻像问题)。
Phantom Problem是指在同一事务下连续执行两次同样的SQL语句可能导致不同的结果第二次的SQL语句可能会返回之前不存在的行。
InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ在该隔离级别下其采用Next-Key Locking的方式来加锁。而在事务隔离级别READ COMMITTED下其仅采用Record Lock。
锁的问题
幻读
在同一个事务中同一个查询多次返回的结果不一致。事务A新增了一条记录事务B在事务A提交前后各执行了一次查询操作发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的这个不能像不可重复读通过记录加锁解决因为对于新增的记录根本无法加锁。需要将事务串行化才能避免幻读。
脏读
脏数据是指事务对缓冲池中行记录的修改并且还没有被提交(commit)。
脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED。
违反了事务的隔离性。
脏读隔离看似毫无用处但在一些比较特殊的情况下还是可以将事务的隔离级别设置为READ UNCOMMITTED。例如replication环境中的slave节点并且在该slave上的查询并不需要特别精确的返回值。
不可重复读
在一个事务内两次读到的数据是不一样的情况这种情况称为不可重复读。
不可重复读和脏读的区别是脏读是读到未提交的数据而不可重复读读到的却是已经提交的数据但是其违反了数据库事务一致性的要求。
InnoDB存储引擎的默认事务隔离级别是READ REPEATABLE采用Next-Key Lock算法避免了不可重复读的现象。
丢失更新
一个事务的更新操作会被另一个事务的更新操作所覆盖从而导致数据的不一致。
要避免丢失更新发生需要让事务在这种情况下的操作变成串行化而不是并行的操作。
死锁
死锁是指两个或两个以上的事务在执行过程中因争夺锁资源而造成的一种互相等待的现象。除了超时机制当前数据库还都普遍采用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;
参考资料