数据库分库分表指南


#系统设计


(本文待完善)

要关注的一些问题:

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

一种错误的分库分表方案

假设某业务要记录用户相关的一些数据,准备用用户标识 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]))


( 本文完 )