引言redo log 与 undo log介绍redo logundo logmysql锁技术共享锁和排他锁意向锁记录锁间隙锁下一键锁插入意图锁自动上锁空间索引的谓词锁MVCC基础事务的实现原子性的实现什么是原子性:undo log 的生成根据undo log 进行回滚持久性的实现隔离性实现READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READ(Mysql默认隔离级别)SERIALIZABLE一致性的实现InnoDB和ACID模型原子性与InnoDB一致性与InnoDB隔离性与InnoDB持久性与InnoDB事务调度InnoDB中的死锁InnoDB死锁示例死锁检测和回滚如何最小化和处理死锁总结
引言
相信大家都用过事务以及了解他的特点,如原子性(Atomicity),一致性(Consistency),隔离型(Isolation)以及持久性(Durability)等。今天想跟大家一起研究下事务内部到底是怎么实现的,在讲解前我想先抛出个问题:
事务想要做到什么效果?
按我理解,无非是要做到可靠性以及并发处理。
可靠性:数据库要保证当insert或update操作时抛异常或者数据库crash的时候需要保障数据的操作前后的一致,想要做到这个,我需要知道我修改之前和修改之后的状态,所以就有了undo log和redo log。
并发处理:也就是说当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,为了避免读到脏数据,所以需要对事务之间的读写进行隔离,至于隔离到啥程度得看业务系统的场景了,实现这个就得用MySQL 的隔离级别。
下面我首先讲实现事务功能的三个技术,分别是日志文件(redo log 和 undo log),锁技术以及MVCC,然后再讲事务的实现原理,包括原子性是怎么实现的,隔离型是怎么实现的等等。最后在做一个总结,希望大家能够耐心看完
1、redo log与undo log介绍
2、mysql锁技术以及MVCC基础
3、事务的实现原理
4、总结
redo log 与 undo log介绍
1. redo log
什么是redo log ?
redo log叫做重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。假设有个表叫做tb1(id,username) 现在要插入数据(3,ceshi)
代码语言:javascript复制starttransaction;selectbalance frombank wherename="zhangsan";// 生成 重做日志 balance=600updatebank setbalance = balance - 400;// 生成 重做日志 amount=400updatefinance setamount = amount 400;commit;
redo log 有什么作用?
mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Boffer Pool(缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步。
那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及执行上面图中红色的操作。这样会导致丢部分已提交事务的修改信息!
所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。
总结:
redo log是用来恢复数据的 用于保障,已提交事务的持久化特性
2.undo log
什么是 undo log ?
undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
还用上面那两张表
每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。
undo log 有什么作用?
undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。
总结:
undo log是用来回滚数据的用于保障 未提交事务的原子性
mysql锁技术
- 共享锁和排他锁
- 意向锁
- 记录锁
- 间隙锁
- 下一键锁
- 插入意图锁
- 自动上锁
- 空间索引的谓词锁
共享锁和排他锁
InnoDB
实现标准的行级锁定,其中有两种类型的锁:共享(S
)锁和排他(X
)锁。
- 甲共享(
S
)锁允许持有锁读取行的事务。 - 一个独占(
X
)锁允许持有锁,更新或删除行的事务。
如果事务T1
持有S
对row 的共享()锁r
,则来自某些不同事务T2
的对行锁定的请求r
将按以下方式处理:
- 由A请求
T2
用于S
锁可以立即被授予。其结果是,无论是T1
与T2
持有S
的锁r
。 - 通过请求
T2
一个X
锁不能立即授予。
如果一个事务在row上T1
拥有一个独占(X
)锁r
,则不能立即批准某个不同事务T2
对任一类型的锁的请求r
。相反,事务T2
必须等待事务T1
释放对row的锁定r
。
意向锁
InnoDB
支持多种粒度锁定,允许行锁和表锁并存。例如,诸如的语句 在指定表上LOCK TABLES ... WRITE
采用排他锁(X
锁)。为了使在多个粒度级别上的锁定变得切实可行,请InnoDB
使用 意图锁定。意向锁是表级锁,指示事务稍后对表中的行需要哪种类型的锁(共享锁或排他锁)。有两种类型的意图锁:
- 的意图共享锁(
IS
)指示一个事务打算设置一个共享 上各个行锁定在表中。 - 的意图独占锁(
IX
)指示一个事务打算设定各行的排他锁在表中。
例如,SELECT ... FOR SHARE
设置一个IS
锁,然后 SELECT ... FOR UPDATE
设置一个IX
锁。
意向锁定协议如下:
- 在事务可以获取表中某行的共享锁之前,它必须首先获取表中的
IS
锁或更高级别的锁。 - 在事务可以获取表中某行的排它锁之前,它必须首先获取
IX
该表中的锁。
表级锁类型的兼容性汇总在以下矩阵中。
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果一个锁与现有锁兼容,则将其授予请求的事务,但如果与现有锁冲突,则不授予该锁。事务等待直到冲突的现有锁被释放。如果锁定请求与现有锁定发生冲突,并且由于可能导致死锁而无法被授予许可 ,则会发生错误。
意向锁不会阻止除全表请求(例如LOCK TABLES ... WRITE
)以外的任何内容。意向锁定的主要目的是表明有人正在锁定表中的行,或者打算锁定表中的行。
对于意图锁定事务数据出现类似于在下面SHOW ENGINE INNODB STATUS
和 InnoDB的监视器 输出:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
记录锁
记录锁定是对索引记录的锁定。例如, SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
可以防止从插入,更新或删除行,其中的值的任何其它交易t.c1
是 10
。
记录锁定始终锁定索引记录,即使没有定义索引的表也是如此。在这种情况下,请 InnoDB
创建一个隐藏的聚集索引,并将该索引用于记录锁定。
用于在记录锁定事务数据出现类似于在以下SHOW ENGINE INNODB STATUS
和 InnoDB的监视器 输出:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
间隙锁
间隙锁定是对索引记录之间的间隙的锁定,或者是对第一个或最后一个索引记录之前的间隙的锁定。例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
防止其他事务将value 15
插入column中t.c1
,无论该列 中是否已有这样的值,因为该范围中所有现有值之间的间隙都是锁定的。
间隙可能跨越单个索引值,多个索引值,甚至为空。
间隙锁是性能和并发性之间权衡的一部分,并且在某些事务隔离级别而非其他级别中使用。
对于使用唯一索引来锁定唯一行来锁定行的语句,不需要间隙锁定。(这不包括搜索条件仅包含多列唯一索引的某些列的情况;在这种情况下,会发生间隙锁定。)例如,如果该id
列具有唯一索引,则以下语句仅使用一个具有id
值100 的行的索引记录锁定,其他会话是否在前面的间隙中插入行并不重要:
SELECT * FROM child WHERE id = 100;
如果id
未建立索引或索引不唯一,则该语句会锁定前面的间隙。
在这里还值得注意的是,可以通过不同的事务将冲突的锁保持在间隙上。例如,事务A可以在间隙上保留一个共享的间隙锁(间隙S锁),而事务B可以在同一间隙上保留排他的间隙锁(间隙X锁)。允许冲突的间隙锁的原因是,如果从索引中清除记录,则必须合并由不同事务保留在记录上的间隙锁。
间隙锁定InnoDB
是“ 纯粹抑制性的 ”,这意味着它们的唯一目的是防止其他事务插入间隙。间隙锁可以共存。一个事务进行的间隙锁定不会阻止另一事务对相同的间隙进行间隙锁定。共享和专用间隙锁之间没有区别。它们彼此不冲突,并且执行相同的功能。
间隙锁定可以显式禁用。如果将事务隔离级别更改为,则会发生这种情况 READ COMMITTED
。在这种情况下,将禁用间隙锁定来进行搜索和索引扫描,并且间隙锁定仅用于外键约束检查和重复键检查。
使用READ COMMITTED
隔离级别还有其他影响 。MySQL评估WHERE
条件后,将释放不匹配行的记录锁。对于 UPDATE
语句,请InnoDB
执行“ 半一致 ”读取,以便将最新的提交版本返回给MySQL,以便MySQL可以确定行是否与的WHERE
条件匹配UPDATE
。
下一键锁
下一键锁定是索引记录上的记录锁定和索引记录之前的间隙上的间隙锁定的组合。
InnoDB
执行行级锁定,以使其在搜索或扫描表索引时对遇到的索引记录设置共享或排他锁。因此,行级锁实际上是索引记录锁。索引记录上的下一键锁定也会影响该索引记录之前的“ 间隙 ”。即,下一键锁定是索引记录锁定加上索引记录之前的间隙上的间隙锁定。如果一个会话R
在索引中的记录上具有共享或排他锁 ,则另一会话不能R
在索引顺序之前的间隙中插入新的索引记录 。
假定索引包含值10、11、13和20。此索引的可能的下一键锁定涵盖以下间隔,其中,圆括号表示排除区间端点,方括号表示包括端点:
代码语言:javascript复制(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
对于最后一个间隔,下键锁锁定在上面的索引的最大值和间隙“ 确界 ” 具有比在索引实际上任何值高的值的伪记录。最高不是真正的索引记录,因此,实际上,此下一键锁定仅锁定最大索引值之后的间隙。
默认情况下,InnoDB
以 REPEATABLE READ
事务隔离级别运行。在这种情况下,请InnoDB
使用next-key锁进行搜索和索引扫描,这可以防止幻像行。
用于下一个键锁定事务数据出现类似于在下面SHOW ENGINE INNODB STATUS
和 InnoDB的监视器 输出:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
插入意图锁
插入意图锁定是一种通过INSERT
行插入之前的操作设置的间隙锁定 。此锁发出插入意图的信号是,如果多个事务未插入间隙中的相同位置,则无需等待插入到同一索引间隙中的多个事务。假设存在索引记录,其值分别为4和7。单独的事务分别尝试插入值5和6,在获得插入行的排他锁之前,每个事务都使用插入意图锁来锁定4和7之间的间隙,但不要互相阻塞,因为行是无冲突的。
下面的示例演示了在获得对插入记录的排他锁之前,使用插入意图锁的事务。该示例涉及两个客户端A和B。
客户端A创建一个包含两个索引记录(90和102)的表,然后启动一个事务,该事务将排他锁放置在ID大于100的索引记录上。排他锁在记录102之前包括一个间隙锁:
代码语言:javascript复制mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);
mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
-----
| id |
-----
| 102 |
-----
客户B开始交易以将记录插入空白。事务在等待获得排他锁的同时获取插入意图锁。
代码语言:javascript复制mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);
用于插入意图锁定事务数据出现类似于在下面 SHOW ENGINE INNODB STATUS
和 InnoDB的监视器 输出:
RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000066; asc f;;
1: len 6; hex 000000002215; asc " ;;
2: len 7; hex 9000000172011c; asc r ;;...
自动上锁
一个AUTO-INC
锁是通过交易将与表中取得一个特殊的表级锁 AUTO_INCREMENT
列。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务都必须等待自己在该表中进行插入,以便第一个事务插入的行接收连续的主键值。
该innodb_autoinc_lock_mode
配置选项控制用于自动增加锁定的算法。它允许您选择如何在可预测的自动增量值序列与插入操作的最大并发性之间进行权衡。
空间索引的谓词锁
InnoDB
支持SPATIAL
包含空间列的列的索引。
要处理涉及SPATIAL
索引的操作的锁定 ,下一键锁定不能很好地支持支持REPEATABLE READ
或 SERIALIZABLE
事务隔离级别。多维数据中没有绝对排序概念,因此不清楚哪个是 “ 下一个 ”键。
要支持具有SPATIAL
索引的表的隔离级别 ,请InnoDB
使用谓词锁。甲SPATIAL
索引包含最小外接矩形(MBR)值,因此, InnoDB
通过设置用于查询的MBR值的谓词锁强制上的索引一致的读取。其他事务不能插入或修改将匹配查询条件的行。
MVCC基础
MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。
InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间,当然存储的并不是实际的时间值,而是系统版本号。
以上片段摘自《高性能Mysql》这本书对MVCC的定义。他的主要实现思想是通过数据多版本来做到读写分离。从而实现不加锁读进而做到读写并行。
InnoDB与MVCC
InnoDB
是一个 多版本存储引擎:它保留有关已更改行的旧版本的信息,以支持诸如并发和回滚之类的事务功能 。该信息以称为回滚段的数据结构存储在表空间中 (在Oracle中类似的数据结构之后)。InnoDB
使用回滚段中的信息来执行事务回滚中所需的撤消操作。它还使用该信息来构建行的早期版本,以实现 一致的读取。
在内部,InnoDB
向数据库中存储的每一行添加三个字段。6个字节的DB_TRX_ID
字段表示插入或更新该行的最后一个事务的事务标识符。此外,删除在内部被视为更新,在该更新中,行中的特殊位被设置为将其标记为已删除。每行还包含一个7字节的 DB_ROLL_PTR
字段,称为滚动指针。回滚指针指向写入回滚段的撤消日志记录。如果行已更新,则撤消日志记录将包含在更新行之前重建行内容所必需的信息。一个6字节的DB_ROW_ID
字段包含一个行ID,该行ID随着插入新行而单调增加。如果 InnoDB
自动生成聚集索引,该索引包含行ID值。否则,该 DB_ROW_ID
列不会出现在任何索引中。
回滚段中的撤消日志分为插入和更新撤消日志。插入撤消日志仅在事务回滚时才需要,并且在事务提交后可以立即将其丢弃。更新撤消日志也用于一致的读取中,但是只有在不存在为其InnoDB
分配了快照的事务( 一致的读取可能需要更新撤消日志中的信息来构建数据库的早期版本)后,才可以将其删除行。
定期提交您的事务,包括仅发出一致读取的事务。否则, InnoDB
无法丢弃更新撤消日志中的数据,并且回滚段可能会变得太大,从而填满了您的表空间。
回滚段中撤消日志记录的物理大小通常小于相应的插入或更新的行。您可以使用此信息来计算回滚段所需的空间。
在InnoDB
多版本方案中,当您使用SQL语句删除行时,并不会立即将其从数据库中物理删除。InnoDB
仅在丢弃为删除而编写的更新撤消日志记录时,才物理删除相应的行及其索引记录。此删除操作称为purge,它非常快,通常花费与执行删除操作的SQL语句相同的时间顺序。
如果您以大约相同的速率在表中以较小的批次插入和删除行,则由于所有“ 死 ”行,清除线程可能会开始滞后并且表可能会变得越来越大 ,从而使所有内容都受磁盘约束慢。在这种情况下,请限制新行的操作,并通过调整innodb_max_purge_lag
系统变量来向清除线程分配更多资源 。
多版本索引和二级索引
InnoDB
多版本并发控制(MVCC)对二级索引的处理与对聚簇索引的处理不同。聚簇索引中的记录将就地更新,其隐藏的系统列指向撤消日志条目,可从中重建记录的早期版本。与聚簇索引记录不同,辅助索引记录不包含隐藏的系统列,也不会就地更新。
更新二级索引列时,将对旧的二级索引记录进行删除标记,插入新记录,并最终清除带有删除标记的记录。当二级索引记录被删除标记或二级索引页由较新的事务更新时,InnoDB
在聚集索引中查找数据库记录。在聚集索引中,DB_TRX_ID
检查记录的记录,如果在启动读取事务后修改了记录,则从撤消日志中检索记录的正确版本。
如果二级索引记录被标记为删除或二级索引页被更新的事务更新, 则不使用覆盖索引技术。而不是从索引结构中返回值,而是InnoDB
在聚集索引中查找记录。
但是,如果启用了 索引条件下推(ICP)优化,并且WHERE
只能使用索引中的字段来评估部分条件,则MySQL服务器仍会将WHERE
条件的这一部分下推到存储引擎,在其中使用指数。如果找不到匹配的记录,则避免聚集索引查找。如果找到了匹配的记录,即使在删除标记的记录中,也要在 InnoDB
聚簇索引中查找记录。
MVCC在mysql中的实现依赖的是undo log与read view
undo log :undo log 中记录某行数据的多个版本的数据。
read view :用来判断当前版本数据的可见性
事务的实现
前面讲的重做日志,回滚日志以及锁技术就是实现事务的基础。
事务的原子性是通过 undo log 来实现的
事务的持久性性是通过 redo log 来实现的
事务的隔离性是通过 (读写锁 MVCC)来实现的
而事务的终极大 boss 一致性是通过原子性,持久性,隔离性来实现的!!!
原子性,持久性,隔离性折腾半天的目的也是为了保障数据的一致性!
总之,ACID只是个概念,事务最终目的是要保障数据的可靠性,一致性。
原子性的实现
什么是原子性:
一个事务必须被视为不可分割的最小工作单位,一个事务中的所有操作要么全部成功提交,要么全部失败回滚,对于一个事务来说不可能只执行其中的部分操作,这就是事务的原子性。
上面这段话取自《高性能MySQL》这本书对原子性的定义,原子性可以概括为就是要实现要么全部失败,要么全部成功。
以上概念相信大家伙儿都了解,那么数据库是怎么实现的呢?就是通过回滚操作。所谓回滚操作就是当发生错误异常或者显式的执行rollback语句时需要把数据还原到原先的模样,所以这时候就需要用到undo log来进行回滚,接下来看一下undo log在实现事务原子性时怎么发挥作用的
undo log 的生成
假设有两个表 bank和finance,表中原始数据如图所示,当进行插入,删除以及更新操作时生成的undo log如下面图所示:
从上图可以了解到数据的变更都伴随着回滚日志的产生:
(1) 产生了被修改前数据(zhangsan,1000) 的回滚日志
(2) 产生了被修改前数据(zhangsan,0) 的回滚日志
根据上面流程可以得出如下结论:
1.每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上
2.所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。
思考:为什么先写日志后写数据库?---稍后做解释
根据undo log 进行回滚
为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚
回滚操作就是要还原到原来的状态,undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,比如:
(1) 如果在回滚日志里有新增数据记录,则生成删除该条的语句
(2) 如果在回滚日志里有删除数据记录,则生成生成该条的语句
(3) 如果在回滚日志里有修改数据记录,则生成修改到原先数据的语句
持久性的实现
事务一旦提交,其所作做的修改会永久保存到数据库中,此时即使系统崩溃修改的数据也不会丢失。
先了解一下MySQL的数据存储机制,MySQL的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的。为此,为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:
读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;
写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;
上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!!!
因为我们的数据已经提交了,但此时是在缓冲池里头,还没来得及在磁盘持久化,所以我们急需一种机制需要存一下已提交事务的数据,为恢复数据使用。
于是 redo log就派上用场了。下面看下redo log是什么时候产生的
既然redo log也需要存储,也涉及磁盘IO为啥还用它?
(1)redo log 的存储是顺序存储,而缓存同步是随机操作。
(2)缓存同步是以数据页为单位的,每次传输的数据大小大于redo log。
隔离性实现
隔离性是事务ACID特性里最复杂的一个。在SQL标准里定义了四种隔离级别,每一种级别都规定一个事务中的修改,哪些是事务之间可见的,哪些是不可见的。
级别越低的隔离级别可以执行越高的并发,但同时实现复杂度以及开销也越大。
Mysql 隔离级别有以下四种(级别由低到高):
- READ UNCOMMITED (未提交读)
- READ COMMITED (提交读)
- REPEATABLE READ (可重复读)
- SERIALIZABLE (可重复读)
只要彻底理解了隔离级别以及他的实现原理就相当于理解了ACID里的隔离型。前面说过原子性,隔离性,持久性的目的都是为了要做到一致性,但隔离型跟其他两个有所区别,原子性和持久性是为了要实现数据的可性保障靠,比如要做到宕机后的恢复,以及错误后的回滚。
那么隔离性是要做到什么呢?隔离性是要管理多个并发读写请求的访问顺序。这种顺序包括串行或者是并行
说明一点,写请求不仅仅是指insert操作,又包括update操作。
总之,从隔离性的实现可以看出这是一场数据的可靠性与性能之间的权衡。
可靠性性高的,并发性能低(比如 Serializable)
可靠性低的,并发性能高(比如 Read Uncommited)
READ UNCOMMITTED
在READ UNCOMMITTED隔离级别下,事务中的修改即使还没提交,对其他事务是可见的。事务可以读取未提交的数据,造成脏读。
因为读不会加任何锁,所以写操作在读的过程中修改数据,所以会造成脏读。好处是可以提升并发处理性能,能做到读写并行。
换句话说,读的操作不能排斥写请求。
优点:读写并行,性能高
缺点:造成脏读
READ COMMITTED
一个事务的修改在他提交之前的所有修改,对其他事务都是不可见的。其他事务能读到已提交的修改变化。在很多场景下这种逻辑是可以接受的。
InnoDB在 READ COMMITTED,使用排它锁,读取数据不加锁而是使用了MVCC机制。或者换句话说他采用了读写分离机制。
但是该级别会产生不可重读以及幻读问题。
什么是不可重读?
在一个事务内多次读取的结果不一样。
为什么会产生不可重复读?
这跟 READ COMMITTED 级别下的MVCC机制有关系,在该隔离级别下每次 select的时候新生成一个版本号,所以每次select的时候读的不是一个副本而是不同的副本。
在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读
REPEATABLE READ(Mysql默认隔离级别)
在一个事务内的多次读取的结果是一样的。这种级别下可以避免,脏读,不可重复读等查询问题。mysql 有两种机制可以达到这种隔离级别的效果,分别是采用读写锁以及MVCC。
采用读写锁实现:
为什么能可重复度?只要没释放读锁,在次读的时候还是可以读到第一次读的数据。
优点:实现起来简单
缺点:无法做到读写并行
采用MVCC实现:
为什么能可重复度?因为多次读取只生成一个版本,读到的自然是相同数据。
优点:读写并行
缺点:实现的复杂度高
但是在该隔离级别下仍会存在幻读的问题,关于幻读的解决我打算另开一篇来介绍。
SERIALIZABLE
该隔离级别理解起来最简单,实现也最单。在隔离级别下除了不会造成数据不一致问题,没其他优点。
--摘自《高性能Mysql》
一致性的实现
数据库总是从一个一致性的状态转移到另一个一致性的状态.
下面举个例子:zhangsan 从银行卡转400到理财账户
代码语言:javascript复制starttransaction;selectbalance frombank wherename="zhangsan";// 生成 重做日志 balance=600updatebank setbalance = balance - 400;// 生成 重做日志 amount=400updatefinance setamount = amount 400;commit;
1.假如执行完 update bank set balance = balance - 400;之发生异常了,银行卡的钱也不能平白无辜的减少,而是回滚到最初状态。
2.又或者事务提交之后,缓冲池还没同步到磁盘的时候宕机了,这也是不能接受的,应该在重启的时候恢复并持久化。
3.假如有并发事务请求的时候也应该做好事务之间的可见性问题,避免造成脏读,不可重复读,幻读等。在涉及并发的情况下往往在性能和一致性之间做平衡,做一定的取舍,所以隔离性也是对一致性的一种破坏。
InnoDB和ACID模型
该ACID模式是一组数据库设计原则强调的是,对于业务数据和关键任务应用重要的可靠性方面。MySQL包含诸如InnoDB
存储引擎严格遵循ACID模型,因此数据不会损坏,结果不会因软件崩溃和硬件故障等异常情况而失真。当您依赖于ACID的功能时,您无需重新发明一致性检查和崩溃恢复机制。如果您有其他软件保护措施,超可靠的硬件或可以容忍少量数据丢失或不一致的应用程序,则可以调整MySQL设置以牺牲一些ACID可靠性,以获得更高的性能或吞吐量。
以下各节讨论MySQL功能(尤其是InnoDB
存储引擎)如何 与ACID模型的类别进行交互:
- 原子性。
- 一致性。
- 隔离性。
- 持久性。
原子性与InnoDB
ACID模型 的原子性方面主要涉及InnoDB
事务。相关的MySQL功能包括:
- 自动提交设置。
COMMIT
声明。ROLLBACK
声明。INFORMATION_SCHEMA
表中的 操作数据。
一致性与InnoDB
ACID模型 的一致性方面主要涉及内部InnoDB
处理,以防止数据崩溃。相关的MySQL功能包括:
InnoDB
[doublewrite缓冲区]。InnoDB
崩溃恢复。
隔离性与InnoDB
ACID模型 的隔离方面主要涉及InnoDB
事务,尤其是适用于每个事务的隔离级别。相关的MySQL功能包括:
- 自动提交设置。
SET ISOLATION LEVEL
声明。InnoDB
锁定的底层细节。在性能调整期间,您可以通过INFORMATION_SCHEMA
表格查看这些详细信息 。
持久性与InnoDB
ACID模型 的持久性方面涉及与特定硬件配置交互的MySQL软件功能。由于取决于您的CPU,网络和存储设备的功能的可能性很多,因此为具体的准则提供最复杂的方面。(这些准则可能采取购买“ 新硬件 ”的形式 。)相关的MySQL功能包括:
InnoDB
doublewrite buffer,由innodb_doublewrite
配置选项打开和关闭 。- 配置选项
innodb_flush_log_at_trx_commit
。 - 配置选项
sync_binlog
。 - 配置选项
innodb_file_per_table
。 - 存储设备(例如磁盘驱动器,SSD或RAID阵列)中的写缓冲区。
- 存储设备中由电池支持的缓存。
- 用来运行MySQL的操作系统,特别是它对
fsync()
系统调用的支持。 - 不间断电源(UPS)保护运行MySQL服务器并存储MySQL数据的所有计算机服务器和存储设备的电源。
- 您的备份策略,例如备份的频率和类型以及备份保留期。
- 对于分布式或托管数据应用程序,MySQL服务器的硬件所位于的数据中心的特定特性,以及数据中心之间的网络连接。
事务调度
InnoDB
使用竞争感知事务调度(CATS)算法对等待锁的事务进行优先级排序。当多个事务正在等待同一对象上的锁时,CATS算法将确定哪个事务首先接收到该锁。
CATS算法通过分配调度权重来确定等待的事务的优先级,调度权是基于事务阻止的事务数来计算的。例如,如果两个事务正在等待对同一对象的锁定,则为阻塞最多事务的事务分配更大的调度权重。如果权重相等,则优先考虑等待时间最长的事务。
注意
在MySQL 8.0.20之前,InnoDB
还使用先进先出(FIFO)算法来调度事务,并且CATS算法仅在重锁争用下使用。MySQL 8.0.20中的CATS算法增强功能使FIFO算法变得多余,从而可以删除它。从MySQL 8.0.20开始,以前由FIFO算法执行的事务调度由CATS算法执行。在某些情况下,此更改可能会影响授予事务锁定的顺序。
您可以通过查询表中的TRX_SCHEDULE_WEIGHT
列 来查看事务调度权重 INFORMATION_SCHEMA.INNODB_TRX
。权重仅针对等待的交易进行计算。LOCK WAIT
如该TRX_STATE
列所报告,等待的事务是处于事务执行状态的事务 。不等待锁的事务将报告NULL TRX_SCHEDULE_WEIGHT
值。
INNODB_METRICS
提供计数器用于监视代码级事务调度事件。有关使用INNODB_METRICS
计数器的信息 。
lock_rec_release_attempts
尝试释放记录锁定的次数。一次尝试可能导致释放零个或多个记录锁,因为单个结构中可能存在零个或多个记录锁。lock_rec_grant_attempts
授予记录锁定的尝试次数。一次尝试可能会导致授予零个或多个记录锁。lock_schedule_refreshes
分析等待图表以更新计划的交易权重的次数。
InnoDB中的死锁
死锁是指由于每个事务都持有对方需要的锁而无法进行其他事务的情况。因为这两个事务都在等待资源变得可用,所以两个都不会释放它持有的锁。
当事务锁定多个表中的行(通过诸如UPDATE
或的 语句SELECT ... FOR UPDATE
)但顺序相反时,可能会发生死锁 。当此类语句锁定索引记录和间隙的范围时,由于时序问题,每个事务都获得了一些锁而没有获得其他锁,也会发生死锁。
为了减少死锁的可能性,请使用事务而不是LOCK TABLES
语句;保持插入或更新数据的事务足够小,以使其长时间不保持打开状态;当不同的事务更新多个表或大范围的行时,SELECT ... FOR UPDATE
在每个事务中使用相同的操作顺序(例如 );在SELECT ... FOR UPDATE
和 UPDATE ... WHERE
语句中使用的列上创建索引。死锁的可能性不受隔离级别的影响,因为隔离级别更改了读取操作的行为,而死锁则是由于写入操作而发生的。有关避免死锁状态并从死锁状态中恢复的更多信息。
启用死锁检测(默认设置)并且发生死锁后,将InnoDB
检测条件并回滚其中一个事务(受害方)。如果使用innodb_deadlock_detect
配置选项禁用了死锁检测,则 在死锁的情况下InnoDB
依靠该 innodb_lock_wait_timeout
设置回滚事务。因此,即使您的应用程序逻辑正确,您仍然必须处理必须重试事务的情况。要查看InnoDB
用户事务中的最后一个死锁,请使用 SHOW ENGINE INNODB STATUS
命令。如果频繁出现死锁,说明事务结构或应用程序错误处理存在问题,请使用 innodb_print_all_deadlocks
启用此设置可将有关所有死锁的信息打印到 mysqld错误日志中。
InnoDB死锁示例
以下示例说明了锁定请求将导致死锁时如何发生错误。该示例涉及两个客户端A和B。
首先,客户端A创建一个包含一行的表,然后开始事务。在事务中,A通过S
在共享模式下选择行来获得对行的 锁定:
mysql> CREATE TABLE t (i INT) ENGINE = InnoDB;
Query OK, 0 rows affected (1.07 sec)
mysql> INSERT INTO t (i) VALUES(1);
Query OK, 1 row affected (0.09 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE i = 1 FOR SHARE;
------
| i |
------
| 1 |
------
接下来,客户端B开始事务并尝试从表中删除该行:
代码语言:javascript复制mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM t WHERE i = 1;
删除操作需要一个X
锁。无法授予该S
锁,因为它与客户端A持有的锁不兼容 ,因此该请求进入针对行和客户端B块的锁请求队列中。
最后,客户端A还尝试从表中删除该行:
代码语言:javascript复制mysql> DELETE FROM t WHERE i = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction
此处发生死锁是因为客户端A需要 X
锁才能删除该行。但是,不能授予该锁定请求,因为客户端B已经有一个X
锁定请求,并且正在等待客户端A释放其S
锁定。由于B事先要求锁,因此S
A持有的锁也不能 升级 X
为X
锁。结果, InnoDB
为其中一个客户端生成错误并释放其锁。客户端返回此错误:
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction
届时,可以授予对另一个客户端的锁定请求,并从表中删除该行。
死锁检测和回滚
当死锁检测被使能(缺省值), InnoDB
自动检测事务 的死锁和回退事务或交易打破僵局。InnoDB
尝试选择要回滚的小事务,其中事务的大小由插入,更新或删除的行数确定。
InnoDB
知道表锁if innodb_table_locks = 1
(默认)和 autocommit = 0
,它上面的MySQL层知道行级锁。否则, InnoDB
无法检测死锁,该死锁是由MySQL LOCK TABLES
语句设置的表锁或由存储引擎设置的锁InnoDB
所涉及的锁 。通过设置innodb_lock_wait_timeout
系统变量的值来解决这些情况 。
当InnoDB
进行交易的完整回滚,由交易设置的所有锁都被释放。但是,如果由于错误而仅回滚单个SQL语句,则可以保留该语句设置的某些锁。发生这种情况是因为InnoDB
以某种格式存储行锁,使得以后无法知道哪个语句设置了哪个锁。
如果SELECT
调用在事务中调用了存储的函数,而该函数内的一条语句失败,则该语句将回滚。此外,如果 ROLLBACK
在此之后执行,则整个事务都会回滚。
如果LATEST DETECTED DEADLOCK
第 InnoDB
监视器输出包括一条消息指出,“ 过深或长时间的搜寻锁表WAITS-FOR图中,我们将回滚下面的事务, ”这表明交易对所述等待名单已经达到了数限制为200。超过200个事务的等待列表将被视为死锁,并且尝试检查等待列表的事务将回滚。如果锁定线程必须查看等待列表上的事务所拥有的1,000,000个以上的锁,也可能发生相同的错误。
禁用死锁检测
在高并发系统上,当多个线程等待相同的锁时,死锁检测会导致速度变慢。有时,禁用死锁检测并在innodb_lock_wait_timeout
发生死锁时依靠设置进行事务回滚可能会更有效 。可以使用innodb_deadlock_detect
配置选项禁用死锁检测 。
如何最小化和处理死锁
死锁是事务数据库中的经典问题,但是除非死锁如此频繁以至于您根本无法运行某些事务,否则它们并不危险。通常,您必须编写应用程序,以便在由于死锁而使事务回滚时,它们始终准备重新发出事务。
InnoDB
使用自动行级锁定。即使在仅插入或删除单行的事务中,您也可能会陷入僵局。这是因为这些操作并不是真正的“ 原子 ”操作;它们会自动对插入或删除的行的(可能是多个)索引记录设置锁定。
您可以使用以下技术来处理死锁并减少发生死锁的可能性:
- 在任何时候,发出
SHOW ENGINE INNODB STATUS
命令以确定最近死锁的原因。这可以帮助您调整应用程序以避免死锁。 - 如果频繁出现死锁警告引起关注,请通过启用
innodb_print_all_deadlocks
配置选项来收集更广泛的调试信息 。有关每个死锁的信息,而不仅仅是最新的死锁,都记录在MySQL 错误日志中。完成调试后,请禁用此选项。 - 如果由于死锁而失败,请始终准备重新发出事务。死锁并不危险。请再试一次。
- 保持交易小巧且持续时间短,以使交易不易发生冲突。
- 进行一系列相关更改后立即提交事务,以减少冲突的发生。特别是,不要长时间关闭未提交事务的交互式 mysql会话。
- 如果您使用锁定读取(
SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
),请尝试使用较低的隔离级别,例如READ COMMITTED
。 - 修改事务中的多个表或同一表中的不同行集时,每次都要以一致的顺序执行这些操作。然后,事务形成定义良好的队列,并且不会死锁。例如,组织数据库操作到功能在应用程序中,或调用存储程序,而不是编码的多个相似序列
INSERT
,UPDATE
以及DELETE
在不同的地方语句。 - 将选择好的索引添加到表中。然后,您的查询需要扫描较少的索引记录,因此设置较少的锁。使用
EXPLAIN SELECT
以确定哪些索引MySQL认为最适合您的查询。 - 使用更少的锁定。如果你能负担得起,以允许
SELECT
从一个旧的快照返回数据,不要添加条款FOR UPDATE
或FOR SHARE
给它。在READ COMMITTED
这里使用隔离级别是件好事,因为同一事务中的每个一致性读取均从其自己的新快照读取。 - 如果没有其他帮助,请使用表级锁序列化事务。
LOCK TABLES
与事务表(例如InnoDB
表)一起使用的正确方法 是,以SET autocommit = 0
(notSTART TRANSACTION
)后跟来开始事务,直到明确提交事务后才LOCK TABLES
调用UNLOCK TABLES
。例如,如果您需要写表t1
和从表中读取数据t2
,则可以执行以下操作: SET autocommit=0; LOCK TABLES t1 WRITE, t2 READ, ...;... do something with tables t1 and t2 here ... COMMIT; UNLOCK TABLES; 表级锁可防止对表的并发更新,从而避免死锁,但代价是对繁忙系统的响应速度较慢。 - 序列化事务的另一种方法是创建一个仅包含一行的辅助“ 信号量 ”表。在访问其他表之前,让每个事务更新该行。这样,所有交易都以串行方式进行。请注意,
InnoDB
在这种情况下,即时死锁检测算法也适用,因为序列化锁是行级锁。对于MySQL表级锁,必须使用超时方法来解决死锁。
总结
本文出发点是想讲一下Mysql的事务的实现原理。
实现事务采取了哪些技术以及思想?
原子性:使用 undo log ,从而达到回滚
持久性:使用 redo log,从而达到故障后恢复
隔离性:使用锁以及MVCC,运用的优化思想有读写分离,读读并行,读写并行
一致性:通过回滚,以及恢复,和在并发环境下的隔离做到一致性。
死锁及事务调度相关技术