数据库分库分表策略
垂直切分
垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。
优点:
- 解决业务系统层面的耦合,业务清晰
- 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
- 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈
缺点:
- 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
- 分布式事务处理复杂
- 依然存在单表数据量过大的问题
水平切分
当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。
根据数值范围
按照时间或者 ID 区间来切分。按日期将不同月甚至是日的数据分散到不同的库中;将userId为19999的记录分到第一个库,1000020000的分到第二个库,以此类推。
优点:
- 单表大小可控
- 天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移
- 使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题
缺点:
- 热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询
根据数值取模
采用 Hash 方式取模切分。
优点:
- 数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈
缺点:
- 后期分片集群扩容时,需要迁移旧的数据
- 容易面临跨分片查询的复杂问题
基因分库法
上面的 按照数值取模 的方式只适合于按照一个 Key 的维度来分表,在实际使用中有时候会有一定的局限性,比如业务需要按照两个维度来查询。在电商系统中,对于订单 (Order) 和 购买用户 (Buyer),需要同时支持按照两个维度来查询
- 用户维度,以用户 ID 作为查询条件,查询用户的所有订单
- 订单维度,以订单 ID 为查询条件,查询订单详情
以任何维度来作为分表的 Sharding Key 都不合适,因此就有了基因分库法。
假设我们将用户的订单拆分成 16 个库(数量必须为 2 的 N 次幂,这里将十进制整数的取模运算转换为了二进制取模),则用户 ID 二进制的后四位为分库基因,在订单 ID 末端加入该分库基因,这样无论是按照用户 ID 取模还是订单 ID 取模,结果都会落到同一个库上。
主键生成策略
UUID
UUID标准形式包含32个16进制数字,分为5段,形式为8-4-4-4-12的36个字符。UUID是主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB下,UUID的无序性会引起数据位置频繁变动,导致分页。
结合数据库维护主键ID表
在数据库中建立 Sequence 表。
字段 | 类型 | 说明 |
---|---|---|
id | unsigned bigint(20) auto_increment | 自增主键 |
stub | char(1) UNIQUE | 标识是哪个表,唯一索引 |
需要获取全局唯一 ID 时,执行下面的 SQL
REPLACE INTO sequence (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
方案简单,但是存在单点问题,强依赖DB,当DB异常时,整个系统都不可用。配置主从可以增加可用性,但当主库挂了,主从切换时,数据一致性在特殊情况下难以保证。另外性能瓶颈限制在单台MySQL的读写性能。
Twitter 的 Snowflake 分布式自增 ID 算法
Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位的Long型数字,组成部分:
- 第一位未使用
- 接下来41位是毫秒级时间,41位的长度可以表示69年的时间
- 5位datacenterId,5位workerId。10位的长度最多支持部署1024个节点
- 最后12位是毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序列
优点:
- 毫秒数在高位,生成的ID整体上按时间趋势递增
- 不依赖第三方系统,稳定性和效率较高,理论上QPS约为409.6w/s(1000*2^12),并且整个分布式系统内不会产生ID碰撞;
- 可根据自身业务灵活分配bit位
缺点:
- 强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复。
参考文献: