数据库事务功能非常重要, 任何应用只要操作的多个对象之间有依赖(约束)关系, 都会不约而同地想到使用事务, 例如银行转账功能, 社交 App 中的粉丝关注功能, 购物网站下订单功能. 任何一个数据库系统, 如果不提供事务功能, 就不能减少用户(应用开发者)的某些麻烦, 因为用户不得不自己在应用层去实现类似事务的代码逻辑.
从用户的角度看, 如果数据库不提供事务, 他就要多写代码, 这让他很不爽. 所以, 即使是 KV 数据库, 也应该提供事务功能. 但是, 不仅事务功能的实现是有成本的, 使用也有成本. 比如, 很多用户不能准确地理解事务的特点和作用.
抽丝剥茧, 我们来分析一下数据库事务的本质是什么? 用户对事务功能的需求的本质是什么?
先从 ACID(Atomicity, Consistency, Isolation, Durability)说起.
- Durability(持久化), 这不是事务的特性, 而是数据库系统的本能特性, 没有持久化的数据库是"伪数据库", 是缓存系统. 所以, 没有啥可讨论的.
- Consistency(一致性), 一致性用于解决多副本不相同, 和解决数据依赖(约束)级联更新问题. 多副本和数据依赖, 本质上都是多处数据更新时间差的问题, 因为多处数据绝对不可能立即同时更新. 一致性本质上是原子性(Atomicity)问题.
- Isolation(隔离性), 隔离性本质上也是受多数数据不能"立即同时"更新所限而产生的. 因为多数数据不能立即同时更新, 所以看到更新的先后顺序会对业务的因果关系(数据依赖)产生影响. 不同的隔离级别产生不同的影响.
- Atomicity(原子性), 原子性是数据库事务的最重要特性, 也是用户的目的(本质). 用户之所以使用事务, 是因为用户追求的正是原子性
数据库事务常常和锁一起使用, 无论是用户的意图, 或者是数据库系统的实现原理, 事务都和锁紧密联系在一起. 大部分的事务都涉及到单一资源的争抢.
在网站购物为例子, 我们在一个事务中扣减商品的库存, 同时添加一条订单记录. 这个事务里, 就涉及到了对商品库存的争抢, 其本质就是锁. 虽然你可以在 SQL 中使用 "update where amount>=x"
这种句式来使用乐观锁, 但业务的本质确实是在争抢资源, 就是在用锁.
只要事务中的多个操作之间有因果依赖关系, 本质就是锁.
有一个加粉丝关注功能的例子, 原来我认为用户需要的只是原子性, 后来才发现, 用户本质上需要的是原子性+锁. 这个例子是这样的:
开始时, B 关注了 A.
接着, A 加 B 为好友, 需要在事务(T1)中做这些操作:
- 往 A 的 following 列表里添加 B
- 往 B 的 followers 列表里添加 A
- 因为 A 和 B 互相关注, 往 A 的 friends 列表里添加 B, 同时往 B 的 friends 列表里添加 A
在 T1 开始之前, 用户 B 决定取关 A, 注意, 只是下了决定, 但还没有执行, 基于决定, B 要在事务(T2)中做这些操作:
- 把 A 从 B 的 following 列表中删除
- 把 B 从 A 的 followers 列表中删除
T2 不操作 friends 列表, 因为 B 下决定的时候, 认为两者不是好友. 如果 T1 完全执行完毕后, T2 再执行, 就会出现 B 在 A 的好友列表中, 但 B 实际上并没有关注 A, 违反了业务约束.
用数据表格来表示数据的随时间变动:
A's following | A's followers | A's friends | B's following | B's followers | B's friends | |
---|---|---|---|---|---|---|
开始 | B | A | ||||
T1 之后 | B | B | B | A | A | A |
T2 之后 | B | B | A | A |
为了解决这个问题, T2 可以利用悲观锁, 在下决定之前加锁(或者把加锁和下决定作为事务的一部分), 排斥 T1.
或者, 保存"下决定"所依据的数据的版本号, 然后在事务 T2 中执行 CAS 操作(乐观锁).
总结
原子性, 悲观锁, 乐观锁, 这3者是用户对数据库事务需求的本质, 大部分实际场景的事务都会使用锁.
PS: 数据库系统的读操作的因果关系可能与原子性冲突, 一个事务中修改了2个对象, 你可能先见到一个对象已更新, 但之后又发现另一个对象还没有更新. 见这个有趣的讨论: https://www.ideawu.net/blog/archives/1150.html