Achievement provides the only real pleasure in life
在 Web Service 中我们一般分为三层:
而作用于这三个层次的对象有:
对于这些对象有基本操作有 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
为避免上述的扩展性和一致性的问题, 所有帐户的操作(Create/Update/Delete) 转化为命令, 这里仅以修改(Update) 帐户余额为例, Create/Delete 也是类似的做法
假设上月留存 7200 元
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_id | change_time | change_user | change_value | create_time |
---|---|---|---|---|
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9 | 1a6de500-02fa-11e9-8080-808080808080 | carl | 500 | 2018-12-18 06:21:22+0000 |
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9 | 19d54e80-02fa-11e9-8080-808080808080 | bob | -3000 | 2018-12-18 06:21:21+0000 |
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9 | 193cb800-02fa-11e9-8080-808080808080 | bob | -500 | 2018-12-18 06:21:20+0000 |
ddccb8eb-1e89-4e27-a222-e35dcc7cd5f9 | b7785000-02f1-11e9-8080-808080808080 | alice | 10000 | 2018-12-18 05:21:20+0000 |
这样的做法, 比直接逐次将余额修改成 17200, 16700, 13700, 13750 看起来更麻烦, 其实在操作上更简单, 查询余额时虽然多了计算的步骤, 可相比处理麻烦的竞态条件, 维护强一致性, 这种做法相比起来更容易实现。
当然,没有银弹,没有万能药,事件源模式有个缺点,如果事件太多会追溯太久,性能难以忍受,这时应该适时存储快照,或者应用具体化视图模式和补偿事务模式