分布式专题

分库分表

分库分表之扩容

分库、分表、垂直拆分和水平拆分

  • 分库:因一个数据库支持的最高并发访问数是有限的,可以将一个数据库的数据拆分到多个库中,来增加最高访问数

  • 分表:因一个表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时只需要查拆分后的某一张表,SQL 语句的查询性能得到提升

  • 分库分表优势:分库分表后,承受的并发增加了多倍;磁盘使用率大大降低;单表数据量减少,sql 执行效率明显提升

  • 水平拆分:把一个表的数据拆分到多个数据库,每个数据库中的表结构不变。用多个库抗更高的并发。比如订单表每个月有 500W 条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库中

  • 垂直拆分:把一个有很多字段的表,拆分成多张表到同一个库或者多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)

分库分表的方式

  • 利用租户来分库、分表

  • 利用字段范围来分库、分表

  • 利用字段 hash 来分库、分表

垂直拆分带来的问题

  • 依然存在单表数据量过大的问题
  • 部分表无法关联查询,只能通过接口聚合的方式解决,提升了开发的复杂度
  • 分布式事务处理复杂

水平拆分带来的问题

  • 跨库的关联查询性能差
  • 数据多次扩容和维护量大
  • 跨分片的事务一致性难以保证

分库分表之唯一 ID

为什么分库分表需要唯一 ID

  • 如果要做分库分表,则必须考虑主键 ID 是全局唯一的,比如有一张订单表,被分到 A 库和 B 库,如果两张表都是从 1 开始递增,那么查询数据的时候就乱了,很多订单 ID 都是重复的,而这些订单其实并不是一笔订单

  • 分库的一个期望结果就是将访问数据的次数分摊到其他库,有些场景是需要均匀分摊的,那么数据插入到多个数据库的时候就需要交替生成唯一的 ID 来保证请求均匀分摊到所有数据库

生成唯一 ID 的原则

  • 全局唯一

  • 趋势递增

  • 单调递增

  • 信息安全

生成唯一 ID 的几种方式

数据库自增 ID,每个数据库每增加一条记录,自己的 ID 增 1
  • 多个库的 ID 可能重复,不适合分库分表后的 ID 生成
  • 信息不安全
UUID
  • UUID 太长,占用空间大
  • 不具有有序性,作为主键时,在写入数据时,不能产生有顺序的 append 操作,只能进行 insert 操作,导致读取整个 B+树节点到内存,插入数据后将整个节点写回磁盘,当记录占用空间很大时,性能很差
获取系统当前时间作为唯一 ID
  • 高并发时,1ms 内可能有多个相同的 ID
  • 信息不安全
Twitter 的snowflake(雪花算法):twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分

2021-04-08-22-34-1320210408223412

  • 优点:
    • 毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的
    • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的
    • 可以根据自身业务特性分配 bit 位,非常灵活
  • 缺点:
    • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态
百度的UIDGenerator算法

2021-04-08-22-43-3920210408224338

  • 基于 snowflake 的优化算法
  • 借用未来时间和双 buffer 来解决时间回拨与生成性能等问题,同时结和 mysql 进行 ID 分配
  • 优点:解决了时间回拨和生成性能问题
  • 缺点:依赖 mysql 数据库
美团的Leaf-snowflake算法
  • 获取 id 是通过代理服务访问数据库获取一批 id(号段)
  • 双缓冲:当前一批的 id 使用 10%时,再访问数据库获取新的一批 id 缓存起来,等上批的 id 用完之后直接用
  • 优点:
    • leaf 服务可以很方便的线性拓展,性能完全能够支撑大多数业务场景
    • id 号码时趋势递增的 8byte 的 64 位数字,满足上述数据库存储的主键要求
    • 容灾性高:leaf 服务内部有号段缓存,即使 DB 宕机,短时间内 leaf 仍能正常对外提供服务
    • 可以自定义 max_id 的大小,非常方便业务从原有的 id 方式上迁移过来
    • 即使 DB 宕机,leaf 仍能持续发号一段时间
    • 偶尔的网络抖动不会影响下个号段的更新
  • 缺点:
    • ID 号码不够随机,能够泄漏发号数量的信息,不太安全

分布式事务

在分布式的世界中,存在着各个服务之间相互调用,链路可能很长,如果有任何一方执行出错,则需要回滚涉及到的其他服务的相关操作。比如订单服务下单成功,然后调用营销中心发券接口发了一张代金券,但是微信支付扣款失败,则需要退回发的那张券,且要将订单状态改为异常订单

分布式事务的几种主要方式

  • XA 方案(两段式提交)
  • TCC 方案(try、confirm、cancel)
  • SAGA 方案
  • 可靠消息最终一致性方案
  • 最大努力通知方案

XA 方案原理

2021-04-09-09-57-4420210409095743

  • 事务管理器负责协调多个数据库的事务,先问问各个数据库准备好了吗?如果准备好了,则在数据库执行操作,如果任何一个数据库没有准备好,则不执行事务
  • 适合单体应用,不适合微服务架构,因为每个服务只能访问自己的数据库,不允许交叉访问其他微服务的数据库

TCC 方案

  • try 阶段:对各个服务的资源做检测以及对资源进行锁定或者预留

  • confirm 阶段:各个服务中执行实际的操作

  • cancel 阶段:如果任何一个服务的业务方法执行出错,需要将之前操作成功的步骤进行回滚

  • 应用场景:

    • 跟支付、交易打交道,必须保证资金正确的场景
    • 对于一致性要求很高
  • 缺点:

    • 要写很多补偿逻辑的代码,且不易维护

Saga 方案

  • 基本原理:
    • 业务流程中的每个步骤若有一个失败了,则补偿前面操作成功的步骤
  • 适用场景:
    • 业务流程长、业务流程多
    • 参与者包含其他公司或遗留系统服务
  • 优势:
    • 第一个阶段提交本地事务,无锁,高性能
    • 参与者可异步执行,高吞吐
    • 补偿服务易于实现
  • 缺点:
    • 不保证事务的隔离性

可靠消息一致性方案

2021-04-09-12-08-0520210409120805

基本原理:

  • 利用消息中间件RocketMQ来实现消息事务
  • 第一步:A 系统发送一个prepared(预备状态,半消息)消息到 MQ,该消息无法被订阅
  • 第二步:MQ 响应 A 系统,告诉 A 系统已经接收到消息了
  • 第三步:A 系统执行本地事务
  • 第四步:若 A 系统执行本地事务成功,将prepared消息改为commit(提交事务消息),B 系统就可以订阅到消息了
  • 第五步:MQ 也会定时轮询所有prepared消息,回调 A 系统,让 A 系统告诉 MQ 本地事务处理的怎么样了,是继续等待还是回滚
  • 第六步:A 系统收到MQ回查,检查本地事务的执行结果
  • 第七步:若 A 系统执行本地事务失败,则 MQ 收到Rollback信号,丢弃消息。若执行本地事务成功,则 MQ 收到commit信号
  • B 系统收到消息后,开始执行本地事务,如果执行失败,则自动不断重试直到成功。或 B 系统采取回滚的方式,同时要通过其他方式通知 A 系统也进行回滚
  • B 系统需要保证幂等性

最大努力通知方案

基本原理:

  • 系统 A 本地事务执行完成之后,发送消息到 MQ
  • MQ 将消息持久化
  • 系统 B 如果执行本地事务失败,则最大努力服务会定时尝试重新调用系统 B,尽自己最大的努力让系统 B 重试,重试多次后,还是不行就只能放弃了。通知开发人员去排查以及后续人工补偿

几种方案如何选择

  • 跟支付、交易打交道,优先 TCC
  • 大型系统,但要求不那么严格,考虑消息事务或 SAGA 方案
  • 单体应用,建议 XA 两阶段提交就可以
  • 最大努力通知方案建议都加上,毕竟不可能一出问题就交给开发排查,先重试几次看看能不能成功