分库分表后,分片键的选择非常重要。一般来说是这样的:
- 信息表,使用 id 进行分片。例如说,文章、商品信息等等。
- 业务表,使用 user_id 进行分片。例如说,订单表、支付表等等。
- 日志表,使用 create_time 进行分片。例如说,访问日志、登陆日志等等。
? 分片算法的选择?
选择好分片键之后,还需要考虑分片算法。一般来说,有如下两种:
取余分片算法。例如说,有四个库,那么 user_id 为 10 时,分到第
10 % 4 = 2
个库。- 当然,如果分片键是字符串,则需要先进行 hash 的方式,转换成整形,这样才可以取余。
- 当然,如果分片键是整数,也可以使用 hash 的方式。
范围算法。
- 例如说,时间范围。
上述两种算法,各有优缺点。
对于取余来说:
- 好处,可以平均分配每个库的数据量和请求压力。
- 坏处,在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。
对于 range 来说:
- 好处,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了。
- 缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。
? 如果查询条件不带分片键,怎么办?
当查询不带分片键时,则中间件一般会扫描所有库表,然后聚合结果,然后进行返回。
对于大多数情况下,如果每个库表的查询速度还可以,返回结果的速度也是不错的。具体,胖友可以根据自己的业务进行测试。
? 使用 user_id 分库分表后,使用 id 查询怎么办?
有四种方案。
1)不处理
正如在 「如果查询条件不带分片键,怎么办?」问题所说,如果性能可以接受,可以不去处理。当然,前提是这样的查询不多,不会给系统带来负担。
2)映射关系
创建映射表,只有 id、user_id 两列字段。使用 id 查询时,先从映射表获得 id 对应的 user_id ,然后再使用 id + user_id 去查询对应的表。
当然,随着业务量的增长,映射表也会越来越大,后续也可能需要进行分库分表。
对于这方式,也可以有一些优化方案。
- 映射表改成缓存到 Redis 等 KV 缓存中。当然,需要考虑如果 Redis 持久化的情况。
- 将映射表缓存到内存中,减少一次到映射表的查询。
3)基因 id
分库基因:假如通过 user_id 分库,分为 8 个库,采用 user_id % 8 的方式进行路由,此时是由 user_id 的最后 3bit 来决定这行 User 数据具体落到哪个库上,那么这 3bit 可以看为分库基因。那么,如果我们将这 3 bit 参考类似 Snowflake 的方式,融入进入到 id 。
这里的 3 bit 只是举例子,实际需要考虑自己分多少库表,来决定到底使用多少 bit 。
上面的映射关系的方法需要额外存储映射表,按非 user_id 字段查询时,还需要多一次数据库或 Cache 的访问。通过基因 id ,就可以知道数据所在的库表。
详细说明,可以看看 《用 uid 分库,uname 上的查询怎么办?》 文章。
目前,可以从 《大众点评订单系统分库分表实践》 文章中,看到大众点评订单使用了基因 id 。
4)多 sharding column
具体的内容,可以参考 《分库分表的正确姿势,你 GET 到了么?》 。当然,这种方案也是比较复杂的方案。