当前位置: 首页 > 后端技术 > Java

订单中心架构设计与实践

时间:2023-04-02 00:09:05 Java

不同的业务采用不同的系统架构,有自己独特的架构问题。今天我们就来了解一下电商业务中订单中心的架构设计,会遇到哪些技术挑战。一、背景随着用户的快速增长,vivo官方商城v1.0单体架构的劣势逐渐暴露出来:模块越来越臃肿,开发效率低下,性能瓶颈,系统维护困难。2017年开始的v2.0架构升级,基于业务模块的系统垂直物理拆分,拆分后的业务线各司其职,提供面向服务的能力,共同支撑主站业务。订单模块是电子商务系统的交易核心。积累的数据快要达到单表存储的瓶颈了。新品发布、促销活动期间流量难以支撑,服务化转型势在必行。本文将介绍vivo商城订单系统建设中遇到的问题及解决方案,分享架构设计经验。2、系统架构订单模块与商城分离,是一个独立的订单系统,使用独立的数据库为商城相关系统提供标准化的订单、支付、物流、售后等服务。系统架构如下图所示:3.技术挑战3.1数据量和高并发问题面临的第一个挑战来自存储系统:数据量问题随着历史订单的不断积累,数据量不断增加MySQL中的订单表的数量已经达到了数千万。我们知道InnoDB存储引擎的存储结构是B+树,查找时间复杂度是O(logn)。因此,当数据总量n变大时,检索速度必然会变慢。无论如何使用索引或优化,都无法解决。我们只能想办法减少单表的数据量。大数据量解决方案包括:数据归档、分表、高并发问题。商城业务正处于高速发展时期。订单量屡创新高。业务的复杂度也越来越高,访问MySQL的应用越来越多。单机MySQL的处理能力是有限的。当压力过大时,所有请求的访问速度都会下降,甚至可能导致数据库宕机。高并发方案包括:使用缓存、读写分离、分库。下面简单介绍一下这些解决方案:数据归档订单数据有时间属性,有热尾效应。大多数情况下,取最近的订单,而订单表中存储了大量使用频率低的旧数据。那么可以将新旧数据分开存储,将历史订单移到另一个表中,在代码中对查询模块做一些相应的改动,可以有效解决数据量大的问题。使用缓存使用Redis作为MySQL的前置缓存,可以阻断大部分查询请求,减少响应延迟。缓存对于和用户关系不大的商品系统特别有效,但是对于订单系统,每个用户的订单数据都不一样,缓存命中率不高,所以效果不是很好。读写分离主库负责执行数据更新请求,然后将数据变化实时同步到所有从库,使用多个从库共享查询请求。但是订单数据的更新操作比较多,订单高峰期主库的压力还没有解决。并且存在主从同步延迟。一般情况下延迟很小,不会超过1ms,但是也会导致某个时刻主从数据不一致。那么需要兼容所有受影响的业务场景,可能会做一些妥协,比如下单成功后跳转到成功下单页面,用户手动点击查看订单后才能看到订单。分馆和分馆包括垂直分馆和水平分馆。①水平分库:将同一张表的数据按照一定的规则拆分到不同的数据库中,每个数据库可以放在不同的服务器上。②垂直分库:按照业务对表进行分类,分布到不同的数据库中。每个数据库都可以放在不同的服务器上。它的核心概念是致力于特殊的数据库。分表和分表包括垂直分表和水平分表。*①水平分表:*在同一个数据库中,将一张表的数据按照一定的规则拆分成多张表。*②垂直分表:*将一个表按照字段分成多个表,每个表存储一部分字段。我们综合考虑改造成本、效果和对现有业务的影响,决定直接使用最后一招:分库分表3.2分库分表技术选型分库分表技术选型-table主要从这几个方向考虑:Clientsdk开源方案middlewareproxy开源方案公司中间件团队提供自研框架,自己造轮子。参考之前的项目经验,与公司中间件团队沟通,采用了开源的Sharding-JDBC方案。它已更名为Sharding-Sphere。Github:https://github.com/sharding-s...文档:官方文档比较粗糙,但是网上资料、源码分析、demo比较丰富。社区:活跃特点:以jar包形式提供,属于客户端分片,支持xa事务3.2.1分库分表策略结合业务特点,选择用户ID作为shardkey,计算hash值用户ID取模得到用户订单数据的数据库表号。假设一共有n个数据库,每个数据库有m张表,则数据库表号的计算方法为:数据库序号:Hash(userId)/m%n表序号:Hash(userId)%m的路由流程如下图所示:3.2.2分库分表分库分表的局限和对策解决了数据量和并发的问题,但是会极大的限制数据库的查询能力.一些以前很简单的相关查询,分库分表后可能无法实现,那么就需要单独重写这些Sharding-JDBC不支持的SQL。此外,还遇到了这些挑战:(1)分库分表设计全局唯一ID后,数据库自增主键不再全局唯一,不能作为订单号使用,而是很多内部系统之间的交互接口只有订单号,没有用户ID的shardkey,如何利用订单号找到对应的库表呢?原来,当我们生成订单号时,我们隐式地将图书馆表号包含在其中。这样在没有用户ID的场景下,可以从订单号中获取库表号。(2)历史订单号不代表库表信息。使用单表存储历史订单号和用户ID的映射关系。随着时间的推移,这些命令逐渐不在系统间交互,逐渐不再被使用。(3)管理后台需要根据各种过滤条件查询页面中所有符合条件的订单,并将订单数据冗余存储在搜索引擎Elasticsearch中,仅供后台查询使用。3.3如何做MySQL到ES的数据同步。上文提到,为了方便后台查询的管理,我们将订单数据冗余存储在Elasticsearch中。那么,MySQL订单数据发生变化后,如何将订单数据同步到ES呢?这里需要考虑的是数据同步的及时性和一致性,对业务代码的侵入最小,不影响服务本身的性能。MQ解决方案ES更新服务作为消费者在收到订单变更MQ消息后更新ES。Binlog解决方案ES更新服务利用canal等开源项目伪装成MySQL从节点,接收Binlog并进行分析,获取实时数据变化信息。然后根据这个变化信息更新ES。其中BinLog方案比较通用,但是实现起来也比较复杂。我们最终选择了MQ方案。因为ES数据只是用于管理后台,所以对数据可靠性和实时同步的要求不是特别高。考虑到宕机、消息丢失等极端情况,后台增加了根据一定条件手动同步ES数据的功能来弥补。3.4如何安全更换数据库如何将数据从原来的单实例数据库迁移到新的数据库集群也是一大技术挑战。不仅要保证数据的正确性,还要保证每一步执行完后,一旦出现问题,能够快速回滚到上一步。我们考虑过宕机迁移和非宕机迁移两种方案:(1)不停机迁移方案:将旧数据库的数据复制到新数据库,启动同步程序,使用Binlog等方案同步数据旧数据库实时更新到新数据库。新旧库双写单服务上线,只读旧库。开启双写,同时停止同步程序,启动对比补偿程序,确保新库数据与旧库数据一致。逐渐将读取请求切换到新库。所有读写切换到新库,对比补偿程序保证旧库数据与新库数据一致。离线旧库、离线订单双写功能、离线同步程序、对比补偿程序。(2)停机迁移计划:启动新订单系统,执行迁移程序将两个月前的订单同步到新数据库,并进行数据审计。停止商城V1应用,保证旧数据库数据不发生变化。执行迁移程序,将第一步未迁移的订单同步到新库并进行审计。启动商城V2应用,开始测试验证,失败则回退到商城V1应用(新订单系统有开关双写老库)。考虑到不停机方案改造成本较高,夜间停机方案业务损失不大,最终选择了停机迁移方案。3.5分布式事务问题在电子商务的交易过程中,分布式事务是一个经典的问题。例如,用户支付成功后,需要通知配送系统将商品配送给用户。用户确认收到商品后,需要通知积分系统为用户发放购物奖励积分。微服务架构下如何保证数据的一致性?不同的业务场景对数据的一致性有不同的要求。业界主流方案中,采用两阶段提交(2PC)和三阶段提交(3PC)来解决强一致性,以及TCC、本地消息、Transactional消息和besteffortnotifications等,这里不再赘述详细描述上面的方案,但是介绍一下我们正在使用的本地消息表方案:将本地事务中要执行的异步操作记录在消息表中,如果执行失败,可以通过定时任务进行补偿。下图是订单完成后通知积分系统赠送积分的例子。3.6系统安全稳定网络隔离只有极少数第三方接口可以通过外网访问,全部会验证签名。内部系统通过内网域名和RPC接口进行交互。并发锁在任何顺序更新操作之前,都会受到数据库行级锁的限制,以防止并发更新。幂等性所有接口都是幂等的,不用担心对方网络超时重试的影响。Fuse使用Hystrix组件对外部系统的实时调用加入熔断保护,防止系统故障的影响扩大到整个分布式系统。监控告警通过配置日志平台的错误日志告警、调用链的业务分析告警、公司各中间件和基础组件的监控告警功能,第一时间发现系统异常。3.7踩坑将数据库的订单相关数据以MQ消费的方式同步到ES,遇到写入的数据不是订单的最新数据先执行,找出数据。此时订单数据更新,线程B开始执行同步操作。找到订单数据后,先线程A一步写入ES,当线程A执行写入时,会写入线程B,导入的数据被覆盖,所以ES中的订单数据不是最新的。解决方案是在查询订单数据的时候加一个行锁,整个业务都在一个事务中执行,执行完再执行下一个线程。sharding-jdbc对所有数据分组后排序分页查询问题示例:selectafromtempgroupbya,borderbyadesclimit1,10。执行是Sharding-jdbc中的groupby和orderby字段顺序不一致,将10设置为Integer.MAX_VALUE,导致分页查询失败。io.shardingsphere.core.routing.router.sharding.ParsingSQLRouter#processLimit的正确写法应该是selectafromtempgroupbyadesc,blimit1,10;使用的版本是sharing-jdbc3.1.1。ES分页查询如果排序字段有重复值,最好加一个unique字段作为第二个排序条件,避免分页查询时漏数据和检测重复数据。比如订单创建时间作为唯一的排序条件,相同时间的数据如果很多,会造成查询到的订单有遗漏或者重复。需要添加唯一值作为第二个排序条件或者直接使用唯一值作为排序条件。4、结果一次性成功上线,稳定运行一年多。核心服务性能提升十余倍。系统解耦,迭代效率显着提升,至少可以支撑商城高速发展五年。5.结语我们在系统设计上没有盲目追求前沿的技术和思想,面对问题也没有直接采用主流的电子商务解决方案,而是根据实际业务情况选择最合适的方法。个人认为一个好的系统不是大牛一开始设计出来的,一定是随着业务的发展演进逐步迭代,不断预判业务发展方向,提前制定架构演进方案。简单的说就是:走到业务的前面!