第6章 锁
什么是锁
Lock与Latch
latch一般称为轻量级锁,因为其要求锁定的时间必须非常短。用来宝成并发线程操作临界资源的正确性,并且通常没有死锁检测机制,在InnoDB中又可分为:
- mutex(互斥量)
- rwlock(读写锁)
lock的对象是事务,用来锁定数据库中的对象:表、页、行。有死锁机制。下面主要讲的是lock
InnoDB存储引擎中的锁
锁的类型
两种标准的 行 级锁:
- 共享锁(S Lock),允许事务读一行数据
- 排他锁(X Lock),允许事务删除或更新一行数据。
锁兼容:多个事务可以获得同一个行的共享锁。其他情况锁都不兼容
意向锁(表级别的锁),设计目的是在一个事务中揭示下一行将被请求的锁类型,支持两种:
- 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
- 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。
介绍InnoDB提供的数据使用方法,可以对当前事务运行与获取锁的情况进行分析。
一致性非锁定读
快照读,读取该行之前版本的数据,通过undo段来完成。
一个行记录可能有不止一个快照数据,一般称这种技术为:行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control, MVCC)。
一致性非锁定读(快照读)在第2和第3个隔离级别下使用,但读取的版本不同:
- 第2隔离级别 READ COMMITTED,读取最新的快照(当前时刻的)
- 第3隔离界别 REPEATABLE READ,总是读取事务开始时的行数据版本(从开始时间算起,最旧的数据)
一致性锁定读
InnoDB存储引擎对SELECT语句支持两种一致性的锁定读(locking read)操作:
- SELECT ... FOR UPDATE (加X锁)
- SELECT ... LOCK IN SHARE MODE (加S锁)
上面两个操作必须在事务当中
自增长与锁
自增长语句有四种类型:
- insert-like(所有插入语句)
- simple inserts(最简单的,插入前可以确定值)
- bulk inserts(插入前不能确定值)
- mixed-mode inserts (2和3的混合)
自增长锁的模式:
- 通过表锁AUTO-INC Locking(过时了)
- 对于simple inserts采用互斥量(mutex),对于bulk inserts采用AUTO-INC Locking。当前默认,并且基于语句的主从复制可以正常工作
- 对于所有insert-like语句都通过互斥量,性能最高。但只能采用基于行的主从复制。
对于上述中主从复制的影响,考虑是因为内存中对互斥量的获取顺序是不能确定的,最终的id值也就没法确定,所以只能基于行来复制。
外键和锁
对于外键值得插入或更新,首先要查询父表中的记录,只能采用一致性锁定读,加S锁,来保证与父表的数据一致性。
锁的算法
行锁的3中算法
InnoDB存储引擎有3中行锁的算法:
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包括记录本身
- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
采用Next-Key Lock的锁定技术称为Next-Key Locking。设计的目的是为了解决Phantom Problem(幻读)。称作Next-Key的原因是锁定的区间类似(],左开右闭。对应的也有Previous-Key Locking(左闭右开)。
当查询的索引含有唯一属性时(仅单列唯一索引的情况,多列的话仍是范围),Next-Key Lock可以优化降级为Record Lock,仅锁住索引本身。
从文中的例子可以看到,Gap Lock的作用阻止多个事务将记录插入到同一个范围内,而这会导致Phantom Problem问题。
解决Phantom Problem
Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能返回之前不存在的行。
InnoDB存储引擎采用Next-Key Locking来避免Phantom Problem,但必须显式的加锁,否则使用快读,不会加Next-Key Lock,例如:
SELECT * FROM t WHERE a>2 <u>FOR UPDATE</u>
用户可以通过InnoDB存储引擎的Next-Key Locking机制在应用层面实现唯一性检测 这里没看明白
锁问题
锁提高了并发,却会带来问题。但因为事务隔离性的要求,锁只会带来三种问题。
脏读
在事务隔离级别为READ UNCOMMITTED时,当前事务读取到其他事务未提交的修改。
不可重复读
在一个事务内读取同一数据集合,在这个事务还没结束时,另外一个事务也访问了该同一个数据集合,并做了DML(Data Manipulatin Language,数据库操纵语言)操作。由于第二个数据的修改,导致第一个事务两次读取到的数据可能是不一样的,这种情况称为不可重复读。
与脏读相比,不可重复读是读取了其他事务已提交的数据
与幻读相比,从书中P270和P274的描述中,指不可重复读和Phantom Problem是相同的问题。
丢失更新
一个事务的更新操作被另一个事务的更新操作所覆盖,从而导致数据的不一致。但目前数据库不存在这个问题。本节主要描述应用系统中的更新丢失。
为了避免应用系统中更新丢失的问题,应该在事务开始时就对SELECT的数据加排他锁,例如:
SELECT * FROM account WHERE user=pUser <u>FOR UPDATE</u>
阻塞
由于不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。
阻塞时间超时,默认配置InnoDB不会进行回滚(大部分异常,死锁除外)。因此用户必须判断是是否需要COMMIT还是ROLLBASCK
死锁
死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种相互等待的现象。
InnoDB采用wait-for graph(等待图)的方式来主动检查死锁,wait-for graph要求数据库保存以下两种信息:
- 锁的信息链表
- 事务等待链表
通过链表构造出一张图,入股图中存在环,就代表存在死锁。当发现死锁InnoDB存储引擎选择回滚undo量最小的事务。
使用深度优先算法检测是否有环,InnoDB1.2版本之前采用递归实现,1.2版开始采用非递归实现。
判断有向图存在环:
方法一:遍历删除入度为0的节点,如果还有剩余节点,则说明有环(用循环O(n^2),用栈O(n+e))
方法二:DFS
判断有向图是否有环
Detect Cycle in a Directed Graph
死锁概率
发生概率和以下三点因素有关:
- 事务的数量,数量越多概率越大
- 每个事务操作的数量,操作的数量越多,发生死锁的概率越大
- 操作数据的集合,越小发生死锁的概率越大。这里操作数据集合应该指的是数据总量
死锁的示例
这里介绍了另一个中死锁,和AB-BA死锁不同,死锁发生时处理方式也不同,这种情况会滚undo log记录大的事务
锁升级
锁升级(Lock Escalation)是指将当前锁的粒度降低:
行锁->页锁->表锁
InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。
第七章 事务
认识事务
概述
ACID
分类
从事务理论的角度来说,可以把事务分为以下几种类型:
- 扁平化事务:普通事务,要么全部提交,要么全部回滚
- 带有保存点的扁平事务:在普通事务的基础上,可以回滚到指定的保存点,然后重新执行事务
- 链事务:将事务按照保存点分成一个一个事务,链成一个链
- 嵌套事务:层次结构框架,由一个顶层事务控制着各个层次的事务。树状结构
- 分布式事务:分布式系统环境下的事务问题。需要分布式锁等来解决。
事务的实现
隔离性:由锁来实现
原子性和持久性:redo log(重做日志)
一致性:undo log
redo:恢复提交事务修改的页操作,通常是物理日志,记录的是页的物理修改操作。基本上都是顺序写
undo:回滚行记录到某个特定版本,是逻辑日志,根据每行记录进行记录。需要进行随机读写
redo
基本概念
redo由两部分组成:
- 内存中的重做日志缓冲(redo log buffer),是易失的
- 重做日志文件(redo log file),是持久的
log block(日志块)
在InnoDB引擎中,重做日志缓存、重做日志文件都是以512字节进行存储的,称之为重做日志块(redo log block)。
log group(重做日志组)
InnoDB存储引擎实际上只有一个log group。
redo log 重做日志格式
到InnoDB1.2版本时,一共有51种重做日志类型。
LSN(Log Sequence Number)
表示日志序列号,在InnoDB存储引擎中,LSN占用8字节,并且单调递增。LSN表示的含义有:
- 重做让日志写入的总量,单位为字节
- checkpoint的位置
- 页的版本
恢复
不管上次数据库运行时是否正常关闭,都会尝试进行恢复。由于重做日志时物理日志,因此其是幂等的。
undo
基本概念
redo存放在重做日志文件中。
undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)。undo段位于共享表空间内。
unodo是逻辑日志:当用户执行ROLLBACK时,对于事务中每个INSERT,InnoDB存储引擎会完成一个DELETE;对于每个DELETE,InnoDB存储引擎会执行一个INSERT;对于每个UPDATE,InnoDB存储引擎会执行一个相反的UPDATE,将修改前的行放回去。
MVCC:
出了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成的。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前行版本信息,以此实现非锁定读取。
undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这时因为undo log也需要持久性的保护。
undo 存储管理
当事务提交时,InnoDB存储引擎会做两件事:
- 将undo log放入列表中,以供之后的purge操作
- 判断undo log所在的页是否可以重用,若可以则分配给下个事务使用
因为多版本中undo log依赖的问题,undo log最终的删除由purge线程来完成,并不是在事务提交后立即删除(insert undo log 可以立即删除)。
undo log格式
在InnoDB存储引擎中,undo log分为两种:
- insert undo log(对其他事务不可见,可以在事务提交后立即删除)
- update undo log(对delete和update产生的undo log,MVCC机制可能会使用到这些log,不能在事务提交后删除,提交时放入undo log 链表,等待purge线程进行最后的删除)