2022-06-14 21:53:56
老八股谈事务处理,核心是在探讨如何确保数据状态的一致性,即保证数据正确且不同数据间无矛盾。事务处理最早源自数据库系统,为实现数据库状态的一致性,需从原子性(Atomic)、隔离性(Isolation)、持久性(Durability)三方面努力,ACID中AID是达成一致性(Consistency)这一目的的手段。事务处理场景不仅限于数据库,还包括缓存、队列、对象存储等所有需确保数据正确性的系统。以下从传统本地数据库事务展开介绍:
传统本地数据库事务本地事务是最基础的事务,不涉及全局事务协调器,事务相关动作如开始、结束、提交、回滚、设置隔离级别依赖底层数据库支持,与XA、TCC、SAGA依靠应用程序代码实现不同。
“写入磁盘”并非原子操作,存在“正在写”的中间态。业务操作组合与数据库落盘行为在不同时空维度,因数据库崩溃恢复时机不同,会导致数据库难以做到业务逻辑自洽的数据一致性。
Commit Logging:强调事务提交要顺序写日志。具体步骤为:
将修改数据的全部信息(哪个页、磁盘、从什么修改成什么)记录到磁盘日志(顺序写)。
见到事务成功提交的“commit record”,数据库才会根据日志信息持久化落盘。
修改完成之后,在日志中加入一条“end record”,表示事务已经完成持久化。
事务日志的生成是事务提交的关键点,如阿里的Oceanbase。但所有对数据的真实修改都必须发生在事务提交之后,这对提升数据库性能不利。
Write Ahead Logging:强调持久化落盘可在事务提交之前,此时undolog出现。当变动数据写入磁盘前,写明修改了哪个数据,从什么改成什么,以便在数据回滚或崩溃恢复时,根据undolog日志对提前写入的数据进行擦除。其恢复过程分为三个阶段:
分析阶段:从事务日志中找出“不包含end record”的日志,作为待恢复的事务集合。
重做阶段:从待恢复事务集合中,找出“含有commit record”的事务日志,这一部分已经写完事务日志,可以重做。
回滚阶段:剩下的事务日志不包含commit record,说明事务日志还没有写完,这部分需要回滚,根据事务日志id到undolog中找到逻辑日志开始回滚。其中,redolog是物理日志,undolog是逻辑日志。
保证每个事务各自读写的数据相对独立,不会彼此影响。读操作(select)默认有共享锁(S锁),允许多个事务施加读锁,但阻止施加写锁,除非显式for update;写操作(update delete add)有排它锁(X锁),持有时不允许再施加读写锁;范围锁是某种查询条件内的范围数据被加排他锁。
施加锁和实际操作的时机很重要,持有锁的释放时机决定了隔离级别,具体如下:
串行读:加读写锁、范围锁,无并发能力。
可重复读:加读写锁且持有至事务结束,不加范围锁,有幻读问题,即两次相同的范围查询的数据集不一样。
读已提交:加读写锁,但读锁在查询之后即释放,不加范围锁,有不可重复读问题,即两次读取某数据可能不一样。
读未提交:加写锁,完全不加读锁,有幻读问题,即读到了另一个事务未提交的数据。
事务的隔离级别定义了各种锁在不同加锁时间上的组合应用,事务的隔离效果由事务的隔离级别和具体的操作语句(决定涉及的数据会用上哪些锁)共同决定。
针对“一个事务读,另一个事务写”的情况,采用无锁优化方案,即对数据的任何修改都不会直接覆盖之前的数据,而是产生新版本与老版本共存。
以mysql为例,每行记录有事务id、回滚指针两个隐藏字段,每次修改形成的旧版本(依赖undolog)也有这隐藏字段,通过回滚指针串起来。每个事务开启时创建自己的ReadView视图。
可重复度和读已提交依赖于MVCC来实现,其中可重复读是读取事务id小于等于当前事务id的最大版本的数据;读已提交是读取最新版本的数据。