目录导读
- 变更通知在开源SpringBoot/SpringCloud微服务中的最佳实践
- 1. 什么是变更通知
- 2. 变更通知的场景分析
- 3. 变更通知的技术方案
- 3.1 变更通知的技术实现方案
 
- 4. 变更通知的最佳实践总结
- 5. 参考资料
 
变更通知在开源SpringBoot/SpringCloud微服务中的最佳实践
1. 什么是变更通知
- 变更通知是指数据发生变化后,相对实时地通知到关联端的技术实现方案;
- 变更通知是在服务拆分的发展过程中,逐渐衍生出的,解耦关联服务的一种技术解决方案;
- 微服务架构中常说的配置中心就是变更通知的技术集成方案;
2. 变更通知的场景分析
- 变更通知首先是数据发生了变化,数据变化后的通知实现,数据变化的交互场景列举分析如下:
| 通知类型 | 模块1 | 模块1数据变更 | 模块2 | 模块2实时变更 | 技术实现方式 | 本文讨论范围 | 
|---|---|---|---|---|---|---|
| 消息通知 | 前端1 | ✔ | 前端2 | ✔ | 前端总线等 | ✖ | 
| 消息保存 | 前端1 | ✔ | 后端2 | ✔ | Post Ajax请求等 | ✖ | 
| 消息通知 | 后端1 | ✔ | 前端2 | ✔ | WebSocket等推送到前端(推数据),也叫消息推送 | ✖ | 
| 消息通知 | 后端1 | ✔ | 前端2 | ✔ | 前端Http主动轮询(拉数据) | ✖ | 
| 变更通知 | 后端1 | ✔ | 后端2 | ✔ | Redis/ZooKeeper/Etcd/Nacos/MQ等 | ✔ | 
总结:
变更通知首先是数据(也可叫消息,下同。)发生了变化,然后引起了关联方同步变化;
数据变化可以发生在前端和后端之间的任意组合之间,包括前端+前端、前端+后端、后端+后端,一般只有后端+后端的连锁反应叫变更通知,这也是本文关注的重点;- 上表串联了所有常规的交互过程,大家也可以从中理解
消息通知和变更通知之间的关联关系:只是不同交互方式下的数据变化的叫法罢了;- 前端感知后端的数据变化,主要有
前端轮询和后端推送两种,二者技术实现差异较大,前者技术实现简单,但是后端资源消耗较大,无法承担高并发;后者技术实现较复杂,但是点对点通信效率高(长连接)。这不是本文讲述的重点,但希望能够类比分析下各种交互方式下的技术方案,做到一通百通;
变更通知主要关注的是后端数据变化,但是不是所有的后端数据变化都需要变更通知。一般来说,跨了服务实例,才比较适用变更通知,单个服务实例内,完全可以通过接口引用等方式自行获取到变化的数据;- 本文重点关注的是
变更通知而不是配置中心,像Nacos、SpringCloud-Config、Apollo等配置中心并不会过多介绍;
- 本文主要关注后端与后端之间的数据变化机制及技术实现,其发展阶段如下:
| 变更类型 | 应用发展阶段 | 技术方案 | 技术实现 | 说明 | 
|---|---|---|---|---|
| 主动获取 | 单实例 | 共享数据库 | 查询数据库 | 数据库查询量不大 | 
| 主动获取 | 单实例 | 共享内存缓存 | 查询时先查缓存 | 数据库查询量非常大 | 
| 主动获取 | 多实例 | 分布式缓存 | 使用读写性能极高的分布式缓存组件 | 如:Memcached/Redis | 
| 变更通知 | 多实例 | 消息中间件 | Redis/ZooKeeper/Etcd/MQ/binlog+canal等 | 订阅通知机制 | 
总结:
变更通知是随着服务的高并发、分布式发展而发展的,在单体架构时,因为都在一个服务内,仅通过数据库或者共享缓存即可达到数据共享的目的,不一定需要变更通知;- 在微服务架构中,也可以采用共享缓存方案,而不是必须使用
变更通知。变更通知适用于并发高、实时性要求高,且服务解耦的场景;
实时是一个相对概念,在变更通知语境里,一般是指异步监听的方式获取变化的数据(专业术语叫订阅通知);
3. 变更通知的技术方案
- 变更通知中间件种类较多,基于本人的理解,对比列举如下:
| 中间件类型 | 实现特点 | 适用场景 | 不足 | 补充说明 | 
|---|---|---|---|---|
| Redis | 基于key的订阅/通知 | 并发高、消息量大 | NoSQL可读性差,持久化不是必选项,存在数据丢失和审计风险 | Redis是极度常用的高效内存组件,建议优选; | 
| ZooKeeper | 基于分布式临时Node创建的订阅/通知 | 可靠性高、实时性高 | 使用的是内存存储,不适合高并发和大量数据的消息变更场景 | 一般是项目中有ZooKeeper,正好可以用作变更通知组件,而不是因为变更通知诉求而引入ZooKeeper | 
| Etcd | 基于分布式的Key/Value创建的订阅/通知 | 可靠性高、实时性高 | 使用的是内存存储,不适合高并发和大量数据的消息变更场景 | Etcd的实现参考了ZooKeeper,一个是Go语言编写、一个是Java语言 | 
| MQ | 异步消息协议 | 可靠性高、并发高、消息量大 | 组件较重 | 包括:Kafka/RocketMQ/RabbitMQ/ActiveMQ等,非常适合电商优惠卷等场景使用 | 
| binlog+canal | 针对MySQL的数据变更监听方案 | 直接监听数据库表字段变化 | 只适用于MySQL,而MySQL使用量正逐年下降 | 只是监听了MySQL的数据表变化,一般还需要配合其它的变更通知组件来配合使用,如:ZooKeeper | 
| 配置中心 | 服务端推送 | 把配置中心组件当成业务配置中心 | 组件重、可靠性低、实时性低 | 配置中心严格来说是个变更通知的解决方案,而上面列举的中间件是纯技术组件,二者的维度不太一样;配置中心一般和注册中心配合使用,因为它本身也需要注册至服务中心,如:Apollo、SpringCloud-Config;有些干脆就被注册中心兼任,如Nacos | 
总结:
变更通知一般来说是微服务中的增强功能,不建议因为有变更通知需求就新增一个组件。如果系统中已经有了Redis,就建议优先选择Redis,因为其性能高、消息存储量大。但Redis是NoSQL存储结构,可读性较差;Redis也有可能没有开启持久化导致数据丢失;Redis也缺失了类似关系数据库自带的操作审计,一旦数据出现了异常,将很难知道是谁做了什么;
ZooKeeper/Etcd则比较适合系统中已经引入该组件了,且变更通知消息数量较小的场景;
MQ比较适用于可靠性高、消息量巨大的场景,值得单独引入。如:大型电商的活动卡券配置等场景;
binlog+canal这个特定组合一般不建议单独使用,一般是canal把变更数据发送给其它变更通知Server,然后在业务模块订阅变更通知Server的这个变化数据,并做相应的业务处理。其它Server可以是ZooKeeper/Etcd/Redis等;- 一般来说,
配置中心包含了单独的配置规则界面和变更通知的能力,拆箱即用,效果当然较好。但并不是所有的变更通知都需要重量级的配置中心,不是非要在配置中心去配置变更数据的,就都没有必要用它;业务需要使用到配置中心组件时,建议在选定注册中心后,再来决策选择其配置中心;
- 变更通知有非常多的实现方式,讲讲本人实际经历的业务场景:
| 场景类型 | 业务诉求 | 微服务架构 | 微服务技术栈 | 方案说明 | 
|---|---|---|---|---|
| 场景1 | DB数据变更立即触发定时任务 | 微服务架构 | spring+mysql+binlog+canal+ZooKeeper | 基于spring自研微服务框架 | 
| 场景2-1 | 业务阈值在1天内生效 | 微服务云原生架构 | SpringBoot+Redis+PostgreSQL | 1.依赖k8s提供服务发现等; 2.依赖redis限流; | 
| 场景2-2 | 业务阈值在1天内生效 | 微服务私部署架构 | SpringBoot | 1.部署至客户机房,不依赖Redis/DB; 2.使用Nginx做负载均衡,也不需要服务注册和发现; | 
| 场景3 | 业务阈值在5分钟内生效 | 微服务云原生架构 | SpringBoot+Redis | 1.依赖k8s提供服务发现等; 2.依赖redis限流; | 
| 场景4 | 业务阈值在立即生效 | SpringCloud微服务架构 | SpringBoot+Redis+Nacos | 1.依赖Nacos提供服务发现等; 2.依赖redis限流; | 
- 补充说下上述业务场景的技术选型限定条件: - 场景1:DB数据变更立即触发定时任务:当时刚刚时兴微服务,我所在公司1的部门基于spring自研了一个低代码平台,我们需要实时监听数据的变化,以触发不同的定时任务,正好平台中也引入了canal开源组件,用于监听mysql的binlog
 变化,于是就选定了canal方案,这样就不用定时轮询数据库了,也不用和增删改数据的服务耦合了;
- 场景2:业务阈值在1天内生效:- 场景2-1和- 场景2-2其实对应同一个目标:我所在公司2的部门希望设计一套架构、一套代码,既能满足云上微服务架构,也能够支持私部署微服务架构。当时云上选型为云原生微服务架构,完全基于k8s+非侵入式的链路追踪中间件,我们基本上只使用最简单的SpringBoot+Redis即可;而私部署则继续沿用了这套架构和代码,只不过移除了k8s、redis、服务注册和服务发现,仅使用Nginx做服务负载均衡;
- 场景3:业务阈值在5分钟内生效:则是我所在公司2的业务团队认为一天生效对业务影响较大,需要调整为5分钟内生效;
- 场景4:业务阈值在立即生效:则是我所在公司2的另一个新项目,在- 场景3代码架构的基础上,使用SpringCloud套件替换了k8s,同时业务也更复杂,业务上必须保证实时生效;
 
- 单独来说上面的每个业务场景,都可以多种技术实现。本人仅站在过来人的角度,逐一展开分析。
3.1 变更通知的技术实现方案
-  场景1的实现方案:单独部署了一个Canal服务,用于监听binlog变化,在Canal服务中又集成了ZooKeeper客户端,Canal收到变化的数据后,通过ZooKeeper推送至订阅的业务微服务;
-  场景2的实现方案:经过分析,私部署不需要变更通知,因为私部署不带数据库,业务阈值是配在yaml中,修改后隔离重启服务即可;云原生微服务则因为数据刷新后1天内生效即可,但为了考虑私部署和云原生架构的统一,所以采用了Guava+持久层的本地缓存方案,Guava缓存的有效期设置为24小时,过期后就会重新从PostgreSQL/yaml中获取。但由于云原生的运维不接收直接修改数据库阈值数据,于是又配套开发了一个小的命令行工具(也可以做成Web运维平台),用于专门更改数据库表的字段值。从中可以看出: - 因为要兼顾云原生和私部署场景,所以需要选择两种场景下都能使用的本地缓存方案:Guava;
- 因为缓存刷新的时效要求低,不使用变更通知也是完全可行的。
 
-  场景3的实现方案:场景3其实是场景2的延续,只是现在数据生效的时间从1天变成了5分钟,考虑技术方案的延续性,在云原生方案中,新增了一个定时任务+Redis,用于刷新缓存,交互逻辑如下图所示:
  说明: - 第1次请求到业务服务时,Guava缓存中也没有数据,则需要业务服务查询一次Database并把数据缓存至Guava,流程为蓝色①→②箭头所示;
- 第2次请求到业务服务时,Guava缓存中已经有了数据,则只需要直接返回数据即可,流程为绿色①所示;
- 当用运维工具更新数据时,同时也会清理掉Redis的缓存标记,一旦ScheduleJob获取的Redis Key(采用redis的setNX语法)不一致时,则会让Guava缓存失效,流程为红色的①→②→③→④箭头所示;
- 后面再有请求过来时,会重新执行上面的第1步和第2步;
 总结: - 场景3仅在- 场景2的基础上迭代增加了虚线框框中的2个小功能点代码就可以了,代码延续性较好;
- ScheduleJob设置为1分钟就可以满足5分钟内生效的业务诉求了;
- 回过头来看看场景2:其实并没有做变更通知,只是采用了Guava自带的缓存过期机制而已;场景3其实也可以采用Guava自带的缓存过期机制,但是会导致微服务需要频繁的穿透缓存去查询数据库,得不偿失;场景3的实际做法则兼顾了准实时性和便利性:只有命令行工具变更了数据时,缓存才会刷新。另外场景3也没有做到变更通知,只是变相的达到了准实时的变更通知的效果。
 
-  场景4的实现方案:场景4是在复用场景3的代码框架的基础上,要求做到数据变更实时通知。此时项目中虽然已引入了SpringCloud的多个组件,但是业务参数配置都有单独的配置界面,无须使用配置中心。考虑到Redis其实也有变更通知的能力,此处正好可以在场景3的基础上继续迭代,去掉ScheduleJob,增加Redis订阅通知的代码即可。
4. 变更通知的最佳实践总结
- 需要搞清楚什么是变更通知,什么是配置中心,不要因为有变更通知的需求就上配置中心,这样有可能把系统搞得非常复杂;
- 好的设计都要顺势而为,首先需要了解系统的需求到底是什么,是不是一定要做变更通知,如本人列举的项目实践中就多次未使用变更通知组件,但是达到了变更通知的效果;
- 如果只需要做变更通知,不需要独立的配置中心,建议优选Redis,因为它可以兼顾业务限流、高速缓存、不规则数据的处理(NoSQL)等,很有可能Redis就已经存在于项目中了;
- 如果数据量非常庞大,还要支持复杂的规则,比如消息确认和重传等,则建议采用MQ(Kafka/RocketMQ/RabbitMQ/ActiveMQ);
- 有些场景下的变更通知非常适合使用配置中心,如:SpringCloud-Gateway的路由规则yaml配置,就非常适合放在配置中心(如:Nacos等);当然如果使用的是k8s,则建议直接使用其ConfigMap;
5. 参考资料
- [1]深入浅出阿里数据同步神器:Canal原理+配置+实战全网最全解析!
- [2]图文解析 Nacos 配置中心的实现
- [3]apollo 基本原理
- [4]Spring Cloud Config 原理简介和实现