• 从单体架构到分布式架构,有哪些坑?


    背景

    提出问题:

    在聊项目架构风格之前,我们先明确一个问题,什么是架构?我们为什么要选择架构、用来解决哪些问题?

    什么是架构?

    书本定义:“软件的架构是一种抽象的结构,他由软件的各个组成部分和这些部分之间的依赖关系构成”。

    我的理解是,架构就是根据业务选择合适的技术、中间件,并且按照合适的设计模式对这些模块,进行组装来满足业务特性的需求。

    选择架构风格的目的

    我们选择架构风格的初衷在于 “三更原则”(自己的理解) :更好的降本提效、更快的发版上线、更好的维护系统稳定性。

    任何一个架构风格,都可以实现功能性需求,但是一个好的架构风格能在功能性需求之上,提升非功能性需求,那么你可能会问,什么是非功能性需求?举例:扩展性、稳定性等等。

    这里我将会以我认知结合踩过的坑,来给大家详细讲一下,我们是如何从单体架构演进到分布式架构,在向分布式单体架构的演进的道路上,又如何进行的抉择,以及为什么最后同时选择了微服务架构+分布式架构的原因。

    接下来就结合一个系统来作为案例,贯穿主线讲解。首先来讲一下,最初的单体架构的经历和转型。

    单体架构

    我们在系统创建之初,往往都是集中业务、单点部署系统,所有业务打一个包,快速上线。满足了业务初期的快速发版上线,而且适合中小公司没有自己的 paas 平台,应对初期快速迭代的业务,开发、迭代、测试、发布都是非常的便捷。那么单体架构都有什么类型呢?

    单体架构的类型

    单体架构也分为,大泥团架构、分层单体架构、模块化单体架构,他们的区别是什么呢?

    大泥团单体架构

    毫无分层、所有模块聚焦在一起 相互穿插(除非是你接手需要改造,否则不要创建这样的架构风格,这种大泥团架构很难拆分,到最后的下场往往都是重新搭建)

    分层单体架构

    普遍的选择,架构进行了简单的分层,比如传统的 mvc 三层架构

    模块化单体架构

    一般是随着业务的发展,由分层单体架构演变而来,特点就是引入了多个业务模块并且提供相应的服务能力。

    单体架构的优缺点

    优点

    1. 应用的开发很简单
    2. 易于对应用程序大规模的更改
    3. 测试相对简单、直观
    4. 部署简单明了
    5. 横向扩展不费吹灰之力

    在业务的初期,单体架构的优点,无论从哪个方面来说,都优于其他架构风格,但是随着业务的增加、耦合,单体架构的缺点也逐渐暴露出来,这个也符合“康威定律”。那么单体架构的“后期”会出暴露出哪些问题呢?

    缺点

    1. 代码库膨胀
    2. 过度的复杂性会吓退开发者
    3. 开发速度慢
    4. 从代码提交到实际部署的周期很长,而且容易出问题
    5. 难以扩展
    6. 系统的稳定性得不到保障
    7. 需要长期依赖某个可能过时的技术栈

    单体架构的这些缺点,其实影响的还是我上面提到的“三更原则”。经过上面的铺垫,相信大家已经对单体架构风格已经有了简单的理解,那么光有方法论是不行的,我们得结合项目以及代码片段来加深理解,做到真正的应用。

    接下来我就用一个库存系统来进行串联进行讲解。先通过这张图来了解下库存系统是用来做什么的?

    在这里插入图片描述

    如图:

    1. 创建之初,1 个服务提供商品库存维护、库存查询、库存扣减能力。
    2. 随着业务的发展,库存面向多个服务:B 端业务,平台内部业务系统、平台外部中台。C 端业务,订单商品扣减库存、网关查询库存数量。

    单体架构的案例:库存系统

    最初的代码分层:

    1. API:对外提供的 dubbo 服务
    2. common:封装了公共方法
    3. dao:封装了数据库 dbcp 交互
    4. domain:实体类
    5. innerApi:系统内部 api 交互
    6. router:废弃
    7. rpc:上下游 rpc 交互
    8. service:业务逻辑层
    9. web:web 服务层
    10. worker:任务调度层

    在最初很长的一段时间里,我们部署了两个单体服务,一个是 API 接口来保障上游的库存查询以及调用,另一个是 web 服务的后台管理平台。

    这两个单体服务很好的贴合了最初的业务迭代和发版速度,但是后来随着业务的增加附加调用量的增加,单体服务的无论是从性能和稳定性都出现了较大的波动。

    意料之外,情理之中的事故惨案

    某一天,库存的 web 管理平台挂了,原因就是大量库存导入,服务器的内存不足导致机器宕机。

    商家、运营无法通过导表的方式去维护库存数量,在这之前已经经历过了多次横向扩容。还是出现了预料之外的流量和稳定性的问题。
    在这里插入图片描述
    而且在接下来的大促过程当中,库存的单体服务 API 接口也承受了非常大的压力。

    一方面是上游调用方有很多,比如 APP 端首页中的门店网关,查询商品是否有库存,是否展示。

    购物车加车,也会查询商品库存的数量,提单则会对库存数量进行扣减,乃至后续的订单取消同样也会调用库存接口。

    另一方面大的 KA 商家通过中台对接对库存进行操作,为了尽可能的让商家门店的库存和线上平台的库存保持一致,减少线上线下库存不一致导致的超卖、少卖。

    中台同步间隔时间都非常短,5 分钟-10 分钟就要全量同步一次。后续随着入驻的商家增多,这个量级增长的也非常的迅速。于是我们开启了单体服务向分布式服务演进的大门。

    分布式架构

    分布式架构的优点

    1. 可用性高
    2. 可扩展性高
    3. 系统容错性高
    4. 业务代码可读性高
    5. 维护简单

    这些优点正是我们当时库存系统欠缺的,尤其是其中的可用性、系统容错性,是我们系统演进迭代的首要目标。

    《分布式架构体系》中描述到,分布式架构的核心理念也是 按照(功能、业务、领域等)对系统进行拆分,通过合理的拆分结构,实现各业务模块的解耦,同时通过系统级容错设计,在廉价硬件基础设施上构建起高可用、可扩展的开放技术体系。

    所以我们库存系统到底要按照什么进行拆分,功能?业务?领域?在拆分之前我们一定要明确设计的目标,避免目标方向错误带来的人力、成本资源的浪费。

    在弄清楚目标之前,我们先了解下分布式架构的缺点,通过了解这些缺点来衡量满足我们目标的前提下,需要进行哪些方面的取舍,就如 CAP 原则一样,只能满足其中的两个,AP 或者 CP。

    分布式架构的缺点

    1. 服务多,人员对拆分后的业务模块理解要花费一些成本
    2. 技术栈升级耗费人力
    3. 分布式事务的保持
    4. 业务模块之间的 rpc 交互损耗

    库存系统的特点,高可用、高并发、强数据一致性。接下来我们就来讲一下,库存是如何从单体架构向分布式架构进行的转型。

    单体架构如何向分布式架构转型

    库存系统由单体转为分布式架构

    因为库存面临的最大的问题是稳定性,所以我们首先针对功能进行了拆分。

    功能拆分

    这一步是相对简单的,我们梳理出库存面向服务的业务方进行服务划分。这部分无需进行太多代码的改造,一套接口通过变更不同的 group 别名,部署到不同的集群即可。

    在这里插入图片描述
    拆分后,不同的服务应对不同的业务方,系统错误的隔离性好,不会说出现一损俱损的局面,稳定性上也有了保障。

    在解决了稳定性的问题后,留给我们了一些喘气的间隔,可以有时间去进行代码的优化。

    因为刚才也提到了,我们只是通过分布式的集群部署来解决容错性的问题,但是代码还是一套,臃肿的代码也会拖慢我们的开发上线速度。

    那么接下来要进行的就是,对业务代码的解耦,这块也是难度最高的。我们是如何做的呢?

    业务拆分

    业务拆分的思路是什么呢?

    1. 以业务本身为导向,充分了解系统业务模型,划分业务边界
    2. 业务依赖的范围,细分功能,尽量减少功能之间的重复依赖
    3. 根据拆分功能的影响大小进行评估,拆小保大
    4. 拆分的过程中不要修改业务逻辑,不要进行拆分之外的任何优化动作(除非是 bug)

    基于上述拆分的思路,库存系统又是如何划分的业务模块呢?动了哪些代码?

    如何划分业务模块

    关于业务划分,网上有很多方法论,事件风暴法、四色建模法等等,但是万法不离其宗,那就是围绕事件。

    以库存系统举例:库存初始化(门店+sku 库存创建)、库存数量维护(修改现货数量、修改可售状态)、扣减业务(购物车扣减、提单扣减、订单取消扣减)、提醒业务(缺货提醒)等。每一个事件都有独立的链路轴,以及时间线可以形成闭环。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    如何在原有模块上拆分

    大多数单体架构都是面向过程的设计,domain 层充斥这个各种 DTO、VO、BO,所以在层与层的数据交互过程中,大都是经历了多次的 POJO。

    另外就是 service 层充斥着和 DAO 层数据交互以及参杂了业务,而且严重违反了依赖倒置原则,整个层变得非常的沉重。

    这里举个例子:

    1. 同层级间相互引用
    2. service 层包含了太多业务逻辑,无法保障原子性
      在这里插入图片描述
      这里截取部分代码片段作为案例,来讲述下我们在拆分业务的过程中,需要做一些什么操作。
      在这里插入图片描述
      如上图:
    3. 对 service 层进行 CQS 的拆分
    4. 把业务逻辑从原有的 service 层抽离,保障 service 方法遵循 SRP 原则
    5. 新增业务聚合层(或者向六边形架构里提到的 adapter 转接口)来聚合 service 层的方法

    原始代码:

    @Service
    public class SkuMainServiceImpl implements SkuMainService {
        private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(SkuMainServiceImpl.class);
    
        @Resource
        private SkuMainDao skuMainDao;
        @Resource
        private ZkConfManagerCenterService zkConfManagerCenterService;
        @Resource
        private ProductImagesService productImagesService;//同级互相引用,未遵循依赖倒置
        @Resource
        private MqService mqServiceImpl;
    
        @Value("${system.group.environment}")
        private String systemGroupEnvironment;
    
        /**
         * 问题:service层聚合了太多业务逻辑 倒置上层方法没办法统一
         * @param skuMainInfoMQEntity
         * @throws Exception
         */
        public void editorSaveProuct(SkuMainInfoMQEntity skuMainInfoMQEntity) throws Exception {
            try {
                SkuMainBean skuMainBean = skuMainInfoMQEntity.getSkuMainBean();
                if (skuMainBean == null) {
                    throw new Exception("修改参数为空!");
                }
    
                SkuMainBean originalSku = this.getSkuMainBeanBySkuId(skuMainBean.getId());
                if (originalSku == null) {
                    throw new Exception("无效SkuId!");
                }
    
                SkuMainBean skuMainUpdate = updateIsWeightMark(skuMainBean);
                SkuMainBean skuMainPre = this.get(skuMainUpdate.getId());
                // 系统下架的商品 强制下架
                if (skuMainPre != null && skuMainPre.getSystemFixedStatus() != null && skuMainPre.getSystemFixedStatus().equals(SystemFixedStatusEnum.SYSTEM_FIXED_STATUS_DOWN.getCode())) {
                    skuMainUpdate.setFixedStatus(FixedStatusEnum.PRODUCT_DOWN.getCode());
                }
    
                boolean flag = skuMainDao.editorProduct(skuMainUpdate);
                if (flag) {
                    if (!zkConfManagerCenterService.isDefaultStoreStatisticsScore(skuMainBean.getOrgCode())) {
                        SkuMainBean saveSkumainBean = this.get(skuMainUpdate.getId());
                        // 防止未查到,把缓存覆盖
                        if (saveSkumainBean != null) {
                            cacheSkuMainBean(saveSkumainBean);
                        }
    
                        // 发送Sku修改MQ
                        sendSkuModifyMq(SkuModifyOpSourceEnum.MIX_UPDATE_SKU, originalSku, new SkuMainInfoMQEntity(skuMainUpdate));
                        ProductImagesBean productImagesBean = productImagesService.queryImagesBySkuId(skuMainUpdate.getId());
                        SkuMainInfoCheckMQEntity skuMainInfoCheckMQEntity = new SkuMainInfoCheckMQEntity();
                        skuMainInfoCheckMQEntity.setSkuMainBean(skuMainUpdate);
                        skuMainInfoCheckMQEntity.setProductImagesBean(productImagesBean);
                        mqServiceImpl.sendJosMQ(skuMainInfoCheckMQEntity, MqTypeEnum.RcsKeyWordsCheck);
                        mqServiceImpl.sendJosMQ(skuMainInfoCheckMQEntity, MqTypeEnum.SenseKeyWordsCheck);
                    } else {
                        LOGGER.info("add open platform sku , not not not send mq! skuId = {}", skuMainBean.getId());
                    }
                }
            } catch (Exception e) {
                LOGGER.error("修改商品信息失败.e:", e);
                throw new Exception(e);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    CQS 和 SRP 的改造,拆解 GOD Classes:

    (1)Read 服务
    在这里插入图片描述
    (2)Write 服务
    在这里插入图片描述
    抽离到业务层 business 层后:

    @Service
    public class SkuMainBusinessServiceImpl implements SkuMainBusinessService {
        private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(SkuMainBusinessServiceImpl.class);
    
        @Resource
        private ZkConfManagerCenterService zkConfManagerCenterService;
        @Resource
        private MqService mqService;
        @Resource
        private SkuMainReadservice skuMainReadservice;
        @Resource
        private SkuMainWriteservice skuMainWriteservice;
    
        @Value("${system.group.environment}")
        private String systemGroupEnvironment;
    
        /**
         * 问题:service层聚合了太多业务逻辑 倒置上层方法没办法统一
         * @param skuMainInfoMQEntity
         * @throws Exception
         */
        public void editorSaveProuct(SkuMainInfoMQEntity skuMainInfoMQEntity) throws Exception {
            try {
                SkuMainBean skuMainBean = skuMainInfoMQEntity.getSkuMainBean();
                if (skuMainBean == null) {
                    throw new Exception("修改参数为空!");
                }
                SkuMainBean originalSku = skuMainReadservice.getSkuMainBeanBySkuId(skuMainBean.getId());
                if (originalSku == null) {
                    throw new Exception("无效SkuId!");
                }
                SkuMainBean skuMainUpdate = skuMainWriteservice.updateIsWeightMark(skuMainBean);
                SkuMainBean skuMainPre = skuMainReadservice.queryDbById(skuMainUpdate.getId());
                // 系统下架的商品 强制下架
                if (skuMainPre != null && skuMainPre.getSystemFixedStatus() != null && skuMainPre.getSystemFixedStatus().equals(SystemFixedStatusEnum.SYSTEM_FIXED_STATUS_DOWN.getCode())) {
                    skuMainUpdate.setFixedStatus(FixedStatusEnum.PRODUCT_DOWN.getCode());
                }
                boolean flag = skuMainWriteservice.editorProduct(skuMainUpdate);
                if (flag) {
                    if (!zkConfManagerCenterService.isDefaultStoreStatisticsScore(skuMainBean.getOrgCode())) {
                        SkuMainBean saveSkumainBean = skuMainservice.queryDbById(skuMainUpdate.getId());
                        // 防止未查到,把缓存覆盖
                        if (saveSkumainBean != null) {
                            skuMainWriteservice.cacheSkuMainBean(saveSkumainBean);
                        }
                        // 发送Sku修改MQ
                        skuMainWriteservice.sendSkuModifyMq(SkuModifyOpSourceEnum.MIX_UPDATE_SKU, originalSku, new SkuMainInfoMQEntity(skuMainUpdate));
                    } else {
                        LOGGER.info("add open platform sku , not not not send mq! skuId = {}", skuMainBean.getId());
                    }
                }
            } catch (Exception e) {
                LOGGER.error("修改商品信息失败.e:", e);
                throw new Exception(e);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    拆分小结

    拆分到这里,业务层的划分基本就比较清晰了,而且在这个增量整合底层代码的过程中,面向过程的业务线也都梳理的比较清晰了,底层方法也都提取到了业务层收口,通过接口对外提供服务。那么接下来我们要面临的问题就是,如何对具体的读写进行拆分。

    分布式的事务

    我们大家都知道事务,简单来说:事务由一组关联操作构成,A->B->C ,如果执行到 C 报错了,那么要回滚 B->A。

    对于本地事务来说,这个相对很简单,如果你用了事务型数据库比如 mysql,并且不涉及多个数据源的情况下,保障事务的 ACID 非常的容易。

    但是我们这里要提到的就是分布式的事务。因为系统拆分后,每个服务是一个独立的模块,负责一块业务,那么在整个业务轴的流程下,各个服务节点的跨系统事务回滚成为了一个难题。

    业界也有一些方案,比如:JTA(Java Transaction API 即 Java 事务 API)和 JTS(Java Transaction Service 即 Java 事务服务),为 J2EE 平台提供了分布式事务服务。

    但是这种需要满足 XA(两阶段提交)的标准,非常的重,而且现在的业务多样性,很多数据库比如:mongo ,并不支持 XA 的标准分布式事务,一些流行的中间件,比如 RabbitMQ 和 Kafka 也不支持分布式事务。

  • 相关阅读:
    008_第一代软件系统架构
    焊接--PCBA
    ArcGIS Pro 转换Smart3D生成的倾斜3D模型数据osgb——创建集成网格场景图层包
    字典类型和字典函数、字典方法
    盘点AI的认证
    python 批量使图片重新排序
    JVM面试点汇总
    云服务器租用价格表概览_阿里云腾讯云华为云
    杰理之发射器相关的 API【篇】
    一些常用到的git命令
  • 原文地址:https://blog.csdn.net/weixin_44427181/article/details/125907997