软件架构与思考

👉 所有文章
高性能 高性能设计模式
数据库 数据库分库分表指南 MySQL 建表参考 MySQL 初始化数据的一些方案 数据版本号
分布式 ID 分布式 ID 生成方案探讨
缓存 缓存 使用数据版本号保证缓存是最新数据 基于redis的二级缓存
微服务 如何实现远程调用 RPC 协议中的数据签名验签和加解密方案探讨 关于服务间调用循环依赖的一些思考 我所理解的负载均衡 一致性哈希 基于Redis的分布式会话管理系统 如何部署服务 灰度发布 如何区分上游和下游 日志级别
算法与协议 一个可扩展的 MQ 消息设计 Dynamo涉及的算法和协议 写时复制
任务分发 Gearman入门 如何使用redis构建异步任务处理程序
安全 关于对账的一些理解 一个简单可靠的 Dubbo 请求/响应数据签名方案
其他 使用卫语句减少 if else 嵌套

数据库分库分表指南


(本文待完善)

目录:


要关注的一些问题:

  • 保证每个表都有数据
  • 数据倾斜问题(数据均匀分布和数据聚合很可能是冲突的)
  • 分布式事务
  • 唯一性保证
  • 业务隔离:不同的业务用不同的库
  • 如何按时间分表

一种错误的分库分表方案

假设某业务要记录用户相关的一些数据,准备用用户标识 user_id(long 类型,MySQL 中是 bigint )进行分库分表。方案如下:

  • 库表数量:4个库,每个库8张表,标号从0开始。
  • 分库方案:user_id % 4
  • 分表方案:user_id % 8

这个分表方案是错误的。因为部分表,会永远没有数据。

代码验证:

使用 Python 3 验证。

stats = {}

for db_index in range(0, 4):
    for tb_index in range(0, 8):
        stats.setdefault(db_index, {})
        stats[db_index].setdefault(tb_index, 0)

for user_id in range(1, 200000):
    db_index = user_id % 4
    tb_index = user_id % 8
    stats.setdefault(db_index, {})
    stats[db_index].setdefault(tb_index, 0)
    stats[db_index][tb_index] += 1

for db_index in range(0, 4):
    for tb_index in range(0, 8):
        print('分库号:{}, 分表号:{}, 数据量:{}'.format(db_index, tb_index, stats[db_index][tb_index]))

运行结果:

分库号:0, 分表号:0, 数据量:24999
分库号:0, 分表号:1, 数据量:0
分库号:0, 分表号:2, 数据量:0
分库号:0, 分表号:3, 数据量:0
分库号:0, 分表号:4, 数据量:25000
分库号:0, 分表号:5, 数据量:0
分库号:0, 分表号:6, 数据量:0
分库号:0, 分表号:7, 数据量:0
分库号:1, 分表号:0, 数据量:0
分库号:1, 分表号:1, 数据量:25000
分库号:1, 分表号:2, 数据量:0
分库号:1, 分表号:3, 数据量:0
分库号:1, 分表号:4, 数据量:0
分库号:1, 分表号:5, 数据量:25000
分库号:1, 分表号:6, 数据量:0
分库号:1, 分表号:7, 数据量:0
分库号:2, 分表号:0, 数据量:0
分库号:2, 分表号:1, 数据量:0
分库号:2, 分表号:2, 数据量:25000
分库号:2, 分表号:3, 数据量:0
分库号:2, 分表号:4, 数据量:0
分库号:2, 分表号:5, 数据量:0
分库号:2, 分表号:6, 数据量:25000
分库号:2, 分表号:7, 数据量:0
分库号:3, 分表号:0, 数据量:0
分库号:3, 分表号:1, 数据量:0
分库号:3, 分表号:2, 数据量:0
分库号:3, 分表号:3, 数据量:25000
分库号:3, 分表号:4, 数据量:0
分库号:3, 分表号:5, 数据量:0
分库号:3, 分表号:6, 数据量:0
分库号:3, 分表号:7, 数据量:25000

可以看到,

  • 分库0只要分表0、分表4有数据;
  • 分库1只有分表1、分表5有数据;
  • 分库2只有分表2、分表6有数据;
  • 分库3只有分表3、分表7有数据;

数学证明

以下均为整数运算:

以1号库为例, 某 user_id 落库到 1 号库,即:

user_id % 4 = 1 

那么一定有整数 k,使得 user_id = 4 * k + 1

所以,分表号 m 是:

m = user_id % 8
= ( 4 * k + 1 ) % 8
=  4 * k + 1 - [ ( 4 * k + 1)/ 8 ] * 8
=  4 * k + 1 - [ ( 4 * k + 1 ) / 8 ] * 4 * 2
=  1 + 4 * k - 4 * [ ( 4 * k + 1 ) / 8 ] * 2
=  1 + 4 * { k - [ ( 4 * k + 1 ) / 8 ] * 2 }

所以,分表号一定是 4的倍数 + 1 。

正确的做法:

方案1:

采用同一字段取模分库分表时,分库数和分表数的最大公约数需要是1,否则会造成数据倾斜甚至部分表无数据。 例如: 库表数量:4个库,每个库31张表,标号从0开始。 分库方案:user_id % 4 分表方案:user_id % 31

方案2:

分库用一个字段,分表用另外一个字段。

方案3:

分库和分表不要用相同的计算方法。 比如分表改成先 crc32 ,再模 8 ,也就是:

  • 分库方案:user_id % 4
  • 分表方案:crc32(user_id) % 8

验证代码如下:

from zlib import crc32

stats = {}

for db_index in range(0, 4):
    for tb_index in range(0, 8):
        stats.setdefault(db_index, {})
        stats[db_index].setdefault(tb_index, 0)

for user_id in range(1, 200000):
    db_index = user_id % 4
    tb_index = crc32('{}'.format(user_id).encode('utf-8')) % 8
    stats.setdefault(db_index, {})
    stats[db_index].setdefault(tb_index, 0)
    stats[db_index][tb_index] += 1

for db_index in range(0, 4):
    for tb_index in range(0, 8):
        print('分库号:{}, 分表号:{}, 数据量:{}'.format(db_index, tb_index, stats[db_index][tb_index]))

( 本文完 )

文章目录