锁
讨论InnoDB如何实现事务的隔离性的。
- 最大程度地利用数据库的并发访问
- 确保每个用户能以一致的方式读取和修改数据
将介绍InnoDB存储引擎对表中的数据的锁定,同时分析InnoDB存储引擎会以怎样的粒度锁定数据。
人们认为行级锁的一个神话:人们认为行级锁总会增加开销。实际上,只有当实现本身会增加开销时,行级锁才会增加开销。InnoDB存储引擎不需要锁升级,因为一个锁和多个锁的开销是 的。
什么是锁
锁机制用于管理对共享资源的并发访问。InnoDB存储引擎在行级别上对表数据上锁,页会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。如缓冲池的LRU列表。
InnoDB锁提供:
- 一致性的非锁定读。
- 行级锁支持,且行级锁没有额外开销,同时得到并发性和一致性。
MyISAM:
- 表级锁,不支持行级锁。
lock与latch
lock与latch都被称为锁,但具有截然不同的含义
- latch称为轻量级锁,要求锁定的时间必须非常短。若持续时间长,则性能会非常差。
- 其对象为线程,保护内部数据结构,存在于每个数据结构的对象中。
- 分为mutex互斥量和rwlock读写锁。
- 保证并发线程操作临界资源的正确性,并没有死锁检测机制。
- lock用来锁定的是数据库中的对象,如表、页、行。
- 其对象为事务,持续整个事务过程。
- 分为包含行锁、表锁、意向锁。
- lock的对象仅在commit或rollback后释放。
- 存在死锁机制。通过waits-for graph、time out等机制进行死锁检测于处理。
- 存在于Lock Manager的哈希表中。
InnoDB存储引擎中的锁
锁的类型
InnoDB存储引擎实现了两种标准的行级锁。
- 共享锁S LOCK,允许事务读一行数据。
- 排他锁X LOCK,允许事务删除或更新一行数据。
兼容性:
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
InnoDB存储引擎支持多粒度锁定,允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,支持一种额外的锁方式:意向锁。
意向锁:将锁定的对象分为多个层次,意味着事务希望在更细粒度上加锁。
- 如果希望对记录上锁,则分别需要对数据库、表、页上意向锁IX,最后对记录上X锁。
- 若其中任一个部分导致等待,那么该操作需要等待粗粒度锁的完成
- 如有事务在表1进行了S锁,则由于需要IX锁不兼容,需要等待
InnoDB存储引擎支持的意向锁是表级别的锁
- 意向共享锁,事务想获得一张表中某几行的共享锁
- 意向排他锁,事务想获得一张表中某几行的排他锁
查看当前锁请求的信息
show engine innodb status
可以查看当前锁请求的信息:
select * from table.innodb_locks\G
一致性非锁定读
多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL的InnoDB存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用MVCC无法实现。
- 如果读取的行正在执行DEL或update操作,这时读取操作不会因此去等待行上锁的释放,而是会去读行的一个快照数据。
- 快照数据是指该行的之前版本的数据。该实现是通过undo段来完成。undo用来在事务中回滚数据。
- 快照数据本身没有额外的开销,并不需要上锁,因为没有事务需要对历史的数据进行修改。
非锁定读:不需要等待访问的行上X锁的释放。因此非锁定读机制极大地提高了数据库并发性。
- 是默认的读取方式。
- 在不同事务隔离级别下读取方式不同。
- 即使都是使用非锁定的一致性读,对快照数据的定义也各不相同。
版本号
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。
隐藏的列
MVCC在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
- 创建版本号:指示创建一个数据行的快照时的系统版本号。
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
Undo日志
MVCC使用到的快照存储在Undo日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。
实现过程
以下实现过程针对可重复读隔离级别。
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
1. SELECT
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没有对一个数据行做修改的事务称为T,T所要读取的数据行快照的创建版本号必须小于T的版本号,因为如果大于或者等于T的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T所要读取的数据行快照的删除版本号必须大于T的版本号,因为如果小于等于T的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
2. INSERT
将当前系统版本号作为数据行快照的创建版本号。
3.DELETE
将当前系统版本号作为数据行快照的删除版本号。
4. UPDATE
将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行DELETE后执行INSERT。
快照读与当前读
快照读
使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。
1 | select * from table ...;Copy to clipboardErrorCopied |
当前读
读取的是最新的数据,需要加锁。以下第一个语句需要加S锁,其它都需要加X锁。
1 | select * from table where ? lock in share mode; |
快照定义
在READ COMMITED和REPEATABLE READ下,采用非锁定的一致性读。但是快照数据的定义不同
- 已提交读下,总是读取被锁定行最新一份快照数据:
- 在事务开始后,有其他的事务对该行数据进行了修改并commit,则会出现对一个数据的读取,两次结果不一致。
- 因为每次会读取最新一份快照。
- 可重复读下,总是读取事务开始时的行数据版本。
- 即只要事务开始了,在该次事务当中,对同一个数据的读取,永远不会改变。
MVCC
从上图6-4可以看到,快照数据是当前行数据的历史版本,因此可能存在多个版本,即存在不止一个快照数据。由此带来的并发控制称为多版本并发控制(MVCC)。
一致性锁定读
在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。
InnoDB存储引擎对于Select语句支持两种一致性锁定读操作,这些操作必须在一个事务当中:
- select … for update。
- 对读取的行记录加一个X锁,其他事务不能对已经锁定的行加任何锁。
- select … lock in share mode。
- 对读取的行记录加一个S锁,其他事务可以加S锁。
自增长与锁
外键和锁
对于一个外键列,如果没有显式地对这个列加索引,InnoDB存储引擎会自动对其加一个索引,因为可以避免表锁。
对于外键值的插入和更新:
- 首先向父表查询,并使用select … lock in share mode,以防止数据不一致的问题。如果使用一致性非锁定读会产生数据不一致问题。
锁的算法
Record Locks
锁定一个记录上的索引,而不是记录本身,即不是行数据。Read Committed下采用。
Record locks会锁住索引记录(而不是行数据),如果表没有设置索引,InnoDB会自动在主键上创建隐藏的聚簇索引,因此Record Locks依然可以使用。
Gap Locks
间隙锁,锁定一个范围,但不包含记录本身。作用是阻止多个事务将记录插入到同一范围内,会导致Phantom Problem问题的产生。
- 如果对于辅助索引b,会话A锁定了b=3的记录。
- 若没有gap Lock锁定(3,6),则用户可以插入索引b=3的记录。
- 此时会话A再次查询时,会返回不同的记录。
而使用了gap locks后,例如当一个事务执行以下 语句,其它事务就不能在t.c中插入 15。
1 | SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; |
locking reads,UPDATE和DELETE时,除了对唯一索引的唯一搜索外都会获取gap锁或next-key锁,即锁住其扫描的范围。
如果表扫描没有用到索引,则会锁住整个表。
READ COMMITTED
1 | For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE statements, |
只会锁住已有记录,不会加gap锁。
SERIALIZABLE
1 | This level is like REPEATABLE READ, but InnoDB implicitly converts all plain |
和REPEATABLE READ的主要区别在于把普通的SELECT变成SELECT … LOCK IN SHARE MODE,即对普通的select都会获取gap锁或next-key锁。
Next-Key Lock
Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并锁定记录本身。
- Repeadtable Read采用。
- 如果查询用到的索引有10,11,13,20,则该索引可能被Next-Key锁定的区间为(-无穷,10],(10,11],(11,13],(13,20],(20,+无穷)。
- 当查询的索引含有唯一属性(主键),则优化为Record Lock,如主键。即此时查询是wan’quan
- 对于辅助索引,会对包含该键值的上下两个区间上锁,上区间加next-key lock,下区间加gap lock。
- 如11,则上区间(10,11],下区间(11,13)。
锁算法规则
InnoDB存储引擎的锁算法的一些规则如下所示:
- 在不通过索引条件查询时,InnoDB 会锁定表中的所有记录。所以,如果考虑性能,WHERE语句中的条件查询的字段都应该加上索引。
- InnoDB通过索引来实现行锁,而不是通过锁住记录。因此,当操作的两条不同记录拥有相同的索引时,也会因为行锁被锁而发生等待。
- 由于InnoDB的索引机制,数据库操作使用了主键索引,InnoDB会锁住主键索引;使用非主键索引时,InnoDB会先锁住非主键索引,再锁定主键索引。
- 当查询的索引是唯一索引(不存在两个数据行具有完全相同的键值)时,InnoDB存储引擎会将Next-Key Lock降级为Record Lock,即只锁住索引本身,而不是范围。
- InnoDB对于辅助索引有特殊的处理,不仅会锁住辅助索引值所在的范围,还会将其下一键值加上Gap LOCK。
- InnoDB使用Next-Key Lock机制来避免Phantom Problem(幻读问题)。
解决Phantom Problem幻像问题
InnoDB存储引擎使用Next-Key Lock避免幻像问题。
Phantom Problem幻像问题:同一个事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。
锁问题
锁提高了并发,但会带来潜在的问题。
脏读
脏读即读取到了脏数据,存在的级别为Read Uncommitted
- 脏数据:事务对缓冲池中行记录的修改,并且还没有被提交
- 如果读到了脏数据,即一个事务读取到另一个事务未提交的数据,违反了数据库的隔离性
- 脏页:在缓冲池已经被修改的页,但还没有刷新到磁盘中
- 由于内存与磁盘的一步造成,不影响数据的一致性,并且最终会到达一致性
不可重复读
在一个事务内多次读取同一数据集合,在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些DML操作。因此在第一个事务中的两次读数据间,由于第二个事务的修改,第一个事务两次读取到的数据可能不一样。
与脏读的区别:
- 脏读读到未提交的数据,不可重复读读取到已经提交的数据。
- 不可重复读违反了数据库事务一致性的要求。
由于读取到的是已经提交的数据,一般而言不会带来很大问题,因此一些数据库允许该现象。
丢失更新
一个事务的更新操作会被另一个事务的更新操作所覆盖,导致数据的不一致:
- 事务T1将行记录r更新为v1,但是事务T1未提交。
- 同时,事务T2将行记录r更新为v2,事务T2未提交。
- 事务T1提交。
- 事务T2提交。
在任何隔离级别下都不会发生,但可能出现另一个问题:
- 事务T1查询一行数据,放入本地内存,显示给一个用户U1。
- 同时,事务T2查询该记录,显式给用户U2。
- U1修改这行记录,更新并提交。
- U2修改这行记录,更新并提交。此时没有去读取新的数据。
- 银行转账场景下会出现问题。
阻塞
由于不同锁间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它占用的资源。
参数innodb_lock_wait_timeout
用来控制等待的时间,默认是50s;innodb_rollback_on_timeout
用来设定是否在等待超时时对进行中的事务进行回滚操作,默认为false;
死锁
锁升级
锁升级是指将当前锁的粒度降低。例如将一个表的1000行锁升级为一个页锁。
升级保护了系统资源,防止系统使用太多内存来维护锁,一定程度上提高了效率。
InnoDB存储引擎不会有锁升级。因为根据页进行加锁,并采用位图方式,开销很小。如果对3000000数据页,每页100记录进行加锁,如果每个页存储的锁信息占用30个字节,锁对象仅需要90MB。
事务
事务指的是满足ACID特性的一组操作,可以通过Commit提交一个事务,也可以使用Rollback进行回滚。
ACID:
- 原子性(atomicity,或称不可分割性)。一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
- 一致性(consistency)。在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
- 隔离性(isolation,又称独立性)。数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性(durability)。事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
- 使用重复日志来保证持久性
这几个特性不是一种平级关系:
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库崩溃的情况。
AUTOCOMMIT
MySQL 默认采用自动提交模式。也就是说,如果不显式使用START TRANSACTION
语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。
事务的使用
- transaction
- rollback
- commit
- savepoint:指事务处理中设置的临时占位符,可以对它发布回退,而不是回退整个事务
1 | select * from ordertotals; |
认识事务
从事务理论的角度来说,可以将事务分为以下几种类型:
- 扁平事务。
- 带有保存点的扁平事务。
- 链事务。
- 嵌套事务。
- 分布式事务
事务的分类
扁平事务:
所有操作都处于同一层次,由begin work
开始Commit work
或rollback work
结束,其间的操作都是原子的,要么都执行要么都回滚。
带有保存点的扁平事务:
在扁平事务的基础上,允许事务执行过程中回滚到同一事务中较早的一个状态。保存点用来停止系统应该记住事务当前的状态,以便当之后发生错误时,事务能够回到保存点当时的状态。
当系统崩溃时,所有保存点都会消失,其保存点并非持久的。
链事务:
保存点事务的一种变种。在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。提交事务操作与开始下一个事务操作将合并为一个原子操作,意味着下一个事务可以看到上一个事务的结果。
嵌套事务:
是一个层次结构框架,由一个顶层事务控制着各个层次的事务,顶层事务下嵌套的事务被称为子事务,其控制着每一个局部的变换。
- 嵌套事务是由若干事务组成的一棵树,子事务既可以是嵌套事务,也可以是扁平事务。
- 处在叶结点的事务是扁平事务,但每个子事务从根到叶结点的距离可以是不同的。
- 子事务可以提交也可以回滚,但是它的提交操作不会马上生效,除非父事务已经提交,即所有子事务必须在顶层事务提交后才真正提交。
- 树的任一个事务回滚都会引起它所有子事务一起回滚。
分布式事务:
通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
事务的隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 Read uncommitted | 可能 | 可能 | 可能 |
已提交读 Read committed | 可能 | 可能 | |
可重复读 Repeatable read | 不可能 | 不可能 | 可能 |
可串行化Serializable | 不可能 | 不可能 | 不可能 |
未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)。
- 脏读:即读取到了脏数据。
- 脏数据:事务对缓冲池中行记录的修改,并且还没有被提交。
- 如果读到了脏数据,即一个事务读取到另一个事务未提交的数据,违反了数据库的隔离性。
可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读。
- 不可重复读:读取到其他事务已经提交的数据:
- 在一个事务内多次读取同一数据集合,在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些DML操作。因此在第一个事务中的两次读数据间,由于第二个事务的修改,第一个事务两次读取到的数据可能不一样。
- 不可重复读违反了数据库事务一致性的要求。
- 由于读取到的是已经提交的数据,一般而言不会带来很大问题,因此一些数据库允许该现象。
串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞:
- 幻读:不可重复读的一种特殊场景:
- 幻读是指当事务不是独立执行时发生的一种现象。
- 事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。
- 幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
默认的隔离级别:可重复读Repeated Read。采用Next-KeyLock算法避免锁的产生。
同时使用隔离级别的开销基本一致。因此即使使用未提交读也不会得到性能的大幅提升。
事务的实现
事务的原子性、一致性、持久性通过数据库的redo log和undo log完成
- redo log保证事务的原子性和持久性,恢复提交事务修改的页操作,是物理日志,记录页的物理修改操作。
- undo log保证事务的一致性,回滚行记录到某个特定版本,是逻辑日志,根据每行记录进行记录。
redo
redo由两部分组成:
- 内存中的重做日志缓冲redo log buffer是易丢失的
- 重做日志文件redo log file是是持久的。
InnoDB通过force log at commit机制实现事务的持久性,即当事务提交时,必须先将事务的所有日志写入到重做日志文件进行持久化,待事务的Coomit操作完成才算结束。
在每次将重做日志缓冲写入重做日志文件后,InnoDB都需要调用一次fsunc操作,因此磁盘性能决定了事务提交的性能。
purge
purge负责最终完成delete和update操作,由于MVCC,记录不能在事务提交时立即进行处理。
如果记录不被任何其他事务所引用,那么就可以真正进行delete操作。
group commit
若事务为非只读事务,则每次事务提交需要进行一次fsync操作,以确保操作日志都写入磁盘了,而group commit使得一次fsync可以刷新确保多个事务日志文件被写入文件。
分布式事务
MySQL数据库分布式事务
InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持对分布式事务的实现。
- 分布式事务:允许多个独立的事务资源参与到一个全局的事务中。
- 事务资源:通常是关系型数据库系统,页可以是其他类型的资源。
- 全局事务要求在其中的所有参与的事务要么都提交,要么都回滚。
实现分布式事务,InnoDB存储引擎的事务隔离级别必须为Serializable。
XA事务允许不同数据库键的分布式事务,如MySQL、oracle数据库,只要参与全局事务中的每个节点都支持XA事务。
XA事务由一个或多个资源管理器、一个事务管理器以及一个应用程序组成。
- 资源管理器:提供访问事务资源的方法,通常一个数据库就是一个资源管理器。
- 事务管理器:协调参与全局事务中的各个事务,需要和参与全局事务的所有资源管理器进行通信。
- 应用程序:定义事务的边界,指定全局事务中的操作。
分布式事务的实现
- 采用两段式提交的方式:
- 第一阶段:所有参与全局事务的节点都开始准备,告诉事务管理器它们准备好提交了。
- 第二阶段:事务管理器告诉资源管理器执行ROLLBACK或COMMIT。
- 与本地事务不同的是,分布式事务要多一次prepare工作,待收到所有节点的同意信息后,再进行commit或者rollback。
Java实现
Java的JTA可以很好的支持MySQL的分布式事务
1 | class MyXid implements Xid{ |
内部XA事务
- 之前的分布式事务时外部事务,即资源管理器是MySQL数据库本身。
- 另一种分布式事务在存储引擎与插件间,或者存在于存储引擎与存储引擎间,称为内部XA事务。
常见的为binlog与InnoDB存储引擎间。