一、背景
单体架构:性能瓶颈
单表存储:存储瓶颈
开发效率:成本
运维困难:发现问题、解决问题的效率和成本
服务化改造势在必行。
二、系统架构
订单模块从商城拆分出来,独立为订单系统,使用独立的数据库,为商城提供订单、支付、物流、售后等标准化服务。
系统架构如下图所示:
三、技术挑战
1、数据量问题
历史订单不断累积,MySQL中订单表数据量已到达千万级。
数据量大的解决方案:
- 数据归档:冷热数据分离,冷数据(3个月以上的订单历史数据)进行归档。
- 分库分表
2、高并发问题
单机的处理能力是有限的,高并发的解决方案有:
-
使用缓存:订单系统有个特点,每个用户的订单数据都不一样,所以缓存命中率不高,效果不太好。
-
读写分离
-
订单数据的更新操作较多,下单高峰期主库的压力依然没有得到解决。
-
存在主从同步延迟,正常情况下延迟非常小,不超过1ms。
-
分库:垂直分库、水平分库
-
分表:垂直分表、水平分表
3、分库分表技术选型
- 应用层开源类库
- ORM框架扩展
- 代理层组件
采用开源的ShardingJDBC方案
分库分表策略
选取用户标识作为分片键,假设有n个库,每个库有m张表。
- 库序号:hash(userId) / m % n
- 表序号:hash(userId) % m
路由过程如下图所示:
分表分库的局限性和应对方案
极大限制数据库的查询能力:一些简单的关联查询可能无法实现(需要制定合适的分库分表策略,使得主表和从表的数据都落在同一个数据库中)
全局唯一ID设计
很多内部系统的交互接口只有订单号,没有用户标识这个分片键,如何用订单号来找到对应的库表呢?在生成订单号时,就将库表编号隐含在其中。在没有用户标识的场景下,从订单号中获取库表编号。
历史订单号没有隐含库表信息
用一张表单独存储历史订单号和用户标识的映射关系,随着时间推移,这些订单逐渐不在系统间交互,就慢慢不再被用到。
管理后台需要根据各种筛选条件,分页查询所有满足条件的订单
将订单数据冗余存储在搜索引擎ES中,仅用于后台查询。
4、怎么做到MySQL到ES的数据同步
双写策略
写完数据库后写ES,可以通过异步解耦。
消息队列
ES更新服务作为消费者,接收订单变更消息后对ES进行更新。
Binlog
ES更新服务借助Canal等开源项目,把自己伪装成MySQL的从节点,接收binlog并解析得到数据变更信息,然后根据变更信息去更新ES。
选择消息队列方案,因为其相对简单,针对极端情况下的消息丢失,可以通过手动同步ES的功能解决。
5、如何安全地更换数据库
不停机迁移方案
- 复制全量旧库数据到新库,然后复制旧库的增量数据到新库。
- 上线检测程序比较新旧数据库的数据是否一致,有则进行修正补偿。
- 应用上线双写程序,以写旧库为准,只读取旧库数据;停止增量同步程序,检测程序继续检查新旧数据库的数据是否一致。
- 应用逐步将读请求切至新数据库。
- 应用以写新库为准,检测程序继续检查新旧数据库的数据是否一致。
- 应用下线双写程序,只读写新数据库;下线检测程序、全量/增量同步程序。
停机迁移方案
- 上线新订单系统,将旧数据库的全量数据同步到新数据库,检测新旧数据库的数据是否一致。
- 商城v1应用停机,确保旧库数据不会再发生变化。
- 执行增量同步程序,将第一步未迁移的订单同步到新数据库。
- 商城v2应用接入流量,开始验证,如果失败则回滚到商城v1应用。
考虑到不停机方案的改造成本太高,而夜间停机方案的业务损失不大,所以选择停机方案。
6、分布式事务问题
不同业务场景对数据一致性的要求不同,业界的主流方案中,
解决强一致性的有:两阶段提交(2PC)、三阶段提交(3PC)
解决最终一致性的有:TCC、消息队列+本地消息表、事务消息、最大努力通知和SEATA等。
本地消息方案相对简单,依赖第三方组件少,所以选择本地消息方案。
在本地事务中将要执行的异步操作记录在消息表中,如果执行失败,可以通过定时任务来补偿。
下图以订单完成后通知积分系统赠送积分为例。
7、系统安全和稳定性
网络隔离
只有极少数第三方接口可通过外网访问,且都有验证签名,内部系统交互使用内网域名和RPC接口。
并发锁
通过数据库的行级锁加以限制,防止出现并发更新的问题;此外还使用行记录的版本号避免延时到达的旧数据覆盖新数据的问题。
幂等性
在提交订单前先获取订单号,提交时根据订单号进行去重,避免重复下单。
限流
单机限流、分布式限流
降级、熔断
Sentinal、Hystrix
监控和告警
系统监控:各种资源的使用率
调用链监控:接口RT、QPS、异常数量、异常比例
分布式日志系统:查看异常
8、踩过的坑
订单数据同步到ES的数据一致性问题
左图为原方案:线程A会将订单的旧数据覆盖线程B写入的订单新数据。
右图为新方案:线程A读取订单数据时会加行锁,线程B阻塞,线程A写入ES后线程B再写入。
ES分页查询时漏掉数据或者重复数据
如果排序字段存在重复的值,最好加一个唯一的字段作为第二排序条件,避免分页查询时漏掉数据、查出重复数据。
比如用的是订单创建时间作为唯一排序条件,同一时间如果存在很多数据,就会导致查询的订单存在遗漏或者重复。
四、结语
系统设计时并没有一味追求前沿技术和思想,面对问题时也不是直接采用主流的解决方案,而是根据业务实际情况来做出取舍,将合适的方案落地。
一个系统避免从一开始就过度设计,需是要遵循简单原则、合适原则、演进原则。