复用与解耦,是推动软件工程技术发展的两大思想溯源。
谈到解耦,就不能不先谈耦合。耦合,是指两个软件组件之间有相互影响的或强或弱的关联关系。软件组件的范围涉及:函数、类、库、框架、模块、微服务、操作系统、硬件体系结构。在实际谈到耦合与解耦时,组件通常指的是模块与微服务。
本文对组件间的耦合和解耦方式做个小小的梳理,以备后用。
耦合
数据库读写耦合
多个组件共享访问同一个数据库,且都具有读写能力。会导致数据管理混乱而无法追溯。一般常见于创业初期快速上线满足业务需求的单体应用。
数据库只读耦合
多个组件共享访问同一个数据库,只有一个组件具备写权限,其它只有只读权限。虽然数据写可控,但读不可控,如果有组件有高并发请求,可能会影响所有服务的稳定性。
缓存读写耦合
多个组件共享访问同一个 redis 缓存实例,都具备读写权限。必须隔离各个组件所使用的名字空间,避免写覆盖导致的数据混乱。如果某个组件的读写把 redis 打挂了,那么所有组件都会受到影响。因此,应用应当对 redis 具有降级策略,避免核心功能过度依赖 redis。
RPC 耦合
多个组件通过 RPC 调用来通信和交换信息。RPC 调用方对被调用方有依赖关系。如果被调用方不够稳定,而调用方又没有降级策略,就可能会产生级联稳定性影响。通过 HTTP 通信同样有此问题。
要解决RPC的稳定性级联影响,通常采用降级策略,对应用中的弱依赖进行降级,使用线程池隔离和异常捕获,避免受弱依赖不稳定的影响。
消息耦合
多个组件通过消息队列来传递信息,彼此没有耦合。实际上是转移了耦合关系,彼此都依赖消息队列。消息队列需要高可用,避免单点故障。这种常成为“生产者消费者模式”,采用“订阅—推送—拉取”的方式实现。
消息耦合是微服务间的相对理想的耦合方式之一。
共享数据耦合
同一个微服务内,多个模块共享同一个内存缓存,或者都能对同一个库表进行读写。
API 数据耦合
同一个微服务内,多个模块有自己独享的库表,其它模块只能通过 API 来访问该模块的独有信息。
API 是同一微服务内部的相对理想的耦合方式之一。
运行环境耦合
多个组件运行于同一个 JVM 实例内。如果 JVM 挂了,那么所有组件都无法正常工作。同样,多个组件运行于同一个操作系统环境下或容器下,都有运行环境耦合,共享和竞争 CPU 、内存、外存和网络带宽资源。
解耦
部署解耦
场景:
- 应用的部署环境标准化,与所在操作系统的环境无关。
- 应用的伸缩容能够配置化和自动化执行。
方式:
- 虚拟化(虚拟机、容器)
- K8S
领域解耦
场景:
- 不同领域有各自复杂的领域模型,必须保持独立性,同时有领域间通信需求。比如资产、入侵、风险、镜像等是不同的领域。
方式:
- 领域模型
- 微服务
- 六边形(适配器)架构
微服务解耦
场景:
- 即使是同一个领域,也会有多个微服务。比如入侵领域也有检测、响应与阻断等微服务。
- 微服务之间需要解耦。
方式:
- 模块化
- 消息传递
- API (HTTP, Restful, RPC)
- 协议通信
业务/模块解耦
场景:
- 同一个微服务中,有不同的子模块,子模块之间有相互关联,又有独立性,需要解耦。比如检测模块行为依赖于检测配置模块里的配置。
方式:
- 消息传递
- API
业务与基础框架解耦
场景:
- 业务要使用基础框架的基本功能。比如使用SpringIOC。
- 业务要向基础框架注入自定义功能或实现。比如应用要系统启动前加载一些自定义动态模块,或在 JVM 退出前做一些资源释放操作。
方法:
- API
- 插件机制
- 接口与钩子
复用与扩展解耦
场景:
- 在现有功能基础上添加扩展/定制功能。
方法:
- 插件架构
- 扩展点机制
- 接口与设计模式(策略模式、模板方法模式、组合模式、装饰模式)
数据处理解耦
场景:
- 一份数据,多种业务处理。比如同一份登录事件要做不同的算法检测处理。或者同一份主机上线卸载事件要做不同业务处理。
方式:
- 消息广播
- 线程池隔离加异常捕获
流程解耦
场景:
- 主流程与子流程分离,主流程的执行不依赖于子流程的执行完成。比如应用启动时的必要资源加载,或者异步操作。
方法:
- 线程池隔离加异常捕获
任务解耦
场景:
- 系统会创建不同类别或同样类别的任务,任务的耗时长短不一,任务执行相互独立。比如导出任务。
方法:
- 线程池隔离加异常捕获
应对复杂性