亿级订单系统分库分表技术方案和Flink数据同步方案

4,904 阅读14分钟

订单系统分库分表技术方案

一、需求背景

随着公司业务的发展,订单系统的数据量和访问量日益增长,单库单表的架构已经无法满足我们的需求。主要面临以下问题:

  1. 数据量大:单一数据库存储所有订单数据,导致数据量过大,影响查询效率。
  2. 并发压力大:大量用户同时访问系统,产生高并发请求,对数据库造成较大压力。
  3. 扩展性差:当需要对订单表进行改动时,大量的数据造成表结构修改时间变长。

二、业务现状

订单系统目前面临以下几个关键问题及相应的解决思路:

问题类型当前状况解决方案
主键设计问题订单表有主键orderId和唯一索引orderNo,orderId依赖数据库自增,orderNo自定义生成并且后4位为类用户id,内部场景用orderId进行数据传递,外部场景用orderNo进行数据传递。去除数据库自增,将orderId和orderNo进行合并,将自定义编号作为唯一主键,内外部场景全部采用orderNo进行数据传递。
索引过多订单表超过10个索引,部分索引用于C端场景,部分索引用于B端场景。将OLTP中的订单数据同步到OLAP数据库中;确保所有C端场景能够命中分库分表的分片键;将B端分析场景进行剥离,例如查询订单列表,在OLAP数据库中进行查询,再根据订单主键CRUD时,再走OLTP数据库。
历史数据不规范订单表中存在部分老数据,老orderNo没有采用后4位是类用户id,并且老orderNo中可能存在字母。借助Flink将老订单编号数据进行清洗(数据清洗技术方案另行商议),统一采用现有订单编号规则。
关联表设计不一致订单表存在一些拓展表,比如订单位置、订单申诉记录等,有些采用orderNo进行关联,有些采用orderId进行关联。统一采用主键进行关联,并且需要保证关联字段为分片键。如果有分库,最好将拓展表也进行分库分表,并且需要保证拓展表的分库分表规则和原表一致。
表结构冗余订单表中字段过多,有计费、开票、退款等非核心数据。考虑纵向拆分,可以从以下几个方面考虑:是否核心、更新是否频繁、字段长度是否过大。

三、技术选型

1. 分库分表和分布式数据库对比

理解不同类型数据库的特点

  • 普通数据库:通常采用传统的垂直架构,由单一的数据库服务器提供数据存储和查询服务。
  • 分布式数据库:采用水平分片的方式,将数据分散到多个数据库服务器上,实现数据的高可用性和可扩展性。
  • 云原生数据库:基于云计算技术构建,具有容器化、微服务等特性,可以快速部署和扩展。
  • 云原生分布式数据库:结合了分布式数据库和云原生数据库的特点,实现高性能、高可用、可扩展的数据库服务。
产品名称PolarDB-分区表OceanBasePolarDB-XTIDB传统分库分表
开发团队阿里-阿里云团队阿里-蚂蚁团队阿里-阿里云团队PingCAP团队根据具体实现和需求可能有多个开发团队参与
类型云原生数据库分布式数据库云原生分布式数据库分布式数据库传统数据库
应用场景HTAP(行列混合)HTAP(行列混合)OLTP(行存索引)HTAP(行列混合)OLTP(行存)
产品优势一写多读、共享分布式存储、计算与存储分离、自动读写分离、高速链路互联、数据可靠性和强一致性、维护成本很低、支持列存索引可满足金融级容灾标准、水平扩展、支持多租户资源隔离、支持列存索引、支持空间索引支持数据强一致性、支持水平扩展、列存索引(灰度中)水平扩展、实时HTAP、行列混合灵活性强、不需要进行数据库迁移
产品劣势分区算法只能采用单一key、行存节点进行了分区,列存节点也会分区、分区表不支持空间类型成本比PolarDB贵一倍、涉及到数据库迁移成本较高、涉及到数据库迁移、列存索引还在灰度中、OLAP依赖并行计算、目前不支持mysql8.0不属于阿里云体系、运维成本较高、涉及到数据库迁移、不支持空间类型考虑分布式事务兼容性、需要集成中间件
分片原理推荐使用分区表代替分库分表基于分布式技术的分片、无共享架构实现数据的分散存储和处理。基于MySQL内核的分片、通过特定的存储计算分离架构实现数据的分散存储和处理。基于TiKV内核的分片、通过Raft协议实现数据的一致性复制和分散存储。根据业务需求和自定义的分片规则进行数据分散存储和处理;可以根据具体的实现方式采用不同的分片算法和策略。
适用场景单一分片键场景、需要HTAP能力、对维护成本敏感金融级应用、多租户场景、需要高可用和强一致性大型OLTP系统、需要水平扩展、对一致性要求高需要HTAP能力、开源社区活跃、需要水平扩展成本敏感、技术栈固定、需要灵活配置
文档地址PolarDBOceanBasePolarDB-XTIDBShardingSphere

2. 分库分表方案对比

中间件名称ShardingSphere-JDBCShardingSphere-ProxyMycat
类型客户端分表数据库代理数据库代理
开发团队ApacheApacheMycat社区
优势轻量级、易于集成、支持多种数据源、提供分布式事务和读写分离功能功能丰富、支持多种数据源、提供分布式事务、读写分离、分布式主键生成等功能、业务无侵入、支持异构语言开源免费、功能完善、支持多种数据源、提供分布式事务、读写分离、分布式序列等功能、适用于各种项目
劣势代码改造、集成分布式事务困难、配置较为繁琐性能损耗略高、需要单独部署维护社区支持有限、维护和更新不及时、性能损耗略高、需要单独部署维护
适用场景对性能要求较高,代码侵入性可接受的OLTP应用支持异构语言,独立于应用程序部署,适用于OLAP应用以及对分片数据库进行管理和运维的场景开源免费,适用于中小规模项目或预算有限的场景
总结适用于对性能要求较高,代码侵入性可接受的OLTP应用支持异构语言,独立于应用程序部署,适用于OLAP应用以及对分片数据库进行管理和运维的场景开源免费,但社区支持有限、维护和更新不及时。独立于应用程序部署,适用于OLAP应用以及对分片数据库进行管理和运维的场景

四、唯一ID方案

在分库分表环境中,设计合适的唯一ID生成方案至关重要。以下是几种常见方案的比较:

1. 数据库自增或Redis自增

  • 优点:实现简单,易于理解
  • 缺点:单点风险、单机性能瓶颈、会暴露业务量
  • 适用场景:小规模系统,数据量不大且增长缓慢的场景

2. Snowflake算法

Snowflake算法结构

组成部分

  • 1位符号位:始终为0
  • 41位时间戳:精确到毫秒,可使用69年
  • 10位工作机器ID:可支持1024个节点
  • 12位序列号:同一毫秒内可生成4096个ID

特点分析

  • 优点:高性能高可用、易扩展、ID有序
  • 缺点:需要独立中心节点,时钟回拨可能造成ID重复、没有业务标识
  • 适用场景:高并发分布式系统,每秒生成数万ID的场景

3. UUID/随机算法

  • 优点:简单、无需中心化节点
  • 缺点:生成ID较长,有极小重复几率,不利于索引性能
  • 适用场景:对ID长度不敏感,且对性能要求不高的系统

4. 美团分布式ID生成-Leaf

实现方式

  • Leaf-snowflake:通过集群部署,自动剔除时钟回拨的节点,避免ID重复
  • Leaf-segment:在数据库自增基础上改进,加入批量生成和本地缓存机制

特点分析

  • 优点:高可用、解决自增和Snowflake部分缺点
  • 缺点:弱依赖ZooKeeper,需要独立部署Leaf系统
  • 适用场景:大型分布式系统,对ID生成有高可用要求的场景

5. 自定义业务ID

示例格式:订单类型(1) + 业务类型(1) + 时间戳yyMMddHHmmss(12) + 随机数(4) + 类用户id后4位

特点分析

  • 优点:单机生成、含业务属性、含用户标识、有顺序性
  • 缺点:过万QPS情况下容易出现ID冲突
  • 适用场景:中小型系统,对ID可读性有要求的业务

五、分片键和分片策略选择

1. 时间范围(Range)

特性说明
适用场景数据有明显的时间属性,例如日志表、记录表、统计表等
优点天然分片,好扩展,方便范围查询和排序操作,也可以方便数据归档
缺点数据可能分布不均匀,易引起单机负载过大的问题

2. 租户ID(List)

特性说明
适用场景数据具有明显业务标识,例如Saas系统中的表按照租户ID、订单表按照订单类型、工单表按照工单类型
优点可以根据具体的属性值进行分片,方便根据属性值进行查询和过滤操作
缺点分片规则不好维护,可能产生数据倾斜,数据不好扩容

3. 自定义业务ID(Hash)

特性说明
适用场景常用于互联网C端场景,例如根据用户ID分片,可以轻松的根据用户ID查找用户所有数据
优点数据分布均匀,可以实现负载均衡
缺点数据扩容困难,范围查询效率较低

六、最佳实践

结合上述技术选型对比,并从数据迁移成本、可维护性考虑,最终决定采用ShardingSphere-JDBC分库分表方案。

订单数据流转图

1. 改造点梳理

1.1 数据库结构改造
改造项具体内容
订单表合并字段创建新订单表,将orderNo和orderId进行合并,统一采用自定义编号,自定义编号长度会超过long最大值,需要采用string类型
关联表改造将有orderId字段的相关表进行改造,统一通过自定义编号进行关联
字段剥离将订单表中多余字段进行剥离(待定
1.2 代码改造
改造项具体内容
数据源隔离区分OLTP请求和OLAP请求,并从数据源进行隔离
引入中间件引入ShardingJDBC,并配置分片键、分库分表数据源、分布式事务代理
查询优化join查询改造,分表后的关联查询必须带有分片键
1.3 数据清洗
清洗项具体内容
订单号清洗对不符合规则的老订单号进行清洗,生成新订单号,并记录新老订单号关系
orderNo关联清洗对有关联orderNo的表进行清洗,将老订单号替换成新订单号
orderId关联清洗对有关联orderId的表进行改造清洗,将orderNo注入到有orderId的关联表中
1.4 数据分片

数据规模估算

  • 现在日订单高峰期20W,按照当前业务5倍进行规划,并预计10年订单量
  • 日订单量100W,年订单量3.6亿,10年订单量36亿
  • 允许单表最大订单量约5000W,36亿/5000W = 72,为了满足一致性hash原则2^n,取64张表

2. 分布式事务Seata接入

参考SpringCloud多数据源接入Seata和ShardingJDBC最佳实践

3. 实施步骤

步骤具体内容
新建库表在新订单库中,创建订单主表和分表,主要字段包括:订单编号、用户ID、订单状态、金额等信息
生成新订单号离线生成新订单号并记录新老订单号关系表order_new_relation,保存原订单ID、原订单编号和新订单编号
Flink数据同步通过Flink将老订单数据与关系表进行关联,使用新订单号替换旧数据,按照分片规则将数据同步到对应分表;同时处理订单拓展表的同步

七、风险评估与应对措施

风险点可能影响应对措施
分片键选择不当查询效率低下,数据分布不均1. 充分分析业务查询模式
2. 选择高频查询字段作为分片键
3. 必要时进行双写或异构索引
数据迁移风险数据丢失,服务中断1. 制定详细的迁移计划并演练
2. 增量同步 + 全量校验
3. 准备回滚方案
分布式事务一致性数据不一致,业务异常1. 使用成熟的分布式事务框架
2. 降低分布式事务粒度
3. 实现补偿机制和监控告警
查询性能下降用户体验差,系统响应慢1. SQL优化,避免跨分片查询
2. 合理使用缓存
3. 添加读写分离和索引
应用代码改造工作量大开发周期延长,引入bug1. 分阶段实施
2. 完善测试用例
3. 采用灰度发布策略

八、常见问题及解决方案

Q1: 如何处理跨分片的分页查询?

解决方案

  • 使用流式查询避免大offset
  • 使用上次查询的最后一条记录ID作为条件
  • 对于复杂报表查询,建议使用OLAP数据库

Q2: 分库分表后如何保证唯一性约束?

解决方案

  • 业务层面进行唯一性校验
  • 使用全局唯一ID生成器
  • 使用分布式锁进行控制

Q3: 如何进行数据扩容?

解决方案

  • 对于Hash分片,采用翻倍扩容(如64张表扩展到128张表)
  • 使用一致性Hash算法减少数据迁移
  • 提前使用FlinkCDC进行数据实时同步

Q4: 分布式事务超时如何处理?

解决方案

  • 设置合理的超时时间
  • Seata有自动补偿机制
  • 高并发场景下,减少分布式事务的使用
  • 减少大事务的使用

九、总结

本方案结合一段时间内未来业务规模、运维成本、服务器成本等多种因素进行考虑,并分析了分区表、分布式数据库、分库分表的区别和优劣势。也简单介绍了一下分库分表需要考虑的唯一ID、分片键、分片算法等问题,并结合实际业务简单梳理了一下改造方案。本文采用停机迁移分库分表方案,如果想要不停机迁移,可以参考大众点评分阶段实施。

备注:如果只分表不分库也能满足需求的话,分区表其实也是一个不错的选择,不用引入其它第三方组件,mysql就原生支持,并且对开发比较友好。但是PolarDB分区表不支持多个分区键用同一个分区规则,并且PolarDB列存数据也会按照行存的进行分区,有一定的性能损耗。

十、参考资料