事件溯源和命令和查询责任分离模式

Achievement provides the only real pleasure in life

CRUD模式

在 Web Service 中我们一般分为三层:

  1. Controller
  2. Service
  3. Repository

而作用于这三个层次的对象有:

  1. DTO (Data Transfer Object) 数据传输对象
  2. BO (Business Object or Domain Object)
  3. DAO (Create/Retrieve/Update/Delete)

对于这些对象有基本操作有 CRUD , CUD 是数据更新, R是数据读取, 两者大不相同, 前者有副作用, 一般需要事务管理, 而后者只是读取数据,无任何副作用。

单机应用无所谓, 传统数据库系统也对强一致性做了很好的 ACID 支持,而到了分布式应用, 使用 NOSQL 系统, 传统的 CRUD 的问题就凸显出来了。

多个应用同时修改一条记录, NOSQL 怎么保证数据一致性呢, NOSQL 没有传统数据库那样的行级锁。从下两个模式可以解决这个问题

事件溯源模式

Event Source 模式由来已久, Greg Young 在 DDD(Domain Driven Design) 的应用中做了更多的阐述.
它通过事件来表示一个领域对象(Aggregation 聚合)的完整状态, 通过自该对象创建以来的一系列事件, 按时事件产生时的顺序进行重放, 来重建对象的当前状态.

它使用只追加存储来记录对数据采取的完整系列操作,而不是仅存储域中数据的当前状态。 该存储可作为记录系统,可用于具体化域对象。

这样一来,无需同步数据模型和业务域,从而简化复杂域中的任务,同时可提高性能、可扩展性和响应能力。

它还可提供事务数据一致性并保留可启用补偿操作的完整审核记录和历史记录。

命令和查询责任分离模式

使用独立接口将读取数据的操作与更新数据的操作分离。 这可以最大程度地提高性能、可伸缩性和安全性。 通过提高灵活性,让系统随着时间的推移而改进;防止更新命令在域级别引发并冲突

CQRS 将之前的CRUD 所针对的一个DAO对象拆分成了两个对象,这种分离是基于方法是执行命令还是执行查询这一原则来定的, CUD是命令 Command, R是查询 Query.

典型示例

以最常用的银行帐户应用为例, 假设 Account Service 的后台存储为 NoSQL的Cassandra

1598924-fee01408aee28728.png

为避免上述的扩展性和一致性的问题, 所有帐户的操作(Create/Update/Delete) 转化为命令, 这里仅以修改(Update) 帐户余额为例, Create/Delete 也是类似的做法

假设上月留存 7200 元

  1. command1: +10000 --> 发工资存入一万
  2. command2: -500 --> 买饭卡花去500
  3. command3: -3000 --> 还信用卡花去3000
  4. command4: +50 --> 基金投资收益 50

Cassandra 的 table 结构如下

CREATE TABLE account_change (
    account_id uuid, 
    change_time timeuuid, 
    change_value int, 
    change_user text, 
    create_time timestamp,
    primary key(account_id, change_time)) 
    with clustering order by(change_time desc)

存储在 NOSQL 中的键值对如下

insert into account_change(account_id, change_time, change_value, change_user, create_time) 
values(ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9, minTimeuuid('2018-12-18 13:21:20-0500'), 10000, 'alice', '2018-12-18 13:21:20+0800');  

insert into account_change(account_id, change_time, change_value, change_user, create_time) 
values(ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9, minTimeuuid('2018-12-18 14:21:20-0500'), -500, 'bob', '2018-12-18 14:21:20+0800'); 

insert into account_change(account_id, change_time, change_value, change_user, create_time) 
values(ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9, minTimeuuid('2018-12-18 14:21:21-0500'), -3000, 'bob', '2018-12-18 14:21:21+0800');

insert into account_change(account_id, change_time, change_value, change_user, create_time) 
values(ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9, minTimeuuid('2018-12-18 14:21:22-0500'), 500, 'carl', '2018-12-18 14:21:22+0800');

对于帐户的查询可回溯以上命令, 先查询出此帐户的更改命令记录.

Query:

select * from account_change where account_id= ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9;
account_idchange_timechange_userchange_valuecreate_time
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f91a6de500-02fa-11e9-8080-808080808080carl5002018-12-18 06:21:22+0000
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f919d54e80-02fa-11e9-8080-808080808080bob-30002018-12-18 06:21:21+0000
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9193cb800-02fa-11e9-8080-808080808080bob-5002018-12-18 06:21:20+0000
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9b7785000-02f1-11e9-8080-808080808080alice100002018-12-18 05:21:20+0000
  • 事件溯源: 将命令逐条取出, 按照时间(change_time timeuuid) 倒序排列, 回放命令得到以下帐户余额: 7200+10000-500-3000+50 = 13750

这样的做法, 比直接逐次将余额修改成 17200, 16700, 13700, 13750 看起来更麻烦, 其实在操作上更简单, 查询余额时虽然多了计算的步骤, 可相比处理麻烦的竞态条件, 维护强一致性, 这种做法相比起来更容易实现。

当然,没有银弹,没有万能药,事件源模式有个缺点,如果事件太多会追溯太久,性能难以忍受,这时应该适时存储快照,或者应用具体化视图模式和补偿事务模式

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值