本篇文档介绍 MiniOB 中的事务模块是如何工作的。

# 背景
事务是数据库中非常基础的一个模块，也是非常核心的功能。事务有一些基本的概念，叫做ACID，分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。如果你对事务的基本概念不太清楚，建议先学习了解事务的基本概念，比如学习[事务处理](../lectures/lecture-6.md)章节，或者在网上搜索更多资料。

# MiniOB 中的事务
## 实现简介
MiniOB 作为一个帮助学习数据库的代码，为了使其学习起来更简单，实现了两种类型的事务。一个叫做Vacuous，另一个叫做MVCC，可以在启动observer时，选择特定的事务模块。

**Vacuous（真空）**

顾名思义，这个事务模块，将不会做任何事务相关的处理，这保留了原始的简单性，有利于学习其它模块时，简化调试。

**MVCC**

多版本并发控制，是一种常见的事务实现。著名的MySQL数据库也支持MVCC，[OceanBase](https://github.com/oceanbase/oceanbase)也实现了此机制。

简单来说，MVCC 会在修改数据——MiniOB当前支持插入和删除——时，不会直接在现有的数据上修改，而是创建一个新的行记录，将旧数据复制出来，在新数据上做修改。并将新旧数据使用链表的方式串联起来。每个数据都会有自己的版本号（或者称为时间戳），而版本号通常使用单调递增的数字表示，每个事务根据自己的版本号与数据的版本号，来判断当前能够访问哪个版本的数据。由此可见，MVCC 一个优点就是可以提高只读事务的并发度，它不与其它的写事务产生冲突，因为它访问旧版本的数据就可以了。

> NOTE: 不同的数据库，会有不同的实现方式。对MVCC感兴趣的同学，可以阅读一些相关的论文。

## 如何运行与测试
当前MiniOB支持两种类型的事务模型，并且默认情况下是Vacuous，即不开启事务特性。

**测试MVCC**

编译时增加选项 `-DCONCURRENCY=ON`:
```bash
cmake -DCONCURRENCY=ON ..
```
然后在build目录执行 make。编译完成后启动 observer 服务端进程。

> 也可以使用 bash build.sh -DCONCURRENCY=ON 来编译

可以在启动observer时，增加 `-t mvcc` 选项来开启MVCC，假设当前目录是build(或build_debug之类)：

```bash
./bin/observer -f ../etc/observer.ini -s miniob.sock -t mvcc
```

>-f 是配置文件，-s 指使用unix socket，-t 指定使用的事务模型

启动observer后，可以使用obclient连接observer：

```bash
./bin/obclient -s miniob.sock
```

可以开启多个客户端。在命令行界面执行 `begin` 可以开启事务，执行 `commit` 提交事务，`rollback` 回滚事务。

## 更多的实现原理

事务代码位于 `src/observer/storage/trx` 目录下，代码很少。

### 事务模型选择
trx.h 文件中有一个抽象类 `TrxKit`，它可以根据运行时参数传入的名字来创建对应的 `VacuousTrxKit` 和 `MvccTrxKit`。这两个类可以创建相应的事务对象，并且按照需要，初始化行数据中事务需要的额外表字段。当前 Vacuous 什么字段都不需要，而MVCC会额外使用一些表字段。

### 事务接口
不同的事务模型，使用了一些统一的接口，这些接口定义在 `Trx` 中。

**事务本身相关的操作**

- start_if_need。开启一个事务。在SQL请求处理过程中，通常需要开启一个事务；
- commit。提交一个事务；
- rollback。回滚一个事务。

**行数据相关的操作**

- insert_record。插入一行数据。事务可能需要对记录做一些修改，然后调用table的插入记录接口。提交之前插入的记录通常对其它事务不可见；
- delete_record。删除一行数据。与插入记录类似，也会对记录做一些修改，对MVCC来说，并不是真正的将其删除，而是让他对其它事务不可见（提交后）；
- visit_record。访问一行数据。当遍历记录，访问某条数据时，需要由事务来判断一下，这条数据是否对当前事务可见，或者事务有访问冲突。

### MVCC 相关实现
**版本号与可见性**

与常见的MVCC实现方案相似，这里也使用单调递增的数字来作为版本号。并且在表上增加两个额外的字段来表示这条记录有效的版本范围。两个版本字段是`begin_xid`和`end_xid`。每个事务在开始时，就会生成一个自己的版本号，当访问某条记录时，判断自己的版本号是否在该条记录的版本号的范围内，如果在，就是可见的，否则就不可见。

> 有些文章或者某些数据库实现中，使用"时间戳"来表示版本号。如果可以保证时间戳也是单调递增的，那这个时间戳确实更好可以作为版本号，并且在分布式系统中，比单纯的单调递增数字更好用。

**记录版本号与事务版本号**

行数据上的版本号，是事务设置的，这个版本号也是事务的版本号。一个写事务，通常会有两个版本号，在启动时，会生成一个版本号，用来在运行时做数据的可见性判断。在提交时，会再生成一个版本号，这个版本号是最终设置在记录上的。

```cpp
trx start:
  trx_id = next_id()
  read record: is_visible(trx_id, record_begin_xid, record_end_xid)

trx commit:
  commit_id = next_id()
  foreach updated record: update record begin/end xid with commit_id
```

>Q:为什么一定要在提交时生成一个新的版本号？只用该事务之前的版本号不行吗？会有什么问题？

**版本号与插入删除**

新插入的记录，在提交后，它的版本号是 `begin_xid` = 事务提交版本号，`end_xid` = 无穷大。表示此数据从当前事务开始生效，对此后所有的新事务都可见。
而删除相反，`begin_xid` 保持不变，而 `end_xid` 变成了当前事务提交的版本号。表示这条数据对当前事务之后的新事务，就不可见了。
记录还有一个中间状态，就是事务刚插入或者删除，但是还没有提交时，这里的修改对其它事务应该都是不可见的。比如新插入一条数据，只有当前事务可见，而新删除的数据，只有当前事务不可见。需要使用一种特殊的方法来标记，当然也是在版本号上做动作。对插入的数据，`begin_xid` 改为 (-当前事务版本号)(负数)，删除记录将`end_xid`改为 (-当前事务版本号)。在做可见性判断时，对负版本号做特殊处理即可。

假设某个事务运行时trx id是 Ta，提交时是 Tc

| operation | trx state | begin xid | end xid |
| --------- | --------- | --------- | ------- |
| inserted  | committed | Tc | +∞ |
| deleted   | committed | some trx_id | Tc |
| insert    | uncommit  | -Ta | +∞ |
| delete    | uncommit  | some trx_id | -Ta |

**并发冲突处理**

MVCC很好的处理了只读事务与写事务的并发，只读事务可以在其它事务修改了某个记录后，访问它的旧版本。但是写事务与写事务之间，依然是有冲突的。这里解决的方法简单粗暴，就是当一个写事务想要修改某个记录时，如果看到有另一个事务也在修改，就直接回滚。如何判断其它事务在修改？判断`begin_xid`或`end_xid`是否为负数就可以。

**隔离级别**

我们通常在聊事务隔离级别时，都会说脏读(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和可串行化(Serializable)，说这些时也通常都会提到大名鼎鼎的MySQL。但实际上隔离级别不止是这4种。
不过这里也没有对隔离级别做特殊的处理，让它顺其自然。

>Q: 通过上面的描述，你知道这里的MVCC是什么隔离级别吗？


## 遗留问题和扩展
当前的MVCC是一个简化版本，还有一些功能没有实现，并且还有一些已知BUG。同时还可以扩展更多的事务模型。

- 事务提交时，对外原子可见

  当前事务在提交时，会逐个修改之前修改过的行数据，调整版本号。这造成的问题是，在某个时刻，有些行数据的版本号已经修改了，有些还没有。那可能会存在一个事务，能够看到已经修改完成版本号的行，但是看不到未修改的行。
  比如事务A，插入了3条数据，在提交的时候，逐个修改版本号，某个情况下可能会存在下面的场景(假设A的事务ID是90，commit id是100)：

  | record | begin xid | end xid | data |
  | ------ | --------- | ------- | ---- |
  | R1     | 100       | +∞      | ...  |
  | R2     | 100       | +∞      | ...  |
  | R3     | -90       | +∞      | ...  |

  此时有一个新的事务，假设事务号是 110，那么它可以看到记录R1和R2，但是看不到R3，因为R3从记录状态来看，还没有提交。

- 垃圾回收

  随着数据库进程的运行，不断有事务更新数据，不断产生新版本的数据，会占用越来越多的资源。此时需要一种机制，来回收对任何事务都不再可见的数据，这称为垃圾回收。垃圾回收也是一个很有趣的话题，实现方式有很多种。最常见的是，开启一个或多个后台线程，定期的扫描所有的行数据，检查它们的版本。如果某个数据对当前所有活跃事务都不可见，那就认为此条数据是垃圾，可以回收掉。当然，这种回收方法最简单，也是最低效的，同学们如何优化或者实现新的回收方法。

- 多版本存储

  当前miniob仅实现了插入和删除，并不支持更新操作。而插入和删除最多会存在两个版本的数据，从实现上来看，最多需要一条数据就可以。这大大简化了MVCC的实现。但是也因此没有涉及到MVCC非常核心的多版本数据存储问题。如何合理的存储多个版本的数据，对数据库的性能影响也是巨大的。比如多个版本数据串联时，使用从新到旧，还是从旧到新。两种方式都有合理性，适用于不同的场景。另外还有，多版本的数据存储在哪里？内存还是磁盘，是与原有的数据放在同一个存储空间，还是规划单独的空间，各有什么优缺点，都适用于什么场景。还有，更新数据时，复制整行数据，还是仅记录更新的字段。各有什么优缺点，各适用于什么场景。

- 持久化事务

  持久性是事务的基本要素之一，是指事务修改后的数据，在数据库重启或者出现异常时，能够从磁盘中将数据恢复出来。除了将修改的数据直接写入到磁盘，还有一个常用的技术手段是WAL，比如Redo日志和Undo日志。那么什么情况下使用Redo，什么时候使用Undo，以及如果只使用Redo或者只使用Undo会有什么问题。另外还有如何存储这些日志，B+树的持久化怎么处理等。有兴趣的同学可以再了解一下 `Steal/No-Steal` 和 `Force/No-Force` 的概念。

- MVCC的并发控制
  
  如前文描述，这里的写事务并发冲突处理过于简单粗暴，以至于可以避免的冲突却没有避免。

- 基于锁的并发控制

  MVCC的并发控制通常认为是乐观事务，就是我们认为此系统中事务之间大部分情况下不会存在冲突。但是在OLTP系统中，访问冲突可能是非常频繁发生的，这时候使用悲观事务，效率会更高一点。常见的悲观事务实现方法就是基于锁来实现。假设使用记录锁(行锁)来实现并发，在读数据时加读锁，写时加写锁，也可以实现多种级别的隔离机制。另外，还可以将使用基于锁的机制与MVCC结合起来，实现更好的并发控制。

  顺便提一下一个常见的问题，就是在使用行锁时，如何与页面锁(latch)协调？
  大家都知道，latch 都是短锁，在latch保护范围内，都不应该出现长期等待的事情。另外，latch没有死锁检测，不处理锁冲突。而行锁是一种长锁，需要做锁冲突处理，可能需要等待。那在拿着某个latch时，需要等待行锁时，如何处理？

  > 这是很多做了CMU 15445课程的同学没有考虑的问题，15445 课程中将 `Lock Manager` 模块单独拎出来让同学们做练习。但是当行锁与latch同时工作时，它的复杂度将提升好几个量级。


# 进一步学习

事务是非常复杂非常有趣的，相关的话题也已经有非常多的研究。如果对事务感兴趣，可以在了解数据库整体实现基础之上，深入研究事务的实现原理。
这里推荐一些介绍数据库入门的书籍：
- 《数据库系统概念》该书是数据库领域的经典教材之一，涵盖了数据库基本概念、关系型数据库设计、事务处理、并发控制等方面的内容，也可以着重阅读事务相关的内容
- 《数据库系统实现》：该书是一本数据库系统实现方面的教材，讲解了数据库系统的核心组成部分，包括事务处理、索引、查询优化等方面的内容，对事务处理机制进行了较为细致的讲解
- 《MySQL技术内幕：InnoDB存储引擎》：该书是一本MySQL数据库方面的重要教材，对InnoDB存储引擎的事务处理机制进行了详细的阐述，包括事务的隔离级别、MVCC实现、锁机制等方面。

想直接上手看工业届的事务实现原理，欢迎阅读：
- [分布式事务并发控制的思考与实现](https://open.oceanbase.com/blog/1100128)
- [OceanBase事务引擎的技术创新](https://open.oceanbase.com/blog/1100194)
- [深入浅出事务的本质，附OceanBase事务解析14问！](https://open.oceanbase.com/blog/1100248)

内功深厚想要直接阅读源码：[OceanBase 事务源码](https://github.com/oceanbase/oceanbase/tree/master/src/storage/tx)

还有一些著名开放课程，理论结合实践，比如 
- [CMU 15445](https://15445.courses.cs.cmu.edu/spring2023/)。理论结合实践，不仅有课程讲解，还有实验项目，对理解数据库实现原理帮助非常大
- [CMU 15721](https://15721.courses.cs.cmu.edu/spring2023/)。卡内基梅陇大学的数据库进阶课程

如果上面的还感觉太浅，可以持续找一些事务的论文研读：

- [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf) 该论文针对ANSI SQL标准中隔离级别的定义进行了深入的分析，提出了一些改进的建议。天天看到RC/RR名词的，可以看看这篇论文，了解更详细一点。

- [Granularity of Locks and Degrees of Consistency in a Shared Data Base](https://web.stanford.edu/class/cs245/readings/granularity-of-locks.pdf) 这也是一个又老又香的论文，提出了基于锁的并发控制。

- [An Empirical Evaluation of InMemory Multi-Version Concurrency Control](https://www.vldb.org/pvldb/vol10/p781-Wu.pdf) 介绍MVCC可扩展性的。通过这篇论文可以对MVCC有非常清晰的认识。

- [Scalable Garbage Collection for In-Memory MVCC Systems](http://www.vldb.org/pvldb/vol13/p128-bottcher.pdf) 这里对各种垃圾回收算法做了说明，并且有些创新算法。

大家看了这些论文会发现，都是一些陈年老论文。数据库领域发展这么多年了，技术依然不过时。

如果这些还不够，可以问问ChatGPT还有啥资料。如果觉得单机上的玩腻了，可以再看看分布式事务，总之希望你能玩得愉快。
