数据库读写分离与分库分表:突破单库连接数瓶颈,提升并发写入能力

单库扛不住的时候,别急着上分布式事务。先看看读写分离和分库分表能不能解决问题。这两个手段,说白了就是「拆」——把读和写拆开,把数据和压力拆散。我见过太多团队一上来就搞分布式事务,结果性能没上去,一致性先崩了。

读写分离:把读的压力卸掉

先问个问题:你的数据库连接数为什么不够用?

大部分场景下,连接数被读请求吃掉了。一个典型的发证系统,查询证书状态的请求可能是发证请求的 10 倍。这些读请求占着连接不释放,写请求反而排不上队。

读写分离的思路很简单:主库只写,从库只读。应用层根据 SQL 类型,把请求路由到不同的库。

核心收益:主库的连接数压力直接减半以上。从库可以水平扩展,读能力几乎无限。

我在项目中遇到过一个问题:读写分离后,刚发的证查不到。为什么?主从同步有延迟。证书发出去 50 毫秒,从库还没同步完,用户一查,显示「未发证」。这体验太差了。

解决方案其实不复杂:

  • 强制读主:发证后 1 秒内的查询,强制走主库。用 ThreadLocal 标记即可。
  • 缓存兜底:发证成功时,把证书状态写入 Redis。查询先查缓存,缓存命中就不走库。
  • 延迟容忍:如果业务允许,直接接受短暂不一致。很多场景下,用户刷新一下就好了。

嗯,这里要注意:读写分离不是银弹。如果写请求本身就把主库打满了,那分离读也没用。这时候得考虑分库分表。

分库分表:把写的能力拆出来

单库的连接数上限是固定的。MySQL 默认 151,调高到 1000 也扛不住每秒几万的并发写入。连接数满了,新的写请求就排队,队列一长,超时、报错、雪崩。

分库分表的核心思路:把一张大表拆成多张小表,分散到多个数据库实例上。每个实例只承担一部分连接数和写入量。

我个人习惯先分库,再分表。分库解决连接数瓶颈,分表解决单表数据量过大导致的索引性能下降。

分片策略怎么选?

策略 适用场景 我踩过的坑
按用户 ID 取模 用户维度数据,均匀分布 扩容时数据迁移量大,得提前规划好分片数
按时间范围 证书流水、日志等时序数据 热点集中在最新分区,旧分区利用率低
按地域/机构 多租户场景,天然隔离 租户间数据量差异大,容易倾斜
一致性哈希 需要平滑扩容的场景 实现复杂,路由开销略高

我曾经在一个发证项目里用了按机构 ID 取模。结果有个大机构一天发 100 万张证,其他机构加起来才 10 万。那个分片直接被打爆了。后来改成按用户 ID 取模 + 按时间分表,才把压力摊平。

避坑指南:分片键的选择决定了系统的上限。选错了,后面改起来比重新开发还痛苦。一定要选业务上最均匀、最稳定的字段。

分库分表后的查询问题

分库分表之后,跨分片的查询就麻烦了。比如「查某个机构所有已发证的用户」,如果分片键不是机构 ID,就得遍历所有分片,然后做结果合并。

我的做法是:

  • 全局表:机构、产品等基础信息,每个分片都存一份完整副本。
  • 索引表:维护一个「机构 ID → 用户 ID」的映射表,用 ES 或 Redis 存。
  • 基因法:在分片键里嵌入业务维度信息。比如用户 ID 的最后两位是机构编码,这样按机构查询时可以直接定位到分片。

说白了,分库分表之后,能按分片键查的就直接查,不能的就走索引或广播。没有完美的方案,只有适合业务的取舍。

实战:发证系统的读写分离 + 分库分表架构

下面这张图是我在一个日发 500 万张证书的项目里落地的架构。你想想看,单库连接数 1000,每秒最多处理 2000 个写请求。拆成 8 个库,每个库 4 张表,连接数直接翻了 8 倍,写入能力翻了 4 倍以上。

发证系统读写分离 + 分库分表架构 应用层(发证服务) 分片路由(ShardingSphere) 主库组(写) db_0(4 表) db_1(4 表) db_2(4 表) db_3(4 表) 从库组(读) db_0_slave db_1_slave db_2_slave db_3_slave 写请求(INSERT/UPDATE) 读请求(SELECT) 主从同步(异步) 分片键:user_id % 4 → 定位到 db_0 ~ db_3 每个 db 内再按 cert_id % 4 分到 4 张表

我的经验:分片数建议取 2 的幂(4、8、16、32)。这样扩容时只需要迁移一半数据,而且取模运算性能更好。我习惯用 8 库 8 表起步,预留未来 3 年的增长空间。

分库分表后的写入流程

发证请求进来后,流程是这样的:

  1. 计算分片:根据 user_id % 4 算出目标库 db_0 ~ db_3。
  2. 计算分表:根据 cert_id % 4 算出目标表 cert_0 ~ cert_3。
  3. 写入主库:INSERT INTO db_1.cert_2 (...) VALUES (...)。
  4. 同步从库:主库异步同步到对应的从库。
  5. 强制读主:发证后 1 秒内的查询走主库,避免延迟。

你看,每个分片只处理自己那部分数据。8 个库同时写,并发能力直接翻了 8 倍。连接数也从单库 1000 变成了 8 个库各 1000,总共 8000 个连接。

关键指标:分库分表后,单库连接数瓶颈被彻底打破。只要分片数足够,并发写入能力可以线性扩展。我实测过,8 库 8 表下,单机 MySQL 的写入 TPS 从 2000 提升到了 15000+。

避坑指南:分库分表后的那些坑

我曾经在一个项目里踩过几个坑,分享给你:

  • 分布式事务:跨分片的写入需要保证一致性。别用 XA,性能太差。用 TCC 或本地消息表,最终一致性就够了。
  • 全局主键:自增 ID 不能用了。用雪花算法(Snowflake)生成全局唯一 ID,注意时钟回拨问题。
  • 分片键变更:用户换了手机号,分片键要不要变?我的建议是:分片键一旦确定,永远不变。用用户 ID 做分片键,其他维度走索引表。
  • 数据迁移:扩容时怎么迁移数据?用双写 + 全量校验的方式。先写新库,再写旧库,最后校验一致性。

嗯,这里要特别强调:分库分表是最后的手段。如果读写分离 + 缓存能解决问题,就别上分库分表。架构越复杂,运维成本越高。我见过太多团队为了炫技而分库分表,最后把自己坑了。

说白了,突破单库连接数瓶颈,核心就两招:读写分离卸掉读压力,分库分表拆散写压力。两招配合使用,单库连接数不再是瓶颈。但记住,架构设计要匹配业务规模,别过度设计。