数据库高可用:MySQL主从复制、读写分离、分库分表策略、连接池优化
数据库的高可用,说白了就是让数据层扛得住故障。我见过太多系统,业务代码写得花里胡哨,结果数据库一挂,整个服务直接瘫痪。今天咱们就聊聊MySQL这块,怎么让它真正「高可用」起来。
核心观点:数据库高可用不是单一技术,而是主从复制、读写分离、分库分表、连接池优化这四驾马车的协同作战。缺一个,系统就可能在某个角落埋雷。
一、主从复制:高可用的地基
主从复制是MySQL高可用的起点。没有它,后面的一切都无从谈起。我早期做项目时,觉得单库跑着也挺好,直到有一次凌晨磁盘坏了,数据恢复花了整整6个小时——从那以后,我再也不敢不做主从。
MySQL主从复制的核心流程,其实就三步:
- 主库写binlog:所有写操作先记到二进制日志里
- 从库拉取:从库的IO线程连上主库,把binlog拉过来写到relay log
- 从库回放:从库的SQL线程读取relay log,在本地执行一遍
嗯,这里要注意一个坑:复制延迟。我遇到过最夸张的一次,从库落后主库整整3分钟,导致用户刚下的单在查询时看不到。为什么会这样?因为从库是单线程回放,主库的写并发一高,它就追不上了。
我的建议:MySQL 5.7以上版本开启并行复制(slave_parallel_workers),能显著缓解延迟问题。另外,监控主从延迟的阈值建议设为1秒,超过就告警。
主从复制的部署模式,我习惯用「一主两从」起步:
- 主库:负责所有写操作
- 从库1:负责读操作 + 日常备份
- 从库2:负责读操作 + 故障切换备用
如果主库挂了怎么办?手动切?太慢了。我建议用MHA或者Orchestrator这类工具做自动故障转移。我曾经踩过一个坑:主库宕机后,从库的relay log里还有没应用完的binlog,直接切换会导致数据丢失。所以一定要配置sync_binlog=1和innodb_flush_log_at_trx_commit=1,保证数据不丢。
二、读写分离:把压力卸掉
主从复制搭好了,接下来就是怎么用。很多团队的做法是:所有请求都打主库,从库只做备份。这其实浪费了从库的能力。读写分离,说白了就是把读请求分流到从库,让主库专心处理写操作。
我常用的实现方式有两种:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 应用层路由 | 灵活可控,代码里显式指定数据源 | 侵入性强,每个DAO都要改 |
| 中间件层(如ShardingSphere、MyCat) | 对应用透明,统一管理 | 增加网络一跳,运维成本高 |
我个人更倾向于中间件方案。为什么?因为应用层路由虽然简单,但一旦业务复杂起来,每个团队都自己写一套读写分离逻辑,最后就是一团乱麻。用ShardingSphere的话,配置起来其实不复杂:
# ShardingSphere 读写分离配置示例
spring.shardingsphere.datasource.names=master,slave0,slave1
spring.shardingsphere.datasource.master.url=jdbc:mysql://192.168.1.1:3306/order
spring.shardingsphere.datasource.slave0.url=jdbc:mysql://192.168.1.2:3306/order
spring.shardingsphere.datasource.slave1.url=jdbc:mysql://192.168.1.3:3306/order
spring.shardingsphere.rules.readwrite-splitting.data-sources.order-ds.type=Static
spring.shardingsphere.rules.readwrite-splitting.data-sources.order-ds.props.write-data-source-name=master
spring.shardingsphere.rules.readwrite-splitting.data-sources.order-ds.props.read-data-source-names=slave0,slave1
避坑指南:我曾经遇到一个线上问题——用户刚提交的订单,刷新页面后查不到。原因是写操作在主库,读操作走了从库,而从库有延迟。解决方案:对于强一致性的读(比如支付结果查询),强制走主库。在ShardingSphere里可以配置hint来指定数据源。
三、分库分表:量变引起质变
当单库数据量超过500GB,或者单表行数超过1000万时,读写分离已经不够用了。这时候就需要分库分表。你想想看,一张表1亿条数据,即使加了索引,B+树的高度也到了4层,IO次数明显增加。
分库分表的核心策略,我总结为三种:
- 垂直分库:按业务模块拆分,比如订单库、用户库、商品库。这是最推荐的做法,因为业务边界清晰。
- 水平分库:同一个表的数据分散到多个库,比如按用户ID取模分到4个库。
- 水平分表:同一个库里的表拆成多张子表,比如order_0、order_1、order_2。
分片键的选择是关键。我见过有人用时间戳做分片键,结果热点全集中在最近一个月的数据上,其他分片几乎没流量。我的习惯是:用业务中最常查询的字段做分片键,比如订单表用user_id,用户表用user_id本身。
分库分表之后,跨分片的查询和事务就成了难题。比如要查某个用户的所有订单,如果分片键是user_id,那直接路由到对应分片就行。但如果要查某个商品的所有订单,分片键是user_id,那就得全分片扫描——这就是所谓的「跨分片查询」。
我的经验:对于跨分片查询,要么在应用层做聚合,要么建立冗余索引表。比如维护一张「商品-订单」的映射表,按商品ID分片,这样就能快速定位。虽然增加了写操作的复杂度,但读性能提升明显。
分库分表的中间件,我推荐ShardingSphere或者Vitess。ShardingSphere对Java生态友好,Vitess更适合Kubernetes环境。选型时考虑团队的技术栈和运维能力。
四、连接池优化:最后一公里的性能
数据库连接池,很多人觉得配个默认值就行了。其实不然。连接池的配置直接影响系统的吞吐量和响应时间。我调优过上百个项目的连接池,总结出几个关键参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| maximumPoolSize | CPU核心数 × 2 + 磁盘数 | 不是越大越好,过大反而增加上下文切换 |
| minimumIdle | 等于maximumPoolSize | 避免频繁创建/销毁连接 |
| connectionTimeout | 30000ms | 等待连接的超时时间,太短容易误报 |
| idleTimeout | 600000ms | 空闲连接存活时间,配合数据库的wait_timeout |
| maxLifetime | 1800000ms | 连接最大存活时间,略小于数据库的wait_timeout |
我用的是HikariCP,Spring Boot默认就集成了它。性能确实好,但配置不当也会出问题。我曾经遇到过一个案例:连接池的maximumPoolSize设成了200,结果数据库的连接数被打满,其他服务连不上。后来发现,应用服务器的CPU只有8核,200个连接大部分都在等待,反而拖垮了系统。
避坑指南:连接池的大小不是越大越好。我习惯用公式「CPU核心数 × 2 + 磁盘数」来估算。比如8核CPU、1块磁盘,那就是8×2+1=17个连接。如果业务中有大量慢查询,可以适当增加,但不要超过CPU核心数的4倍。
另外,连接池的监控也很重要。HikariCP提供了JMX指标,可以实时查看活跃连接数、等待线程数、连接获取时间等。我一般会在Grafana上配一个面板,一旦活跃连接数超过最大值的80%,就触发告警。
五、整体架构图
下面这张图是我画的一个抢单系统数据库高可用架构。你可以看到,从应用层到数据层,每一层都有对应的容错机制。
这张图里,应用服务通过HikariCP连接池访问ShardingSphere中间件,中间件根据SQL类型和分片规则,将请求路由到主库或从库。主库负责写,从库负责读,同时通过binlog同步保持数据一致。分库分表策略在中间件层配置,对应用完全透明。
六、总结
数据库高可用这件事,没有银弹。主从复制解决的是单点故障,读写分离解决的是读压力,分库分表解决的是数据量膨胀,连接池优化解决的是连接管理效率。这四个环节环环相扣,任何一个短板都会成为系统的瓶颈。
我见过太多团队,只做了主从复制就觉得高枕无忧,结果从库延迟导致数据不一致;也见过只做了分库分表,但连接池没调优,数据库连接数被打满。所以,我的建议是:把这四件事当成一个整体来规划,从架构设计阶段就考虑进去,而不是等出问题了再补。
最后说一句:高可用不是100%不出故障,而是出了故障能快速恢复。做好监控、做好预案、做好演练,比任何技术方案都重要。
公众号:蓝海资料掘金营,微信deep3321