mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
697 lines
38 KiB
HTML
697 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>当我们在讨论CQRS时,我们在讨论些神马?.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
|
||
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java中9种常见的CMS GC问题分析与解决(上).md.html">Java中9种常见的CMS GC问题分析与解决(上).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java中9种常见的CMS GC问题分析与解决(下).md.html">Java中9种常见的CMS GC问题分析与解决(下).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Java魔法类:Unsafe应用解析.md.html">Java魔法类:Unsafe应用解析.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL优化:优化 select count().md.html">MySQL优化:优化 select count().md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/MySQL的MVCC(多版本并发控制).md.html">MySQL的MVCC(多版本并发控制).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/从SpringCloud开始,聊微服务架构.md.html">从SpringCloud开始,聊微服务架构.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/文章/当我们在讨论CQRS时,我们在讨论些神马?.md.html">当我们在讨论CQRS时,我们在讨论些神马?.md.html</a>
|
||
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/服务注册与发现原理剖析(Eureka、Zookeeper、Nacos).md.html">服务注册与发现原理剖析(Eureka、Zookeeper、Nacos).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/进阶:Dockerfile 高阶使用指南及镜像优化.md.html">进阶:Dockerfile 高阶使用指南及镜像优化.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析(上).md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析(上).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析(下).md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析(下).md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>当我们在讨论CQRS时,我们在讨论些神马?</h1>
|
||
<p>当我写下这个标题的时候,我就有些后悔了,题目有点大,不太好控制。但我还是打算尝试一下,通过这篇内容来说清楚CQRS模式,以及和这个模式关联的其它东西。希望我能说得清楚,你能看得明白,如果觉得不错,右下角点个推荐!</p>
|
||
<p>先从CQRS说起,CQRS的全称是Command Query Responsibility Segregation,翻译成中文叫作命令查询职责分离。从字面上就能看出,这个模式要求开发者按照方法的职责是命令还是查询进行分离,什么是命令?什么是查询?我们来继续往下看。</p>
|
||
<h2>Query & Command</h2>
|
||
<p><strong>什么是命令?什么是查询?</strong></p>
|
||
<ul>
|
||
<li>命令(Command):不返回任何结果(void),但会改变对象的状态。</li>
|
||
<li>查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。</li>
|
||
</ul>
|
||
<p><strong>对象的状态是什么意思呢?</strong></p>
|
||
<p>对象的状态,我们可以理解成它的属性,例如我们定义一个Person类,定义如下:</p>
|
||
<pre><code>public class Person {
|
||
public string Id { get; set; }
|
||
public string Name { get; set; }
|
||
public int Age { get; set; }
|
||
|
||
public void Say(string word) {
|
||
Console.WriteLine($"{Name} Say: {word}");
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>在Person类中:</p>
|
||
<ul>
|
||
<li>Name、Age:属性(状态)</li>
|
||
<li>Say(string): 方法(行为)</li>
|
||
</ul>
|
||
<p>再回到本小节讨论的内容,是不是就很好理解了呢?当我定义一个方法,要改变Person实例的Name或Age的时候,这个方法就属于Command;如果定一个方法,只查询Person实例信息的时候,这个方法就属于Query。当我们按照职责将Command和Query进行分离的时候,你就在使用CQRS模式了。</p>
|
||
<p><strong>其实这就是CQRS的全部。</strong></p>
|
||
<p>有朋友可能要说了,如果这就是CQRS的全部,也太过于简单了吧?是的,大道至简!</p>
|
||
<h2>读写分离</h2>
|
||
<p>当我们按照CQRS进行分离以后,你是不是已经看出来,这玩意儿太适合做读写分离了?当我们的数据库是主从模式的时候,主库负责写入、从库负责读取,完全匹配Command和Query,简直完美。那么我们接下来就说一下读写分离。</p>
|
||
<p>现在主流的数据库都支持主从模式,主从模式的好处是方便我做故障迁移,当主库宕机的时候,可以快速的启用从库,从而减小系统不可用时间。</p>
|
||
<p>当我们在使用数据库主从模式的时候,如果应用程序不做读写分离,你会发现从库基本上没用,主库每天忙的要死,既要负责写入,又要负责查询,遇见访问量大的时候CPU飙升是常有的事。然而从库就太闲了,除了接收主库的变更记录做数据同步,再没有别的事情可做,不管主库压力多大,从库的CPU一直跟心电图似的0-1-0-1...当我们读写分离以后,主库负责写入,从库负责读取,代码要怎么改呢?我们只需要定义两个Repository就可以了:</p>
|
||
<pre><code>public interface IWritablePersonRepository {
|
||
//写入数据的方法
|
||
}
|
||
public interface IReadonlyPersonRepository {
|
||
//读取数据的方法
|
||
}
|
||
</code></pre>
|
||
<p>在IWritablePersonRepository中使用主库的连接,IReadonlyPersonRepository中使用从库的连接。然后,在Command里面使用IWritablePersonRepository, 在Query里面使用IReadonlyPersonRepository,这样就在应用层实现了读写分离。</p>
|
||
<h2>CRUD和EventSourcing</h2>
|
||
<p>说到CQRS,不可避免的要说到这两个数据操作模型。为什么要说数据操作模型呢?因为数据操作严重影响性能,而我们分离的一个重要目的就是要提高性能。</p>
|
||
<h3>CRUD</h3>
|
||
<p>CRUD(Create、Read、Update、Delete)是<strong>面向数据</strong>的,它将对数据的操作分为创建、更新、删除和读取四类,这四个操作可以对应我们SQL语句中的insert、select、update、delete,非常直观明了,它的存在就是操作数据的。</p>
|
||
<p>因为存在即合理,我们不能片面的说CRUD是好或者坏,这里只简单说一下它存在的问题:</p>
|
||
<ul>
|
||
<li>并发冲突:这是个大问题,当A和B同时更新一行记录的时候,你的事务必然报错。</li>
|
||
<li>丢失数据操作的上下文:这个问题也不小,对于开发者来说,我们通常要知道数据是谁在什么时候做了什么更新,但是CURD只存储了最终的状态,对数据操作的上下文一无所知。</li>
|
||
</ul>
|
||
<p>好了,更多的问题不再列举,单是“并发冲突”这一个问题,在高并发的环境下就不适用。既然CRUD不适用,我们在构建高性能应用的时候,就只能寄希望于ES了。</p>
|
||
<h3>Event Souring</h3>
|
||
<p>Event Souring,翻译过来叫事件溯源。什么意思呢?它把对象的创建、修改、删除等一系列的操作都当作事件(<em>注意:事件和命令还有区别,后面会讲到</em>),持久化的时候只存储事件,存储事件的介质叫做<strong>EventStore</strong>,当要获取一个对象的最新状态时,通过EventStore检索该对象的所有Event并重新加载来获取对象的最新状态。EventStore可以是数据库、磁盘文件、MongoDB等,由于Event的存储都是新增的,所以不存在并发冲突的问题。</p>
|
||
<h2>Command和Event</h2>
|
||
<p>在CQRS+ES的方案中,我们要面对这两个概念,命令和事件。</p>
|
||
<ul>
|
||
<li>Command:描述了用户的意图。</li>
|
||
<li>Event:描述了对象状态的改变。</li>
|
||
</ul>
|
||
<p>我们举一个例子,比如说你要更新自己的个人资料,例如将Age由35修改为18,那么对应的命令为:</p>
|
||
<pre><code>public class PersonUpdateCommand {
|
||
public string Id { get; set; }
|
||
public int Age{ get; set; }
|
||
|
||
public PersonUpdateCommand(string id, int age){
|
||
this.Id = id;
|
||
this.Age = age;
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>PersonUpdateCommand是一个命令,它描述了用户更新个人资料的意图。当程序接收到这个命令以后,就需要对数据更改,从而引发数据状态变化,产生Event:</p>
|
||
<pre><code>public class PersonAgeChangeEvent {
|
||
public string Id { get; private set; }
|
||
public int Age{ get; private set; }
|
||
|
||
public PersonAgeChangeEvent(string id, int age){
|
||
this.Id = id;
|
||
this.Age = age;
|
||
}
|
||
}
|
||
public class PersonUpdateCommandHandler {
|
||
private PersonUpdateCommand Command;
|
||
|
||
public PersonUpdateCommandHandler(PersonUpdateCommand command) {
|
||
this.Command = command;
|
||
}
|
||
|
||
public void Handle() {
|
||
var person = GetPersonById(Command.Id);
|
||
if(person.Age != Command.Age) {
|
||
//生成并发送事件
|
||
var @event = new PersonAgeChangeEvent(Command.Id, Command.Age);
|
||
EventBus.Send(@event);
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h2>数据一致性</h2>
|
||
<p>常见的数据一致性模型有两种:强一致性和最终一致性。</p>
|
||
<ul>
|
||
<li>强一致性:在任何时刻所有的用户或者进程查询到的都是最近一次成功更新的数据。</li>
|
||
<li>最终一致性:和强一致性相对,在某一时刻用户或者进程查询到的数据可能有不同,但是最终成功更新的数据都会被所有用户或者进程查询到。</li>
|
||
</ul>
|
||
<p>说到一致性的问题,我们就不得不说一下CAP定理。</p>
|
||
<h3>CAP定理</h3>
|
||
<p>1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。</p>
|
||
<ul>
|
||
<li>Consistency:一致性</li>
|
||
<li>Availability:可用性</li>
|
||
<li>Partition tolerance:分区容错</li>
|
||
</ul>
|
||
<p>它们的第一个字母分别是 C、A、P,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。</p>
|
||
<p>对于分布式系统来说,受CAP定理的约束,最终一致性就成了唯一的选择。实现最终一致性要考虑以下问题:</p>
|
||
<ul>
|
||
<li>重试策略:在分布式系统中,我们无法保证每一次操作都能被成功的执行,例如网络中断、服务器宕机等临时性的错误,都会导致操作执行失败,那么我们就要等待故障恢复后进行重试。重试的操作对于系统来说可能会造成一些副作用,例如你正在支付的时候网络中断了,这个时候你不知道是否支付成功,联网以后再次重试,可能就会造成重复扣款。如果要避免重试造成的系统危害,就要将操作设计为幂等操作。</li>
|
||
<li>
|
||
<ul>
|
||
<li><strong>幂等性</strong>:简单的说,就是一个操作执行一次和执行多次产生的结果是一样的,不会产生副作用。</li>
|
||
</ul>
|
||
</li>
|
||
<li>撤销策略:与重试策略相对应的,如果一个操作最终确定执行失败,那么我们需要撤销这个操作,将系统还原到执行该操作之前的状态。撤销操作有两种,一种是直接将对象修改为执行前的状态,这种情况将造成数据审计不一致的问题;另一种是类似于财务上的红冲操作,新增一个命令,冲掉上一个操作,从而保证数据的完整性,并能够满足数据审计的要求。</li>
|
||
</ul>
|
||
<h2>Messaging</h2>
|
||
<p>通过上面的介绍,我们已经知道在一个系统中所有的改变都是基于操作和由操作产生的事件所引发的。消息可以是一个Command,也可以是一个Event。当我们基于消息来实现CQRS中的命令和事件发布的时候,我们的系统将会更加的灵活可扩展。</p>
|
||
<p>如果你的系统基于消息,那么我猜你离不开消息总线,我在<a href="https://www.cnblogs.com/youring2/p/10991338.html">《手撸一套纯粹的CQRS实现》</a>中写了一个基于内存的CommandBus的实现,感兴趣的朋友可以去看一下,CommandBus的代码定义如下:</p>
|
||
<pre><code>public class CommandBus : ICommandBus
|
||
{
|
||
private readonly ICommandHandlerFactory handlerFactory;
|
||
public CommandBus(ICommandHandlerFactory handlerFactory)
|
||
{
|
||
this.handlerFactory = handlerFactory;
|
||
}
|
||
public void Send<T>(T command) where T : ICommand
|
||
{
|
||
var handler = handlerFactory.GetHandler<T>();
|
||
if (handler == null)
|
||
{
|
||
throw new Exception("未找到对应的处理程序");
|
||
}
|
||
handler.Execute(command);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>基于内存的消息总线只能用于开发环境,在生产环境下不能够满足我们分布式部署的需要,这个时候就需要采用基于消息队列的方式来实现了。消息队列有很多,例如Redis的订阅发布、RabbitMQ等,消息总线的实现也有很多优秀的开源框架,例如Rebus、Masstransit等,选一个你熟悉的框架即可。</p>
|
||
<h2>数据审计</h2>
|
||
<p>数据审计是CQRS带给我们的另一个便利。由于我们存储了所有事件,当我们要获取对象变更记录的时候,只需要将EventStore中的记录查询出来,便可以看到整个的生命周期。这种操作,简直比打开了你青春期的日记本还要清晰明了。</p>
|
||
<p>当然,如果你要想知道对象的操作审计日志怎么办?同样的道理,我们记录下所有的Command就可以了。那所有查询日志呢?哈哈,不要调皮了。记录的东西越多,你的存储就越大,如果你的存储空间允许的话,当然是越详细越好的,主要还是看业务需求。</p>
|
||
<p>如果我们记录了所有Command,我们还可以有针对性的进行分析,哪些命令使用量大、哪些命令执行时间长。。这些数据将对我们的扩容提供数据支撑。</p>
|
||
<h2>分组部署</h2>
|
||
<p>在分布式系统中,Command和Query的使用比例是不一样的,Command和Command之间、Query和Query之间的权重也存在差异,如果单纯的将这些服务平均的部署在每一个节点上,那纯粹就是瞎搞。一个比较靠谱的实践是将不同权重的Command和Query进行分组,然后进行有针对性的部署。</p>
|
||
<h2>总结</h2>
|
||
<p>CQRS很简单,如何用好CQRS才是关键。CQRS更像是一种思想,它为我们提供了系统分离的基本思路,结合ES、Messaging等模式,为构建分布式高可用可扩展的系统提供了良好的理论依据。</p>
|
||
<p>园子里有很多钻研CQRS+ES的前辈,本文借鉴了他们的文章和思想,感谢他们的分享!</p>
|
||
<p>文章中有任何不准确或错误的地方,请不吝赐教!欢迎讨论!</p>
|
||
<h2>参考文档</h2>
|
||
<ul>
|
||
<li><a href="https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html">https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html</a></li>
|
||
<li><a href="https://www.cnblogs.com/netfocus/p/4150084.html">https://www.cnblogs.com/netfocus/p/4150084.html</a></li>
|
||
<li><a href="https://www.ruanyifeng.com/blog/2018/07/cap.html">http://www.ruanyifeng.com/blog/2018/07/cap.html</a></li>
|
||
<li><a href="https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn589800(v=pandp.10)">https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn589800(v=pandp.10)</a></li>
|
||
<li><a href="https://msdn.microsoft.com/magazine/mt238399">https://msdn.microsoft.com/magazine/mt238399</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/文章/异步网络模型.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70998058d9b88b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|