概述

事务是数据库区别于文件系统的一个关键特性。

事务的分类

① 扁平事务,使用最频繁;

② 带有保存点的扁平事务;

③ 链事务,下一个事务将能够看到上一个事务的结果,只能恢复到最近一个的保存点;

④ 嵌套事务; 任何子事务都在顶层事务提交后才真正的提交;是一棵树状的结构;

只有叶子节点的事务才能访问数据库、发送消息、获取其他类型的资源;

⑤ 分布式事务;需要根据数据所在位置访问网络中的不同节点;保存点在事务内部是递增的;可以借助消息队列实现分布式事务。

相关的 SQL

// TODO 关联 Spring 提供的几个事务级别

1
SHOW VARIABLES LIKE 'ios%';

四大特性

  • 原子性(Atomic): 所有操作要么全部成功,要么全部失败
  • 一致性(Consistency): 数据从一个一致性状态转移到另一个一致性状态,一致指的是 数据的完整性约束 没有被破坏
  • **隔离性(Isolation)**: 并发执行事务时,一个事务应该不影响其他事务的执行
  • 持久性(Duration): 对 DB 的修改永久,恢复性能

事务的实现方式

实现的原理: InnoDB 中的 undo.log, redo.log 日志文件。

隔离性: 通过锁实现

原子性和持久性: 通过 redo 物理日志实现;

事务的一致性: 通过 undo log 实现;

redo log

blog

可通过参数调节控制 redo log 刷新到磁盘的策略;

log block: redo log 的块大小与磁盘扇区大小一样都是 512 字节,保证了原子性,不需要 doublewrite 技术;

为物理日志,恢复速度比逻辑日志快,是幂等的。

重做日志记录了事务的行为,可以很好地通过其对也进行 “重做” 操作

undo log

  • 帮助事务回滚;
  • 帮助实现 MVCC;
  • 是实现快照读的一种必要机制;
  • 存放在数据库内部的一个特殊字段上;

功能一: 是逻辑日志,将数据库逻辑地恢复到原来的样子;

功能二: 当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过 undo 读取之前的行版本信息,以此实现非锁定读取。

分类:

insert undo log

update undo log

delete 操作不直接删除记录,而只是将记录标记为已删除。

undo 信息的数据字典:

真正删除这行记录的操作其实被 “延时” 了,最终在 purage 操作中完成。

两阶段提交

第一阶段: 所有参与全局事务的节点都开始准备(PREPARE) ,告诉事务管理器准备好了;

第二阶段: 事务管理器告诉资源管理器质性 ROLLBACK 还是 COMMIT,分布式事务需要多一次的 PREPARE 操作,待收到所有节点的统一信息后,再进行 COMMIT 或是 ROLLBACK 操作。

事务相关的 SQL 语句

一条语句失败并抛出异常,不会导致先前已经执行的语句自动会馆,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- salfpoint 
ROLLBACK

-- 删除一个保存点
RELEASE SAVEPOINT t1;

-- 定义一个保存点
SAVEPOINT t2;

-- 回滚到某个保存点, 此时事务没有结束
ROLLBACK TO SAVEPOINT t2;

-- 设置级别
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL {...}

分布式事务

XA

XA 事务由一个或多个资源管理器、一个事务管理器以及一个应用程序组成。

Serializable 级别

长事务

执行时间较长的事务;

进行的优化:在 1 亿用户表中,这个操作被封装在一个事务中完成,通过为其转化成小批量的事务进行处理;

好处一: 便于回滚每完成一个小事务,将完成的结果存放在 batchcontext 表中,表示已完成批量事务的最大账号 ID。 在发生错误时,可以从这个已完成的最大事务 ID 继续进行批量的小事务,重新开启事务的代价就显得比较低。

好处二: 用户可以知道现在大概已经执行到了哪个阶段

1
UPDATE account SET account_total=account_total+1 + (1+interest_rate);

并发问题

更新丢失:

http://img.janhen.com/202103072221061551851135178.png

Dirty Read

读取到未提交的数据,之后回滚 ,修改成 READ UNCOMMITTED 隔离级别可以处理

1
SELECT @@tx_isolation;

二级封锁协议

在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。

可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。

.

不可重复读

session1 执行事务期间,另一个 session2 事务对session1 读取的数据修改并提交

将事务隔离级别升级为 REPEATABLE READ 即可处理该问题

幻读

侧重于删除和增加

Transaction A 读取与搜索条件相匹配的若干行, Transaction B 插入或删除行修改 Transaction A 的结果集。

  1. 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插⼊的数据的。因此,幻读在“当前读”下才会出现。
  2. 上⾯session B的修改结果,被session A之后的select语句⽤“当前读”看到,不能称为幻读。幻读仅专指“新插⼊的⾏”。

幻读有什么问题? ⾸先是语义上的。session A在T1时刻就声明了,“我要把所有d=5的⾏锁住,不准别的事务进⾏读写操作”。⽽实际上,这个语义被破坏了。 其次,是数据⼀致性的问题。 我们知道,锁的设计是为了保证数据的⼀致性。⽽这个⼀致性,不⽌是数据库内部数据状态在此刻的⼀致性,还包含了数据和⽇志在逻辑上的⼀致性。 原因很简单。在T3时刻,我们给所有⾏加锁的时候,id=1这⼀⾏还不存在,不存在也就加不上锁。 也就是说,即使把所有的记录都加上锁,还是阻⽌不了新插⼊的记录,这也是为什么“幻读”会被单独拿出来解决的原因。 到这⾥,其实我们刚说明完⽂章的标题 :幻读的定义和幻读有什么问题。

隔离级别

隔离得越严实,效率就会越低。

  1. READ UNCOMMITTED: ⼀个事务还没提交时,它做的变更就能被别的事务看到。
  2. READ COMMIT: ⼀个事务提交之后,它做的变更才会被其他事务看到。
  3. REPEATABLE READ: ⼀个事务执⾏过程中看到的数据,总是跟这个事务在启动时看到的数据是⼀致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可⻅的。
  4. SERIALIZABLE: 对于同⼀⾏记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前⼀个事务执⾏完成,才能继续执⾏。主要用于实现 InnoDB 的分布式事务。

InnoDB 在 REPEATABLE READ 级别下,使用 Next-Key Lock 锁算法,避免幻读的产生。

隔离级别与事务问题

事务隔离的实现

read view 算法

在 MySQL 中,实际上每条记录在更新的时候都会同时记录⼀条回滚操作。记录上的最新值,通过回滚操作,都可以得到前⼀ 个状态的值。 回滚⽇志什么时候删除呢?

在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要⽤到这些回滚⽇志时,回滚⽇志会被删除。

什么时候才不需要了呢?

当系统⾥没有⽐这个回滚⽇志更早的 read-view 的时候。

为何尽量不使用长事务?

⻓事务意味着系统⾥⾯会存在很⽼的事务视图。由于这些事务随时可能访问数据库⾥⾯的任何数据,所以这个事务提交之前,数据库⾥⾯它可能⽤到的回滚记录都必须保留,这就会导致⼤量占⽤存储空间。

除此之外,⻓事务还占⽤锁资源,可能会拖垮库。

其他

开启事务的方式

  • 显式启动事务语句: begin 或者 start transaction, 提交 commit,回滚 rollback;
  • set autocommit=0: 该命令会把这个线程的⾃动提交关掉。这样只要执⾏⼀个 select 语句,事务就启动,并不会⾃动提交,直到主动执⾏ commit 或 rollback 或断开连接。

建议使⽤⽅法⼀,如果考虑多⼀次交互问题,可以使⽤ commit work and chain 语法。在 autocommit=1 的情况下⽤ begin 显式启动事务,如果执⾏ commit 则提交事务。如果执⾏ commit work and chain 则提交事务并⾃动启动下⼀个事务。

快照

InnoDB⾥⾯每个事务有⼀个 唯⼀的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。 ⽽每⾏数据也都是有多个版本的。每次事务更新数据的时候,都会⽣成⼀个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。 也就是说,数据表中的⼀⾏记录,其实可能有多个版本(row),每个版本有⾃⼰的row trx_id。

InnoDB利⽤了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能⼒。

更新数据都是先读后写的,⽽这个读,只能读当前的值,称为“当前读”(currentread)。

InnoDB的⾏数据有多个版本,每个数据版本有⾃⼰的row trx_id,每个事务或者语句有⾃⼰的⼀致性视图。普通查询语句是⼀致性读,⼀致性读会根据row trx_id和⼀致性视图确定数据版本的可⻅性。

  • 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;

  • 对于读提交,查询只承认在语句启动前就已经提交完成的数据;

    ⽽当前读,总是读取已经提交完成的最新版本。你也可以想⼀下,为什么表结构不⽀持“可重复读”?这是因为表结构没有对应的⾏数据,也没有row trx_id,因此只能遵循当前读的逻辑。

RR 下解决幻读

表象:快照读(非阻塞读)–伪MVCC 内在:是因为事务对数据加了next-key锁(行锁+gap锁) -gap锁会用在非唯一索引或者不走索引的当前读中

RC、RR 下的 InnoDB 的非阻塞读实现

RR 下可能读取到老的版本

RR 创建快照的时机决定了事务的版本

1
2
3
4
5
6
session1:
UPDATE ... -- 1

session2:
SELECT -- 3
SELECT ... LOCK IN SHARE MODE; -- 2
  1. 数据行中三个行隐藏参数:

DB_TRX_ID: 最近一次对本行数据进行修改的数据 ID

DB_ROW_PTR: 回滚指针, 指向 undo 日志

DB_ROW_ID: 无主件时隐式的 ID

(2) undo 日志: 老版本

针对 Insert undo log,

针对 update undo log

(3) read view: 可见性算法

http://img.janhen.com/202103072224031551854704429.png

http://img.janhen.com/202103072224191551854739908.png

MVCC: 读不加锁,读写不冲突,读多写少

伪 MVCC: 无法多版本共存

RR 避免 幻读

产⽣幻读的原因是,⾏锁只能锁住⾏,但是新插⼊记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引⼊新的锁,也就是间隙锁(Gap Lock)。

但是间隙锁不⼀样,跟间隙锁存在冲突关系的,是“往这个间隙中插⼊⼀个记录”这个操作。间隙锁之间都不存在冲突关系。

间隙锁和 next-key lock 的引⼊,帮我们解决了幻读的问题,但同时也带来了⼀些“困扰”。

间隙锁的引⼊,可能会导致同样的语句锁住更⼤的范围,这其实是影响了并发度的。

⾏锁确实⽐较直观,判断规则也相对简单,间隙锁的引⼊会影响系统的并发度,也增加了锁分析的复杂度,但也有章可循

next-key 锁

行锁:

Gap 锁: 锁定一个范围,不包含当前

() GAP 锁出现的时机

出现的场景: WHERE + INDEX

where 条件全部命中,不会加 Gap Lock, 只会加 Record Lock

where 条件部分命中,或全部不命中,加 Gap Lock;

Gap 锁会用在非唯一索引或者不走 index 的当前读中:

  • 非唯一索引
  • 不走索引的当前读,尽量避免

MVCC

Multiversion concurrency control 多版本并发控制。

并发访问(读或者写)数据库时,对正在事务内处理的数据做多版本的管理,用来避免由于写操作的堵塞,而引发读操作失败的并发问题。

Ref

  • 《MySQL技术内幕:InnoDB存储引擎(第二版)》姜承尧