分库分表与读写分离实践

业务场景

分库分表和读写分离属于架构层面了, 网络上常流行说 "脱离业务谈架构, 就是耍流氓", 因此我们先聊聊业务场景吧!

  1. 智能推荐引擎. 我的上一份工作主要是负责智能推荐引擎的研发, 每天晚上我们的计算流程, 会把 Item 之间的推荐关系计算出来, 然后存到 MySQL 表中, 供第二天实时推荐服务查询. 当然具体的查询时会使用 Redis 缓存.

  2. 内容结构化. 将合作方的网页文章爬去到 MySQL 数据库中, 然后做内容的结构化, 然后推送给业务方做展示. 整个过程中, 一篇文章会被存三次, 一年大约 300GB.

瓶颈分析

  1. 一次写入多次读取, 每一条记录, 内容很少, 占用的存储很少. 瓶颈在于读取的压力, 以及写入数据时的表锁.

  2. 随时会有数据的写入和读取, 不过频次比较低, 但每条记录, 内容很多, 占用的存储很多. 瓶颈在于读写的实时性, 和数据的一致性, 以及数据发生变更时, binlog 很大, 同步到 hive 时, 拉取日志的压力很大.

解决方案

  1. 针对第一种场景, 推荐使用读写分离, 也就是主从同步 (Master-Slave), 根据读取的需求量, 可以设置 2 个以上的从库. 写入数据只能走主库, 读取数据绝大部分路由到从库, 可以少量路由到主库, 也可以全部走从库. 注意, 主从同步的方案, 涉及到主从延迟的问题, 对于数据实时性和一致性要求高的业务场景, 请不要使用, 比如第二个场景.

  2. 针对第二场景, 推荐使用分库分表, 简单来说, 就是把原来存放在一张表里的数据, 拆分到多张表中, 这样每张表的数据量就很少了. 比如记录日志, 这种情况只需要按照常规的方式分库分表即可, 而对于这里的场景, 还需要从业务层面进行分表. 接下来, 我们就详细聊聊这次分库分表实践吧.

分库分表实践

按业务分表

拆分之前, 文章的数据表 Article 包含 25 个字段, 包括原始信息, 以及结构化算法生成的结果信息, 其中 HTML 内容根据不同阶段, 共存放了三份, 这样的一条记录大约 200kB.

问题来了, 如果某个短的字段, 比如文章分类发生了变更, 那么 binlog 的机制会认为这条记录发生了变更, 就会记录一个 200kB 的日志. 事实上, 这 25 个字段, 很多字段只会有一次写入, 不会变更; 有的字段会有一次变更; 还要一些字段才会多次变更. 另外, 绝大部分是短字段, 只有三个是长字段, 如果只是短字段发生变更, 那么 binlog 就不要记录长的字段了.

综上, 我们按照字段的长短和变更的频次分成三张表:

  1. ArticleOrig 原始信息 (一次写入, 几乎不更新)
  2. ArticleBase 基础信息 (经常变更, 短字段, 索引字段)
  3. ArticleBlob 富文本信息 (经常变更, 长字段)

分库分表

可以只有分表, 让多个分表在同一个分库中; 不过, 建议分表的同时, 也分库. 比如我们这里是分成 8 个库, 每个库, 再分 16 张表, 也就说, 原来的每张表被分成 128 张表.

注意, 不是所有的表都需要分库分表的, 分库分表带来的好处是, 增大数据的存储量, 但是也会牺牲一些全局特性, 稍后会介绍到. 我们这里只会对 ArticleOrig 和 ArticleBlob 分库分表, 而 ArticleBase 不用, 因为这张表的字段都很短, 占用的存储很少.

自增主键

每张数据表 MySQL 产生一个自增 ID 字段, 作为主键, 感觉是理所应当的, 不过分库分表之后, 这就很难办到了, 因为失去了全局唯一性, MySQL 不能自动生成一个自增 ID 了. (自增 ID 是相对于单表而言的)

这时, 我们可以通过外部方式生成分表中的自增主键 ID, 比如 Leaf——美团点评分布式ID生成系统, 对于我们这里的情形, 正好可以用不分表的 ArticleBase 来生成自增 ID.

写入读取

建议在代码中保留逻辑表和物理表. 前面提到到三张表 (Orig, Base, Blob) 可以看作物理表, 而拆分前的 Article 表可以看作逻辑表, 通过 DAO 层屏蔽分库分表带来的细节.

写入数据时, 先写入 ArticleBase 表, 同时生成一个全局自增 ID, 然后写入 ArticleOrig 和 ArticleBlob 两张表, 注意整个写入过程要在一个事务中完成.

建议在分表中只支持按 ID 查询, 而其他唯一索引上的查询都转移到 ArticleBase 中, 从该表查询到对应的主键 ID, 再去分表中查询.

建议此时谨慎使用读写分离技术, 因为在分库分表的情况下, 有的分表可能存在主从延迟, 可能导致读取失败, 或者数据不一致. 如果对数据实时性要求不高, 可以忽略这点.

总结

  • 分库分表和读写分离是两种完全不同的优化方案, 使用场景也不同
  • 建议先按字段长短和变更频次从业务层面进行分表
  • 需要一种方式生成全局唯一主键
  • 需要用事务的方式写入数据
  • 建议用逻辑表屏蔽分库分表带来的细节

参考文献