Open AlexiaChen opened 4 years ago
CQRS的概念来自于领域驱动设计(DDD),就是命令与查询职责分离,相当于根据领域模型进行更高一层次的读写分离。一般读写两端的数据库在物理上是分割的。写端就是commands端,读端其实就是query端,这样做能最大化性能和提高系统的可伸缩性,更加灵活,如果CQRS模式结合Event sourcing模式实现复杂度也会更高,至今还没有一个成熟的框架。如果要实现,比较考验架构师的能力,因为要从CQRS的概念出发,实现基本的底层特性。
当然,读写两端的存储层就不用自己动手写了,一般用成熟的数据库,比如写端可以是MySQL或者PostgreSQL,读端可以是MongoDB或者PostgreSL(PG支持materialized views)。 当然,按理来说写端和读端的数据库是不一样的。但据知乎上更专业的人士评价,PostgreSQL作为号称全世界最先进的数据库,一个牛逼之处就是,几乎可以通吃主流场景(OLAP,OLTP),而且都能达到不错的规模,这是相当不得了,或许读写两端都可以用PostgreSQL了?https://github.com/tobyhede/postgresql-event-sourcing/ https://medium.com/@tobyhede/event-sourcing-with-postgresql-28c5e8f211a2 https://github.com/commanded/eventstore 看以上几个链接,看来PG通吃读写两端了。
CQRS配合Event sourcing的基本实现原理如下:
应用界面程序发起一个command,这个command在Commands端是的CommandHandler异步接收的,然后一个command一般只做好一件事(对领域模型对象的聚合进行修改,实现业务逻辑),command之间尽量正交。
然后把关于一个领域对象变更的多个command的操作打包成一个不可变的事件(Event),这个事件追加(append-only)写入事件存储层(Event store)中。这样顺序写可以最大化磁盘IO性能,学过数据库的都知道,磁盘随机写是最耗时的了。同时也可以尽量减少事务冲突竞争,提高并发,想象下,数据库是有行锁的,并发修改同一行数据元组会产生竞争。
事件写入存储层之后,一般会发布这个事件,就是把这个事件放到MQ中来“通知”Query端的系统甚至其他我们现在暂时不关心的外围应用。(这里用到MQ需要考虑MQ的常见问题,比如消息去重,at least once语义等,MQ可以采用Kafka,RabbitMQ等)
Query端异步接收这些事件消息,通过这些有顺序的事件序列,重放事件序列关联的数据实体的当前状态,Query端根据这些事件流可以定时或定量地创建快照,以提高重放性能,因为事件流如果太大太长,从原始状态恢复当前状态会影响性能。(跨机器异步接收Event,就需要考虑最终一致性的问题了,比如采用补偿事务,重试等手段保证,这种手段微服务架构经常提到,比如saga事务等。这里一定不会用到分布式事务的强一致性算法,比如2PC等,所以潜在逻辑也说明,CQRS不适合做金融领域的余额等要求强一致性的业务)
Query端然后根据构建出来的数据实体的当前状态来构建物化视图(materialized view),这些视图结构要根据查询需求调整定制。这些视图都是只读的,这样可以最大化Query端的查询性能以及灵活性(到这里你会发现,Event sourcing很像比特币的UTXO模型了,Scala大神邓草原说过,区块链虽然落地困难,但是其思想或许提供了一个新的设计模式)
另外提一些细节:因为事件序列(Event sequence)是有顺序的,与UTXO模型很像,类似区块链里面的交易(Transaction)链,顺序一旦搞乱了,重放事件序列会有错误,所以每个事件有自己的ID和时间戳,Event ID需要关联到一个数据实体上,也就是说,该事件是对哪一个数据实体做的变更记录。也可以通过实体ID拿到关于它的事件序列,通过事件序列来重放(恢复)它的当前状态。
当然,还有更多的细节,可以看以下参考资料,或者结合自己的架构,工程经验来实现了,目前应该没啥捷径可走。
参考资料
CQRS的概念来自于领域驱动设计(DDD),就是命令与查询职责分离,相当于根据领域模型进行更高一层次的读写分离。一般读写两端的数据库在物理上是分割的。写端就是commands端,读端其实就是query端,这样做能最大化性能和提高系统的可伸缩性,更加灵活,如果CQRS模式结合Event sourcing模式实现复杂度也会更高,至今还没有一个成熟的框架。如果要实现,比较考验架构师的能力,因为要从CQRS的概念出发,实现基本的底层特性。
当然,读写两端的存储层就不用自己动手写了,一般用成熟的数据库,比如写端可以是MySQL或者PostgreSQL,读端可以是MongoDB或者PostgreSL(PG支持materialized views)。 当然,按理来说写端和读端的数据库是不一样的。但据知乎上更专业的人士评价,PostgreSQL作为号称全世界最先进的数据库,一个牛逼之处就是,几乎可以通吃主流场景(OLAP,OLTP),而且都能达到不错的规模,这是相当不得了,或许读写两端都可以用PostgreSQL了?https://github.com/tobyhede/postgresql-event-sourcing/ https://medium.com/@tobyhede/event-sourcing-with-postgresql-28c5e8f211a2 https://github.com/commanded/eventstore 看以上几个链接,看来PG通吃读写两端了。
CQRS配合Event sourcing的基本实现原理如下:
应用界面程序发起一个command,这个command在Commands端是的CommandHandler异步接收的,然后一个command一般只做好一件事(对领域模型对象的聚合进行修改,实现业务逻辑),command之间尽量正交。
然后把关于一个领域对象变更的多个command的操作打包成一个不可变的事件(Event),这个事件追加(append-only)写入事件存储层(Event store)中。这样顺序写可以最大化磁盘IO性能,学过数据库的都知道,磁盘随机写是最耗时的了。同时也可以尽量减少事务冲突竞争,提高并发,想象下,数据库是有行锁的,并发修改同一行数据元组会产生竞争。
事件写入存储层之后,一般会发布这个事件,就是把这个事件放到MQ中来“通知”Query端的系统甚至其他我们现在暂时不关心的外围应用。(这里用到MQ需要考虑MQ的常见问题,比如消息去重,at least once语义等,MQ可以采用Kafka,RabbitMQ等)
Query端异步接收这些事件消息,通过这些有顺序的事件序列,重放事件序列关联的数据实体的当前状态,Query端根据这些事件流可以定时或定量地创建快照,以提高重放性能,因为事件流如果太大太长,从原始状态恢复当前状态会影响性能。(跨机器异步接收Event,就需要考虑最终一致性的问题了,比如采用补偿事务,重试等手段保证,这种手段微服务架构经常提到,比如saga事务等。这里一定不会用到分布式事务的强一致性算法,比如2PC等,所以潜在逻辑也说明,CQRS不适合做金融领域的余额等要求强一致性的业务)
Query端然后根据构建出来的数据实体的当前状态来构建物化视图(materialized view),这些视图结构要根据查询需求调整定制。这些视图都是只读的,这样可以最大化Query端的查询性能以及灵活性(到这里你会发现,Event sourcing很像比特币的UTXO模型了,Scala大神邓草原说过,区块链虽然落地困难,但是其思想或许提供了一个新的设计模式)
另外提一些细节:因为事件序列(Event sequence)是有顺序的,与UTXO模型很像,类似区块链里面的交易(Transaction)链,顺序一旦搞乱了,重放事件序列会有错误,所以每个事件有自己的ID和时间戳,Event ID需要关联到一个数据实体上,也就是说,该事件是对哪一个数据实体做的变更记录。也可以通过实体ID拿到关于它的事件序列,通过事件序列来重放(恢复)它的当前状态。
当然,还有更多的细节,可以看以下参考资料,或者结合自己的架构,工程经验来实现了,目前应该没啥捷径可走。
参考资料