电商系统服务拆分实战
技术发展到这个年代,微服务几乎是标配,人们对单体应用的概念反而模糊了。但整个演变过程还是要稍微过一下。
一个系统最开始的模样很可能就是个单体,tomcat一启动就是整个系统。
一台机撑不住了便需要横向扩展加机器,开始了分布式的第一步。这时需要保持用户登录态,便有了集中式会话管理,方案若干种,会话粘连、ip哈希、tomcat集群同步、集中式,现在基本统一成spring-session-redis。
慢慢地前端也分离出来了,在nginx反向代理的基础上做静态加速,页面交给nginx和CDN,动态数据转发给后端。
逐渐地,数据库成了瓶颈,于是把压力分出去,列表查询交给es,库存查询交给redis等等。
随着业务的增长,服务器一个大单体撑不住了,光是加机器也不行,代码臃肿、启动很慢、各业务线变更冲突多,开发效率和运行效率都低下,需要把整个应用拆小出来了。
拆分目标
拆分时也要考虑其副作用的,比如拆分模块多了代码不好找、比如多几层转发和对象复制影响开发效率等等。因此拆分要紧扣这目标逐步展开。
模块复用:拆分过程中要尽量沉淀出可复用的基础模块,例如用户模块、商品模块、订单模块等等,这些基础的轮子能有效提高后续新业务的开发效率。
解耦:比方说商品加一个字段会导致交易下单代码会报错必须跟着改,这种是严重且不合理的耦合,因而要尽量把拆分出来的模块的职责边界定义清楚。
扩展性:一般来说微服务不会有扩展性问题,也就是说一个服务拆出来一般是无状态的可以简单通过加机器扩展的,而相应的数据库也要适当考虑数据规模与扩展。
中台化:中台建设和服务拆分不是同个维度的话题,但服务拆分和模块可编排可配置是中台的基础,因而在服务拆分过程中可以适当考虑配置化,为后续做铺垫。
拆分实战
总览与方向
总览图如下,是现阶段分后的大概划分。
拆分前的商品系统和交易系统是两个大“单体”,代码的包结构有的按端划分、有的按域模块划分、有的按不同行业业务划分,十分混乱。
拆分是个漫长演进过程,大体上会按三个方向推进:
水平方向按域拆分,即会员、商品、商户、交易与订单、内容、营销、TMS、WMS、库存等等。
上层业务则逐渐拆分出业务编排和行业端等模块,组织聚合各域基础服务,承载上层差异化的业务。
垂直方向可进一步分层拆分,例如商品分层出商品管理、行业维度商品聚合、搜索与缓存等。
本期重点讨论的是商品服务的拆分,如下图所示。
业务梳理过程
服务拆分前最重要的便是梳理出主要业务,以及模块边界。具体以商品服务来讨论的话:
商品主要有商品管理CRUD、查询服务(按编码,主要供信息聚合)、类目、规格与属性、上下架、消费者端的列表与详情(搜索与缓存)、库存、价格。
其中,商品SKU、SPU、类目、规格与属性是比较通用的能力,适合拆分出来。
上下架、搜索与缓存等比较模棱两可,要视具体业务而定,要看具体中间件是否能打通,要看搜索的文档是否足够通用,而答案往往是否定的,不同的行业不同端的列表组织方式并不通用。
至于库存与价格,我本人是强烈反对放到商品服务的,业务单一的初期无所谓,可当业务足够丰富了之后,光库存二字就可能是自己一个庞大的系统。且不提供应链中的仓库存、供应库存、在途库存,光是讨论销售库存,也都还区分SPU库存、SKU库存、商家销售库存、区域门店销售库存,B2C模式用的是商家销售库存,O2O模式关心的却是门店库存,跟业务关联十分紧密。价格也类似。
因此,确定下来模块的主要能力是SKU、SPU、类目、属性的基础服务。
商品主维度
业务方面这里可以展开多讲一点,关于商品主维度。(当然了,如果业务足够单一,也不需要这方面的讨论。)商品主维度要讨论的是,如何唯一确定一个商品。
首先是商品在各生命周期中的变化:后台发布商品、往上游是供应商与采购的商品(也可以理解为批次)、往下游下单时会生成商品快照。那么,上一批采购跟这一批在另一供应商采购的商品是否同个商品呢?这个例子倒没有太大争议,是同一个基础商品,但是是不同供应商不同批次的商品。
再者是行业模式特性的变化:比如特卖分早晚两档,两档有各自的促销标语和价格,又比如两个门店卖同一款商品,但图片和价格又不太一样。所以哪个维度才是基础商品呢?如果我整个平台都是特卖的分场次的,以场次商品作为商品的主要维度又有何不可。当然了,分成基础商品和场次商品两层会更合理点,基础商品还可以支撑其他模式。
所以,最极端情况下,商品是需要支撑一品多商(商家和门店) 、一品多供(供应商)、一品多区(区域,常见于快消行业)、一品多码(条形码,例如新旧包装随机发货)的不同维度关联,这个倒没有标准答案的,选择适合自己行业的维度和复杂度即可。
开发过程
编码的套路有两种思路可供参考。
如果是原有代码本来就有包拆分和模糊的模块概念,那么只要把相关的包迁移出来即可,像商品这种偏固定的、不太有流程变化的模块特别适合。
另一种则相对通用,是先按上一步梳理出对应的功能进一步细化成服务和接口,上层对接口接入和适配,下层则对接口做实现。
等价替换
服务拆分其实是一种重构,也就是说对上层业务来说是等价替换的。因此,单元测试可以做对比测试,集成测试可以直接跑自动化测试。
选择合理的方式真的可以让单元测试又快又稳。如图,将服务暴露成不同的版本(这里使用的是dubbo,用version和group都可以做版本隔离),便可以在同个用例调用中直接对比调用结果。而结果对照甚至可以直接用json的equals方法比对,字段是有稳定顺序的。
不过主要还是用于幂等接口例如查询,对于插入操作仍需要人肉比对。
数据迁移
数据迁移也是个头疼的问题。这里探讨几种不同级别的迁移方案。
方案1.0,停服务迁移。完全停掉相关服务,mysql数据全量迁移(全量很快的就几分钟)。
方案1.1,忽略丢数据直接迁移。认为这段时间不会有操作(新建商品确实很低频),直接全量复制直接发布重启。
方案1.2,停部分服务迁移。用动态开关控制相关服务都降级为不可用,然后数据迁移,然后发布重启,然后关闭降级开关从而恢复服务。
以上方案1系列都会有明显的不可用时间,不严谨,但优点是操作简单。回滚也简单。
方案2,数据库增量同步。连同全量数据全都当成增量,持续同步到新库,直到服务发版,旧库不再更新。正向发版倒是比较严谨,只要处理下id生成器不要冲突即可。逆向回滚时很麻烦,得专门考虑补增量数据。
方案3,双写,更新操作在新旧库都要写入(可开关控制),查询操作在新库查询(可开关切换)。这种方案要解决三个问题,一个是双写部分失败怎么办,不存在事务回滚这一说,恐怕只能人工排查补救;第二是,全量数据补齐时是否会与增量冲突,这个也是需要处理下id生成器。第三是非幂等的接口重复操作的问题,例如库存保存在redis上且每次都是加1减1这种增量操作,如果双写两边都加1可能导致最后结果是加2,需要控制只写入一次。回滚则简单了,只需要开关控制。
行业数据隔离
不同行业的商品数据是有不同特性的:比如服装快时尚行业每天上新2000个sku;比如快消行业,同一个基础商品,可能会衍生出不同区域不同渠道门店,而访问的热度可能是跟着商品走也可能是跟着渠道走;又比如生鲜行业,商品的访问热度跟季节有很强关联。
随着行业模式增多(这个倒是很慢),以及行业内商品数据的增多,必定要面临进一步的拆分和加速。可以从两个角度考虑。
第一是按行业做拆分,比如按行业分库。
第二是在具体行业内,配置分表策略和缓存策略,比如生鲜行业,按id或时间哈希可以达到分散热点的目的,而缓存则应该按季节标或促销标。(不过,缓存还是尽可能是上层业务自己做。)
因此,表设计的时候可以预留行业字段可用于分库、热点字段可用于分表。当然了,这个也可以往后再加,视具体业务。