事务的目的
要么全都执行,要么全都不执行。
事务的分类
从理论角度,将事务分为:
- 扁平事务(flat transactions)
- 带有保存点的扁平事务(flat transactions with savepoints)
- 链事务(chained transactions)
- 嵌套事务 (nested transactions)
- 分布式事务(distributed transaction)
扁平事务
是事务类型中最简单的一种,但在实际生产环境中,这可能是使用最为频繁的事务。它由 BRGIN WORK
开始,COMMIT WORK
或者 ROLLBACK WORK
结束。处于之间的操作要么都执行要么都不执行。
带有保存点的扁平事务
指支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。通过在事务内部使用 SAVEPOINT 节点名字
来建立一个节点,然后可以通过 ROLLBACK TO SAVEPOINT 节点名称
来回滚到某节点。
- 对应扁平事务来说,其隐式地设置了一个保存点(事务开始时的状态
- 保存点用 SAVE WORK 函数来建立,通知系统记录当前的处理状态
- 保存点在事务内部是单调递增的,ROLLBACK 不影响保存点的计数
当出现问题时,可以选择回滚到最近一个保存点或者更早的保存点。例如通过 ROLLBACK TO SAVEPOINT 节点名称
,回退到节点名称这个事务状态。此时事务不是正常的回滚,还是活跃的(事务没有结束,此时执行了节点名称前的所有操作)。如果确认节点前的操作无误,那么就可以使用 COMMIT
提交。或者使用 ROLLBACK
回滚掉整个事务。
链事务
可视为保存点模式的一种变种。
- 带有保存点的扁平事务中的保存点时易失的,而非持久的,系统崩溃时,索引保存点都将消失
- 意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保持点继续执行
- 链事务的思想
- 提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式传给下一个开始的事务
- 提交事务操作和开始下一个事务操作将合并为一个原子操作
- 与带有保存点的扁平事务不同之处
- 带有保存点的扁平事务能回滚到任务正确的保存点,而链事务中的回滚仅限于当前事务
- 即只能恢复到最近的一个的保存点
- 链事务在执行 COMMIT 后释放了当前事务所持有的锁,而前者不影响迄今为止所持有的锁
- 带有保存点的扁平事务能回滚到任务正确的保存点,而链事务中的回滚仅限于当前事务
嵌套事务
是一个层次结构框架,又一个顶层事务控制各个层次的事务(子事务,控制每一个局部的变换
- 嵌套事务的层次结构
- 嵌套事务的定义
- 嵌套事务是由若干事务组成的一棵树,子树既可以是嵌套事务,也可以说是扁平事务
- 处在叶节点的事务是扁平事务(子事务从根到叶节点的距离可以是不同的
- 处于根节点的事务称为顶层事务,其他事务称为子事务(事务的前驱称为父事务,下一层称为儿子事务
- 子事务既可以提交也可以回滚(但它的提交操作并不马上生效,除非其父事务已经提交
- 树中的任意一个事务的回滚会引起它的所有子事务一同回滚(子事务仅保留 A、C、I 特性,不具备 D
- 在 Moss 的理论中,实际的工作是交由叶子节点来完成的,高层的事务仅负责逻辑控制
- 即只有叶子节点的事务才能访问数据库、发送消息、获取其他类型的资源
- 高层事务绝对何时调用相关的子任务
分布式事务
通常是一个在分布式环境下运行的扁平事务,需要根据数据所在位置访问网络中的不同节点
- 对于分布式事务,其同样需要满足 ACID 特性,要么都发生,要么都失效
事务控制语句
在 MySQL 命令行的默认设置下,事务都是自动提交(auto commit)的,即执行完语句后就会马上执行 COMMIT
操作,可以通过执行 select @@autocommit;
产看当前的事务是否为自动提交,值为 1 代表是自动提交,值为 0 则代表不是。
可以执行 set @@autocommit=0
或者 set autocommit=0
关掉自动提交事务。这时在窗口一执行 insert
操作后,再打开一个窗口二执行 select
并不会看见窗口一插入的内容,此时等待窗口一 commit
后窗口二就能查询到了。如果是自动提交的话,那么只要窗口一插入数据,窗口二就能查询到。
因此需要显式的开启事务需要使用命令 BEGIN
或者 START TRANSACTION
。输入这两个命令后,再这之后与 ROLLBACK
或 COMMIT
之前的操作就属于事务操作了。
相关语句
BEGIN
|START TRANSACTION
显式的开启一个事务
COMMIT
|COMMIT WORK
都用来提交事务,不同的在于 COMMIT WORK
用来控制事务提交后的行为是 CHAIN 还是 RELEASE 的。如果是 CHAIN 方式,那么事务就变成了链事务。可以通过参数 completion_type 来进行控制,默认为 0 表示没有任何操作,这种时候 COMMIT
和 COMMIT WORK
是完全等价的。为 1 时 COMMIT WORK
等同于 COMMIT AND CHAIN
,表示马上开启一个相同隔离级别的事务。为 2 时 COMMIT WORK
等同于 COMMIT AND RELEASE
,当事务提交后自动与服务器断开连接。
ROLLBACK
|ROLLBACK WORK
都用来回滚事务。结束正在执行的事务,并撤销所有还没提交的修改。
SAVEPOINT identifier
SAVEPOINE 允许在事务当中创建一个保存点,一个事务中可以创建多个保存点。
RELEASE SAVEPOINT identifier
删除事务的一个保存点,当没有一个该保存点时抛出异常。
ROLLBACK TO [SAVEPOINT] identifier
与 SAVEPOINT identifier
配合使用,用来将事务回滚到某个 identifier 的状态。
SET TRANSACTION
设置事务的隔离级别,InnoDB 中提供了 READ UNCOMMITTED,READ COMMITTED,REPEATABLE READ,SERIALIZABLE 四种隔离级别。InnoDB 默认为 REPEATABLE READ。
事务的隔离级别
事务隔离级别:
- 读未提交(read uncommitted)
- 读提交(read committed)
- 可重复读(repeatable read)
- 串行化(serializable )
读未提交(读取未提交内容)
读未提交,任何操作都不加锁,所以能读到其他事务修改但未提交的数据行,也称之为脏读(Dirty Read)。
读提交(读取提交内容,解决了出现脏读的问题)
读操作不加锁,写操作加锁。读被加锁的数据时,读事务每次都读 undo log 中的最近版本,因此可能对同一数据读到不同的版本(不可重复读),但能保证每次都读到最新的数据(事务提交之后的,不可重复读,两次读不一致),但是不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果。
可重复读(可重读,解决不可重复读)
第一次读数据的时候就将数据加行锁(共享锁),使其他事务不能修改当前数据,即可实现可重复读。但是不能锁住 insert 进来的新的数据,当前事务读取或者修改的同时,另一个事务还是可以 insert 提交,造成幻读;
(注:mysql 的可重复读的隔离级别解决了 “不可重复读” 和 “幻读” 2 个问题,因为使用了间隙锁。)
串行化(可串行化,解决幻读问题)
InnoDB 锁表,读锁和写锁阻塞,强制事务串行执行,解决了幻读的问题;
隔离级别与问题对应表如下:
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
- SQL 和 SQL2 标准的默认事务隔离级别是 SERIALIZABLE
- InnoDB 存储引擎默认支持的隔离级别是 REPEATABLE READ
- 但与标准 SQL 不同的是:通过使用 Next-Key Lock 锁的算法来避免幻读的产生
- 即 InnoDB 在默认的默认隔离级别下已经能完全保证事务的隔离性要求(达到 SQL 标准的 SERIALIZABLE 级别)
- 在 SERIALIZABLE 的事务隔离级别,InnoDB 会对每个 SELECT 语句后自动加上 LOCK IN SHARE MODE,即共享读锁
- 因此在此隔离级别下,读占用了锁,对一致性的非锁定读不再予以支持
- 此隔离级别复核数据库理论上的要求,即事务是 well-formed 的,并且是 two-phrased
- SERIALIZABLE 的隔离级别主要用于 InnoDB 存储引擎的分布式事务
- 在 READ COMMITTED 的事务隔离级别下,除唯一性约束检查及外键约束检查需要 gap lock,其他情况都不会使用
事务的锁
-
根据读写,分为共享锁 S 和排它锁 X
共享锁:即读加锁,不能写并且可并行读
排它锁:写加锁,其他读写都阻塞 -
根据加锁范围分为表锁、行锁、间隙锁
表锁:锁 整个表,性能开销最大,其他的读写都要挂起
行锁:锁整个行,以默认隔离级别为例:如果是读,那么会上共享锁,不允许写,如果是写,那么改行其他事务无论读写都得阻塞
间隙锁:间隙锁分为两种,一种是不包含记录间隙锁(GAP),一种是包含记录间隙锁(Next-Key Lock: Gap Lock+Record Lock),比如对于默认隔离级别的 innoDB 下, 比如表 A 上的 id 字段有索引, 并且 id 有 3,8,12,20 这几个值,那么该索引可能被上的包含记录间隙锁区间为:(负无穷,3)、[3,8)、[8,12)、[12,20)、[20,正无穷)
1.当事务 T1 锁定了 [8,12),[12,20)这 2 个区间时,当插入 15 时,上面的区间变成:
[8,12)、[12,15)、[15,20)
2.但查询索引含有唯一索引时,Next-Key Lock 降级为 Record Lock,仅锁住索引本身的数据行
3.好,现在表 A 的 id 值变成了: 3,8,12,15,20
4.如果执行下列语句:
select * from A where id>16 for update.
InnoDB 会对(16,正无穷) 加锁,
5.但在 read committed 的事务隔离级别下,因为采用 Record Lock,只会锁定 20 这个值
6.如果在此时另外一个事务 T2,插入了 22 这个值,此时, read committed 隔离级别下就会产生"幻读"的问题
7.但在 InnoDB 默认存储引擎下的 Next-key Lock 模式下,22 是插入是会被阻塞的,直到事务 T1 提交后,释放 X 锁,才能提交 22 这值.这样,InnoDB 就这样解决了幻读的问题
本文参考
极客时间·MySQL 实战 45 讲
MYSQL 事务的实现
《MySQL 技术内幕 :SQL 编程》 · 姜承尧
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于