前言
关系型数据库如SQL server, Oracle, mysql这类强一致性的数据库天然地支持数据库事务,而大部分NoSQL数据库(如Riak)则由于没有隔离性、锁机制等原因不能实现数据库事务,要实现事务控制则必须从应用层面来处理。本文以Riak为例,介绍基于nosql数据库的应用如何实现数据库事务的方案。
事务和隔离性
在开始介绍方案前,先简单回顾下事务的四个特性(ACID):
- atomicity
- consistency
- isolation
- durability
其中,隔离性是指多个事务(用户)之前的操作不会相互影响,多个事务并行执行结果应该与各个事务串行执行结果一致。注意,这里说的是执行结果应该与串行执行一致,而不是必须串行地执行,因为串行执行会成为性能提升的瓶颈。不同的隔离性会产生不同的副作用,比如:
- Load Updates
- Dirty Read
- Non-repeatable read
- Phantom read
其中,第一项"更新丢失"在大部分强一致性数据库系统中不会出现,因为这些系统通常都会采用锁的机制来保证同一数据同时只能被一个事务修改。
围绕着如何在隔离性(以及他说产生的问题)与系统性能之间的平衡产生了一系列的隔离级别:
- Read uncommitted
- Read committed
- Repeatable read
- Serializable
注意,数据库隔离级别的名称跟部分事务副作用有类似,甚至有人直接用隔离级别产生的副作用代替级别名称,留意这些,防止被这些概念搞混。
方案初探
不少介绍数据库事务的资料都是以银行账户之间转账来说明数据库事务,这里也沿用这个传统:用户A需要先用户B转入一笔金额,在事务内需要进行如下操作:
- 账户A扣款
- 账户B打款
由于Riak不支持回滚操作,即不论第二步操作是否成功,第一步操作的结果都将保存在数据库中,所以需要增加一个“表”:事务日志,于是正常的操作转账将变为三步:
- 账户A扣款
- 账户B打款
- 假如上述两步都成功则写入事务日志
具体来说: 这是事务开始之前的状态: /account/a
1 2 3 4 5 6 7 8 |
|
/account/b
1 2 3 4 5 6 7 8 |
|
这是事务完成之后: /account/a
1 2 3 4 5 6 7 8 9 10 11 |
|
/account/b
1 2 3 4 5 6 7 8 9 10 11 |
|
/transactionlog/txid_20141008_xxx
1 2 3 4 |
|
完成了这些之后,在读取时还需遵循这一的规则:
- 读取账户余额时需要从balance中的历史记录中倒推计算得出
- balance记录只有在transactionlog表中存在对应的记录方为有效
- 在事务范围内,忽略本事务id之后的事务id(即使是那些成功的事务)
只有在遵循了上述几个原则之后,才能保证在事务执行时的三步任意一步失败时,标志着整个事务失败了。其中,
- 1避免了强一致性数据库中锁的应用,又能避免lost updates的问题:所有的操作都有据可查,数据不会在执行更新操作时被覆盖。
- 2保证了原子性:一个失败全部失败
- 3保证了只有在事务开始之前的事务结果才是有效的,也即达到了read committed的隔离级别
- 三个原则一起保证了durability和consistency