InnoDB 事务与分布式事务中一些关键问题


本文地址:http://www.6aiq.com/article/1565459547589
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出

事务特性 ?

bd2f24ad587c414e8d451f96e49b5323.png

原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性: 执行事务前后,数据保持一致;
隔离性: 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;
持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

事务隔离级别?

SQL 标准定义了四个隔离级别:

READ-UNCOMMITTED(读取未提交): 
最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

REPEATABLE-READ(可重读): 一个事务内多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

SERIALIZABLE(可串行化): 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)

2982a5d05a514b1387630a8d00e8e408.png

注: 与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统 (如 SQL Server) 是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)  已经可以完全保证事务的隔离性要求,即达到了 SQL 标准的SERIALIZABLE(可串行化) 隔离级别。

并发事务的问题

不可重复度和幻读区别:

不可重复读的重点是修改,幻读的重点在于新增或者删除。

InnoDB 的七种锁

总的来说,InnoDB 共有七种类型的锁:

  • 共享 / 排它锁 (Shared and Exclusive Locks)
  • 意向锁 (Intention Locks)
  • 记录锁 (Record Locks)
  • 间隙锁 (Gap Locks)
  • 临键锁 (Next-key Locks)
  • 插入意向锁 (Insert Intention Locks)
  • 自增锁 (Auto-inc Locks)

并发控制的场景在于对临界资源的访问,为了并发情况下保证数据一致性一般采取的手段:

  • 加锁
  • 多版本并发控制(MVCC)

如果对并发中任何操作都加锁,完全串行,吞吐量严重受限,不过讨论一些并发的场景,对于 临界资源的
读读操作 可以在读的时候加上共享锁,可以读读并行;

读写操作 可以使用MVCC,做到读写并行;

写写操作 使用 排他锁

redo undo

理解这两种日志,就能理解事务实现MVCC 实现

每次数据库 commit 操作后为了保证事务特性中的持久性,应该将提交的事务数据刷到磁盘,频繁的磁盘 IO操作性能会很差并且还是磁盘随机写,性能更差。所以其实 commit 操作只是临时把数据刷到内存 buffer(Page Cache)中,并将 page cache 的内存结构的物理改变以二进制写入 redo log,此时变成了磁盘顺序写,其 buffer 数据定期刷盘。即使某一刻数据库宕机,重启后,已提交的且在宕机前尚未刷盘事务数据会从 redo log 重做回来。
其实这里面涉及的 存储设计 非常通用,消息中间件为了确保消息不丢失一般都先落盘再投递,落盘尽量转化为磁盘顺序 IO,并且一般都批量异步刷盘。以 kafka 和 rocketmq 存储设计举例
Kafka 会在 Broker 上为每一个 topic 创建一个独立的 partiton 文件,Broker 接受到消息后,会按主题在对应的 partition 文件中顺序的追加消息内容。而 RocketMQ 则会创建一个 commitlog 的文件来保存分片上所有主题的消息。
Broker 接收到任意主题的消息后,都会将消息的 topic 信息,消息大小,校验和等信息以及消息体的内容顺序追加到 Commitlog 文件中,Commitlog 文件一般为固定大小,当前文件达到限定大小时,会创建一个新的文件,文件以起始便宜位置命名。
同时,Broker 会为每一个主题维护各自的 ConsumerQueue 文件,文件中记录了该主题消息的索引,包括在 Commitlog 中的偏移位置,消息大小及校验和,以便于在消费时快速的定位到消息位置。ConsumerQueue 的维护是异步进行的,不影响消息生产的主流程,即使 ConsumerQueue 没有及时更新的 情况下,服务异常终止,下次启动时也可以根据 Commitlog 文件中的内容对 ConsumerQueue 进行恢复。

数据库事务未提交时,会将事务内的修改前的记录在 undo 日志里面,用于事务执行失败的回滚,比如对于insert 操作,undo 日志记录新数据的 PK(ROW_ID),回滚时直接删除;
对于delete/update 操作,undo 日志记录旧数据 row,回滚时直接恢复;

所以说 undo 日志保证了事务的原子性

InnoDB 的内核,会对所有 row 数据增加三个内部属性:

(1)DB_TRX_ID,6 字节,记录每一行最近一次修改它的事务 ID;

(2)DB_ROLL_PTR,7 字节,记录指向回滚段 undo 日志的指针;

(3)DB_ROW_ID,6 字节,单调递增的行 ID;

所以说 MVCC 中旧版本数据都存在于 undo 日志中,当对临界资源有写操作时,读操作可以不加锁的读取旧版本数据,进而提高读写性能。



MVCC 在 读提交 和 可重复读 隔离级别下的差异

在 mvcc 中读操作

  • 总是能读到本事务内操作
  • RC 下,快照读总是能读到最新的行数据快照,当然,必须是已提交事务写入的
  • RR 下,某个事务首次 read 记录的时间为 T,未来不会读取到 T 时间之后已提交事务写入的记录,以保证连续相同的 read 读到相同的结果集

事务隔离级别的实现

InnoDB 使用不同的锁策略来实现不同的隔离级别。
** 读未提交 **

该场景下 select 操作不加锁,可能出现脏读
串行化

这种事务的隔离级别下,所有 select 语句都会被隐式的转化为 select … in share mode. 这可能导致,如果有未提交的事务正在修改某些行,所有读取这些行的 select 都会被阻塞住。这是一致性最好的,但并发性最差的隔离级别。
可重复读

  • 普通的 select使用快照读 (snapshot read),这是一种不加锁的一致性读 (Consistent Nonlocking Read),底层使用 MVCC 来实现

  • 加锁的 select(select … in share mode / select … for update), update, delete 等语句,它们的锁,依赖于它们是否在唯一索引 (unique index) 上使用了唯一的查询条件(unique search condition),或者范围查询条件(range-type search condition):

    • 在唯一索引上使用唯一的查询条件,会使用记录锁 (record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock) 与临键锁(next-key lock)

    • 范围查询条件,会使用间隙锁与临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录,以及避免不可重复的读

** 读提交 **

  • 普通读是快照读;

  • 加锁的 select, update, delete 等语句,除了在外键约束检查 (foreign-key constraint checking) 以及重复键检查 (duplicate-key checking) 时会封锁区间,其他时刻都只使用记录锁;

此时,其他事务的插入依然可以执行,就可能导致,读取到幻影记录。

分布式事务方案以及分布式 MVCC 机制

CAP & BASE 理论

BASE 理论是指 BA(Basic Availability,基本业务可用性);S(Soft state,柔性状态);E(Eventual consistency,最终一致性)。该理论认为为了可用性、性能与降级服务的需要,可以适当降低一点一致性的要求,即 “基本可用,最终一致”。

TCC 与 XA 比较

fad78d6c36464ef1a3ce83f48df359d5.png
▲XA 模型的并发事务
a22977add9bc43c3aa06c61001447dda.png

▲TCC 模型的并发事务

从上面的对比可以发现,TCC 模型相比 XA 模型进一步减少了资源锁的持有时间。XA 模型下,在 Prepare 阶段是不会把事务 1 所持有的锁资源释放掉的,如果事务 2 和事务 1 争抢同一个资源,事务 2 必须等事务 1 结束之后才能使用该资源。

而在 TCC 里,因为事务 1 在 Try 阶段已经提交了,那么事务 2 可以获得互斥资源,不必再等待事务 1 的 Confirm 或 Cancel 阶段执行完。也就是说,事务 2 的 Try 阶段可以和事务 1 的 Confirm 或 Cancel 阶段并行执行,从而获得了一个比较大的并发性能提升。

TCC 第二阶段提交异步化

acf75ef6ebf044849d63feb6252bbf8a.png

这就是 TCC 分布式事务模型的二阶段异步化功能,各从业务服务的第一阶段执行成功以后,主业务服务就可以提交完成,框架会保证正确记录事务状态,然后再由框架异步的执行各从业务服务的第二阶段,从而比较完整的诠释最终一致性。

分布式 MVCC

数据库内部解决写和非加锁读的冲突是通过 MVCC 机制来实现的。假如说最新的数据块在更新的同时,你的读是可以读正在更新的数据块的上一个快照。但是在分布式架构下,单机 MVCC 机制并不能满足数据实时性一致性要求。

转账业务场景,A 账户给 B 账务转账 10 块钱。但是 A 账户和 B 账户分别在两个数据库分片 DB1 和 DB2 上。其操作执行过程如下所以:
0deff1eb999d47949f7c168251ea051f.png

如上图所示,DB1 的本地子事务已经提交完毕,但是 DB2 的本地子事务还没提交,这个时候只能读到 DB1 上子事务执行的内容,读不到 DB2 上的子事务。也就是说,虽然在单个 DB 上的本地事务是实时一致的,但是从全局来看,一个全局事务执行过程的中间状态被观察到了,全局实时一致性就被破坏了。

  但是原生的 XA 协议没有规定快照读这个概念,也就没有定义怎么实现全局实时一致性。最简单的做法就是使用串行化的隔离级别,即使是快照读也需要转换为加锁读,从而来保证分布式事务的实时一致性。

  当然,由于串行化隔离级别的性能较差,很多分布式数据库都自己实现了分布式 MVCC 机制来提供全局的实时一致性读。一个基本思路是用一个集中式或者逻辑上单调递增的东西来控制生成全局快照 (Snapshot),每个事务或者每条 SQL 执行时都去获取一次,从而实现不同隔离级别下的全局一致性,如下图所示:
9a2f28c1ffaa48c88896319cdd8c4597.png

在 DB1 的本地子事务已经提交完毕,DB2 的本地子事务还没提交的中间状态,其他并发事务只能看到该事务执行之前的快照。


本文地址:http://www.6aiq.com/article/1565459547589
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出