从 ACID 到 BASE
在业务很简单,数据量不大的时候,传统单机关系数据库就够用了。在各个关系数据库里,很早就实现了单机版的事务,并总结成了 ACID 四个特性。
- Atomity,原子性,事务内的所有操作要么全部执行,要么全部不执行。
- Consistency,一致性,事务执行完后,数据库仍然保持合法的状态,如不违反主外键约束等。
- Isolation,隔离性,事务之间互相隔离,并发执行时也不受影响。
- Durability,持久性,事务提交后,涉及的变化都会被持久化下来,不会因系统故障而丢失。
另一个关联很大,但容易混淆的概念,是前面文章讲过的的 CAP。
- Consistency,一致性,不同时刻的不同请求都能返回同样的数据。
- Availability,可用性,每个外部请求都能得到系统的有效响应。
- Partition tolerance,分区容忍性,在出现网络分区时,系统仍然能正常运转。
可以看到,ACID 和 CAP 里的 C 有着完全不一样的含义。前者强调数据库状态始终合法有效,后者强调不同副本上的数据对外而言始终一样。
以 MySQL 为典型的单机关系型数据库能很好的支持 ACID,但是,当数据量和请求并发膨胀到一定程度后,必然会横向扩展为分布式数据库。
典型的实现是在单机数据库的基础上,做多库多表。无论是纵向的对同一类数据的切分,比如把用户表切分后放到 100 个数据库实例;还是横向的对不同类型的数据做切分,比如把用户表和商品表分别放到不同的数据库实例,都是难免的。
数据库变成分布式之后,我们自然能像前面文章介绍的那样,在单机事务的基础上,通过 2PC 的方式实现分布式事务。
而出于高可用的需要,还可以采用 binlog 等方式把数据复制到 slave 机器上,这个过程也可以通过 2PC 实现。
但是,任何分布式系统都逃不过 CAP 的诅咒。
前面文章已经详细说明过,2PC 模式的分布式事务,多轮多节点的协商导致性能不佳,并且无法提供分区容忍性。
虽然 ACID 中的 C 和 CAP 中的 C 含义并不一样,但引申到分布式语境下,ACID 确实也隐含了强一致性保证。而基于 2PC 的分布式事务则在分布式的场景下延续了对强一致性的追求,可以称之为 ACID on 2PC
。哪怕我们继续优化,比如 ACID on 3PC
,也没法从根本上解决问题。
同时,上篇文章说过,在大数据量和高并发的场景下,有时候,可用性和性能(也可以看作可用性的体现)的重要性并不比一致性弱。一个动不动不响应或者响应非常慢的系统,数据再一致,也很难大规模应用起来。
一方面,一致性难以完全保证,另一方面,可用性和性能又不能不管,那对分布式事务而言,到底有没有更好的出路呢?
答案是肯定的。并且已经有人将这个思路总结为 BASE 理论:
- BA,Basically Available,基本可用,不追求完整的可用性。部分可用也好过完全不可用。
- Soft state,软状态,不追求状态机那样机械的状态转换,允许出现中间状态。所谓「柔性事务」也是这个意思。
- Eventually consistency,最终一致,不追求无时无刻的强一致。
简单讲,就是牺牲一部分一致性,来换取可用性。非常重要也非常典型的 trade-off。
在化学术语中,ACID 是酸的意思,而 BASE 则是碱的意思。从名字上也能看出二者的关系。
基于 Dynamo 的分布式事务
对了啊!上篇文章不是刚介绍过 Dynamo 吗,现成的因果(弱)一致性分布式数据库,那干嘛不直接在 Dynamo 的基础上做事务?!
在客户端实现事务
Amazon 官方曾经提供过一个叫 dynamodb-transactions 的库,来帮助应用在客户端实现分布式事务。
大致来说,是一个多步提交(multi-phase commit protocol)的实现:
- create,创建一个主键唯一的 TX record,保存为 Dynamo 对象。
- add,把事务相关的对象添加到 TX 对象的对象列表里。
- lock,逐个把相关对象的 lock 设置为本 TX id。
- save,保存相关对象的副本,以备回滚。
- verify,重读 TX record,确保状态仍是 pending,以防止和其他事务产生竞争。
- apply,执行事务对应的操作。修改操作会直接改动原对象,删除操作这时不会执行。
- commit,TX record 状态从 pending 改为 commited。
- complete,释放事务相关对象的锁,并删除 save 阶段保存的副本。
- clean,TX record 状态更新为 complete。
- delete,删除 TX record。
看起来列了很多步,有点吓人,实际上只是操作细节而已,和 2PC 的 Prepare-Commit 差不多。
上面是没有出现冲突时的正常执行流程。一旦出现冲突,则会进入另外的处理流程:
- decide,从对象锁拿到占用它的 TX id,判断该 TX 状态,如果是 pending,说明没开始,则改成 roll-back(所以上面正常流程才有 verify 这步)。
- complete,如果该事务状态是 committed,则继续执行正常的提交流程;如果是 roll-back,则执行回滚流程。
- clean,同上的 clean 操作。
可以看到,事务之间是可以互相帮忙推进流程的,这个比较激进的机制当然有利于推动事务尽快完成,但也加剧了竞争。频繁的互相回滚是可以预见的局面。所以可以考虑在 decide 步骤前加入等待等办法来缓解。
除了事务之间的竞争,DynamoDB 还支持多个 coordinator 处理同一个事务,不过解决竞争的思路类似,就不再赘述了。
需要注意的是,apply 阶段的修改操作会直接更新对象,即使事务没提交,这个修改也是可见的。当然,可以通过加读锁来避免。
在服务端实现事务
客户端自己实现总是不方便且容易出错,于是在 2018 年,DynamoDB 终于在服务端集成了事务功能。
// 以下省略 checkCustomerValid、markItemSold 和 createOrder 的定义代码
Collection<TransactWriteItem> actions = Arrays.asList(
new TransactWriteItem().withConditionCheck(checkCustomerValid),
new TransactWriteItem().withUpdate(markItemSold),
new TransactWriteItem().withPut(createOrder));
TransactWriteItemsRequest placeOrderTransaction = new TransactWriteItemsRequest()
.withTransactItems(actions)
.withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);
// Execute the transaction and process the result.
try {
client.transactWriteItems(placeOrderTransaction);
System.out.println("Transaction Successful");
} catch (ResourceNotFoundException rnf) {
System.err.println("One of the table involved in the transaction is not found" + rnf.getMessage());
} catch (InternalServerErrorException ise) {
System.err.println("Internal Server Error" + ise.getMessage());
} catch (TransactionCanceledException tce) {
System.out.println("Transaction Canceled " + tce.getMessage());
}
如上图的示例代码,通过以 transactWriteItems
和 transactGetItems
为核心的事务 API,几行熟悉的代码,就能实现事务的效果。
无论是客户端还是服务端的实现,由于 DynamoDB 采用了 W+R
的读写模式,即使事务执行成功,其他请求仍有可能读到过时的副本。不过,可以设置 ConsistentRead 参数强制读足够多的副本来获取最新的数据。
另外,为了降低网络问题导致的事务大量失败带来的用户体验问题,DynamoDB 只支持同一个 region(简单理解为 AWS 上的一个机房) 下的事务,不支持跨 region 的 global table 上的事务。
基于 BASE 的分布式事务
DynamoDB 虽好,但是如果我们已有的系统没用 DynamoDB 呢,或者用了 DynamoDB,但事务里面还包含其他数据库和服务(heterogeneous distributed transactions)呢,又该怎么办?
牺牲一致性
再回过头想想,既然 BASE 想要做的是牺牲一致性来换取可用性和性能(某种程度看也属于可用性)。那就先牺牲起来,怎么牺牲一致性呢?
回顾前面几篇文章,我们提到的单主同步、2PC 和 Paxos 这些强一致性模型,虽然差别很大,但都有一个共性:通过同步的方式做数据交互。
那想要牺牲一致性,只要改同步为异步就好了。这符合我们在系列第 8 篇讲到的内容,也正好解决了提升性能的需求。
而要做异步,最常用的就是 MQ。
从 ACID 到 BASE
在业务很简单,数据量不大的时候,传统单机关系数据库就够用了。在各个关系数据库里,很早就实现了单机版的事务,并总结成了 ACID 四个特性。
- Atomity,原子性,事务内的所有操作要么全部执行,要么全部不执行。
- Consistency,一致性,事务执行完后,数据库仍然保持合法的状态,如不违反主外键约束等。
- Isolation,隔离性,事务之间互相隔离,并发执行时也不受影响。
- Durability,持久性,事务提交后,涉及的变化都会被持久化下来,不会因系统故障而丢失。
另一个关联很大,但容易混淆的概念,是前面文章讲过的的 CAP。
- Consistency,一致性,不同时刻的不同请求都能返回同样的数据。
- Availability,可用性,每个外部请求都能得到系统的有效响应。
- Partition tolerance,分区容忍性,在出现网络分区时,系统仍然能正常运转。
可以看到,ACID 和 CAP 里的 C 有着完全不一样的含义。前者强调数据库状态始终合法有效,后者强调不同副本上的数据对外而言始终一样。
以 MySQL 为典型的单机关系型数据库能很好的支持 ACID,但是,当数据量和请求并发膨胀到一定程度后,必然会横向扩展为分布式数据库。
典型的实现是在单机数据库的基础上,做多库多表。无论是纵向的对同一类数据的切分,比如把用户表切分后放到 100 个数据库实例;还是横向的对不同类型的数据做切分,比如把用户表和商品表分别放到不同的数据库实例,都是难免的。
数据库变成分布式之后,我们自然能像前面文章介绍的那样,在单机事务的基础上,通过 2PC 的方式实现分布式事务。
而出于高可用的需要,还可以采用 binlog 等方式把数据复制到 slave 机器上,这个过程也可以通过 2PC 实现。
但是,任何分布式系统都逃不过 CAP 的诅咒。
前面文章已经详细说明过,2PC 模式的分布式事务,多轮多节点的协商导致性能不佳,并且无法提供分区容忍性。
虽然 ACID 中的 C 和 CAP 中的 C 含义并不一样,但引申到分布式语境下,ACID 确实也隐含了强一致性保证。而基于 2PC 的分布式事务则在分布式的场景下延续了对强一致性的追求,可以称之为 ACID on 2PC
。哪怕我们继续优化,比如 ACID on 3PC
,也没法从根本上解决问题。
同时,上篇文章说过,在大数据量和高并发的场景下,有时候,可用性和性能(也可以看作可用性的体现)的重要性并不比一致性弱。一个动不动不响应或者响应非常慢的系统,数据再一致,也很难大规模应用起来。
一方面,一致性难以完全保证,另一方面,可用性和性能又不能不管,那对分布式事务而言,到底有没有更好的出路呢?
答案是肯定的。并且已经有人将这个思路总结为 BASE 理论:
- BA,Basically Available,基本可用,不追求完整的可用性。部分可用也好过完全不可用。
- Soft state,软状态,不追求状态机那样机械的状态转换,允许出现中间状态。所谓「柔性事务」也是这个意思。
- Eventually consistency,最终一致,不追求无时无刻的强一致。
简单讲,就是牺牲一部分一致性,来换取可用性。非常重要也非常典型的 trade-off。
在化学术语中,ACID 是酸的意思,而 BASE 则是碱的意思。从名字上也能看出二者的关系。
基于 Dynamo 的分布式事务
对了啊!上篇文章不是刚介绍过 Dynamo 吗,现成的因果(弱)一致性分布式数据库,那干嘛不直接在 Dynamo 的基础上做事务?!
在客户端实现事务
Amazon 官方曾经提供过一个叫 dynamodb-transactions 的库,来帮助应用在客户端实现分布式事务。
大致来说,是一个多步提交(multi-phase commit protocol)的实现:
- create,创建一个主键唯一的 TX record,保存为 Dynamo 对象。
- add,把事务相关的对象添加到 TX 对象的对象列表里。
- lock,逐个把相关对象的 lock 设置为本 TX id。
- save,保存相关对象的副本,以备回滚。
- verify,重读 TX record,确保状态仍是 pending,以防止和其他事务产生竞争。
- apply,执行事务对应的操作。修改操作会直接改动原对象,删除操作这时不会执行。
- commit,TX record 状态从 pending 改为 commited。
- complete,释放事务相关对象的锁,并删除 save 阶段保存的副本。
- clean,TX record 状态更新为 complete。
- delete,删除 TX record。
看起来列了很多步,有点吓人,实际上只是操作细节而已,和 2PC 的 Prepare-Commit 差不多。
上面是没有出现冲突时的正常执行流程。一旦出现冲突,则会进入另外的处理流程:
- decide,从对象锁拿到占用它的 TX id,判断该 TX 状态,如果是 pending,说明没开始,则改成 roll-back(所以上面正常流程才有 verify 这步)。
- complete,如果该事务状态是 committed,则继续执行正常的提交流程;如果是 roll-back,则执行回滚流程。
- clean,同上的 clean 操作。
可以看到,事务之间是可以互相帮忙推进流程的,这个比较激进的机制当然有利于推动事务尽快完成,但也加剧了竞争。频繁的互相回滚是可以预见的局面。所以可以考虑在 decide 步骤前加入等待等办法来缓解。
除了事务之间的竞争,DynamoDB 还支持多个 coordinator 处理同一个事务,不过解决竞争的思路类似,就不再赘述了。
需要注意的是,apply 阶段的修改操作会直接更新对象,即使事务没提交,这个修改也是可见的。当然,可以通过加读锁来避免。
在服务端实现事务
客户端自己实现总是不方便且容易出错,于是在 2018 年,DynamoDB 终于在服务端集成了事务功能。
// 以下省略 checkCustomerValid、markItemSold 和 createOrder 的定义代码
Collection<TransactWriteItem> actions = Arrays.asList(
new TransactWriteItem().withConditionCheck(checkCustomerValid),
new TransactWriteItem().withUpdate(markItemSold),
new TransactWriteItem().withPut(createOrder));
TransactWriteItemsRequest placeOrderTransaction = new TransactWriteItemsRequest()
.withTransactItems(actions)
.withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);
// Execute the transaction and process the result.
try {
client.transactWriteItems(placeOrderTransaction);
System.out.println("Transaction Successful");
} catch (ResourceNotFoundException rnf) {
System.err.println("One of the table involved in the transaction is not found" + rnf.getMessage());
} catch (InternalServerErrorException ise) {
System.err.println("Internal Server Error" + ise.getMessage());
} catch (TransactionCanceledException tce) {
System.out.println("Transaction Canceled " + tce.getMessage());
}
如上图的示例代码,通过以 transactWriteItems
和 transactGetItems
为核心的事务 API,几行熟悉的代码,就能实现事务的效果。
无论是客户端还是服务端的实现,由于 DynamoDB 采用了 W+R
的读写模式,即使事务执行成功,其他请求仍有可能读到过时的副本。不过,可以设置 ConsistentRead 参数强制读足够多的副本来获取最新的数据。
另外,为了降低网络问题导致的事务大量失败带来的用户体验问题,DynamoDB 只支持同一个 region(简单理解为 AWS 上的一个机房) 下的事务,不支持跨 region 的 global table 上的事务。
基于 BASE 的分布式事务
DynamoDB 虽好,但是如果我们已有的系统没用 DynamoDB 呢,或者用了 DynamoDB,但事务里面还包含其他数据库和服务(heterogeneous distributed transactions)呢,又该怎么办?
牺牲一致性
再回过头想想,既然 BASE 想要做的是牺牲一致性来换取可用性和性能(某种程度看也属于可用性)。那就先牺牲起来,怎么牺牲一致性呢?
回顾前面几篇文章,我们提到的单主同步、2PC 和 Paxos 这些强一致性模型,虽然差别很大,但都有一个共性:通过同步的方式做数据交互。
那想要牺牲一致性,只要改同步为异步就好了。这符合我们在系列第 8 篇讲到的内容,也正好解决了提升性能的需求。
而要做异步,最常用的就是 MQ。
从 ACID 到 BASE
在业务很简单,数据量不大的时候,传统单机关系数据库就够用了。在各个关系数据库里,很早就实现了单机版的事务,并总结成了 ACID 四个特性。
- Atomity,原子性,事务内的所有操作要么全部执行,要么全部不执行。
- Consistency,一致性,事务执行完后,数据库仍然保持合法的状态,如不违反主外键约束等。
- Isolation,隔离性,事务之间互相隔离,并发执行时也不受影响。
- Durability,持久性,事务提交后,涉及的变化都会被持久化下来,不会因系统故障而丢失。
另一个关联很大,但容易混淆的概念,是前面文章讲过的的 CAP。
- Consistency,一致性,不同时刻的不同请求都能返回同样的数据。
- Availability,可用性,每个外部请求都能得到系统的有效响应。
- Partition tolerance,分区容忍性,在出现网络分区时,系统仍然能正常运转。
可以看到,ACID 和 CAP 里的 C 有着完全不一样的含义。前者强调数据库状态始终合法有效,后者强调不同副本上的数据对外而言始终一样。
以 MySQL 为典型的单机关系型数据库能很好的支持 ACID,但是,当数据量和请求并发膨胀到一定程度后,必然会横向扩展为分布式数据库。
典型的实现是在单机数据库的基础上,做多库多表。无论是纵向的对同一类数据的切分,比如把用户表切分后放到 100 个数据库实例;还是横向的对不同类型的数据做切分,比如把用户表和商品表分别放到不同的数据库实例,都是难免的。
数据库变成分布式之后,我们自然能像前面文章介绍的那样,在单机事务的基础上,通过 2PC 的方式实现分布式事务。
而出于高可用的需要,还可以采用 binlog 等方式把数据复制到 slave 机器上,这个过程也可以通过 2PC 实现。
但是,任何分布式系统都逃不过 CAP 的诅咒。
前面文章已经详细说明过,2PC 模式的分布式事务,多轮多节点的协商导致性能不佳,并且无法提供分区容忍性。
虽然 ACID 中的 C 和 CAP 中的 C 含义并不一样,但引申到分布式语境下,ACID 确实也隐含了强一致性保证。而基于 2PC 的分布式事务则在分布式的场景下延续了对强一致性的追求,可以称之为 ACID on 2PC
。哪怕我们继续优化,比如 ACID on 3PC
,也没法从根本上解决问题。
同时,上篇文章说过,在大数据量和高并发的场景下,有时候,可用性和性能(也可以看作可用性的体现)的重要性并不比一致性弱。一个动不动不响应或者响应非常慢的系统,数据再一致,也很难大规模应用起来。
一方面,一致性难以完全保证,另一方面,可用性和性能又不能不管,那对分布式事务而言,到底有没有更好的出路呢?
答案是肯定的。并且已经有人将这个思路总结为 BASE 理论:
- BA,Basically Available,基本可用,不追求完整的可用性。部分可用也好过完全不可用。
- Soft state,软状态,不追求状态机那样机械的状态转换,允许出现中间状态。所谓「柔性事务」也是这个意思。
- Eventually consistency,最终一致,不追求无时无刻的强一致。
简单讲,就是牺牲一部分一致性,来换取可用性。非常重要也非常典型的 trade-off。
在化学术语中,ACID 是酸的意思,而 BASE 则是碱的意思。从名字上也能看出二者的关系。
基于 Dynamo 的分布式事务
对了啊!上篇文章不是刚介绍过 Dynamo 吗,现成的因果(弱)一致性分布式数据库,那干嘛不直接在 Dynamo 的基础上做事务?!
在客户端实现事务
Amazon 官方曾经提供过一个叫 dynamodb-transactions 的库,来帮助应用在客户端实现分布式事务。
大致来说,是一个多步提交(multi-phase commit protocol)的实现:
- create,创建一个主键唯一的 TX record,保存为 Dynamo 对象。
- add,把事务相关的对象添加到 TX 对象的对象列表里。
- lock,逐个把相关对象的 lock 设置为本 TX id。
- save,保存相关对象的副本,以备回滚。
- verify,重读 TX record,确保状态仍是 pending,以防止和其他事务产生竞争。
- apply,执行事务对应的操作。修改操作会直接改动原对象,删除操作这时不会执行。
- commit,TX record 状态从 pending 改为 commited。
- complete,释放事务相关对象的锁,并删除 save 阶段保存的副本。
- clean,TX record 状态更新为 complete。
- delete,删除 TX record。
看起来列了很多步,有点吓人,实际上只是操作细节而已,和 2PC 的 Prepare-Commit 差不多。
上面是没有出现冲突时的正常执行流程。一旦出现冲突,则会进入另外的处理流程:
- decide,从对象锁拿到占用它的 TX id,判断该 TX 状态,如果是 pending,说明没开始,则改成 roll-back(所以上面正常流程才有 verify 这步)。
- complete,如果该事务状态是 committed,则继续执行正常的提交流程;如果是 roll-back,则执行回滚流程。
- clean,同上的 clean 操作。
可以看到,事务之间是可以互相帮忙推进流程的,这个比较激进的机制当然有利于推动事务尽快完成,但也加剧了竞争。频繁的互相回滚是可以预见的局面。所以可以考虑在 decide 步骤前加入等待等办法来缓解。
除了事务之间的竞争,DynamoDB 还支持多个 coordinator 处理同一个事务,不过解决竞争的思路类似,就不再赘述了。
需要注意的是,apply 阶段的修改操作会直接更新对象,即使事务没提交,这个修改也是可见的。当然,可以通过加读锁来避免。
在服务端实现事务
客户端自己实现总是不方便且容易出错,于是在 2018 年,DynamoDB 终于在服务端集成了事务功能。
// 以下省略 checkCustomerValid、markItemSold 和 createOrder 的定义代码
Collection<TransactWriteItem> actions = Arrays.asList(
new TransactWriteItem().withConditionCheck(checkCustomerValid),
new TransactWriteItem().withUpdate(markItemSold),
new TransactWriteItem().withPut(createOrder));
TransactWriteItemsRequest placeOrderTransaction = new TransactWriteItemsRequest()
.withTransactItems(actions)
.withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);
// Execute the transaction and process the result.
try {
client.transactWriteItems(placeOrderTransaction);
System.out.println("Transaction Successful");
} catch (ResourceNotFoundException rnf) {
System.err.println("One of the table involved in the transaction is not found" + rnf.getMessage());
} catch (InternalServerErrorException ise) {
System.err.println("Internal Server Error" + ise.getMessage());
} catch (TransactionCanceledException tce) {
System.out.println("Transaction Canceled " + tce.getMessage());
}
如上图的示例代码,通过以 transactWriteItems
和 transactGetItems
为核心的事务 API,几行熟悉的代码,就能实现事务的效果。
无论是客户端还是服务端的实现,由于 DynamoDB 采用了 W+R
的读写模式,即使事务执行成功,其他请求仍有可能读到过时的副本。不过,可以设置 ConsistentRead 参数强制读足够多的副本来获取最新的数据。
另外,为了降低网络问题导致的事务大量失败带来的用户体验问题,DynamoDB 只支持同一个 region(简单理解为 AWS 上的一个机房) 下的事务,不支持跨 region 的 global table 上的事务。
基于 BASE 的分布式事务
DynamoDB 虽好,但是如果我们已有的系统没用 DynamoDB 呢,或者用了 DynamoDB,但事务里面还包含其他数据库和服务(heterogeneous distributed transactions)呢,又该怎么办?
牺牲一致性
再回过头想想,既然 BASE 想要做的是牺牲一致性来换取可用性和性能(某种程度看也属于可用性)。那就先牺牲起来,怎么牺牲一致性呢?
回顾前面几篇文章,我们提到的单主同步、2PC 和 Paxos 这些强一致性模型,虽然差别很大,但都有一个共性:通过同步的方式做数据交互。
那想要牺牲一致性,只要改同步为异步就好了。这符合我们在系列第 8 篇讲到的内容,也正好解决了提升性能的需求。
而要做异步,最常用的就是 MQ。