
在互联网业务高速发展的今天,数据量和访问量呈指数级增长。当单数据库服务器无法承载高并发读写请求时,读写分离成为解决性能瓶颈的关键技术之一。它通过将 “读” 和 “写” 操作拆分到不同数据库节点,充分利用硬件资源,大幅提升系统吞吐量与稳定性。本文将从原理到实践,拆解读写分离的实现逻辑、核心挑战与落地技巧,帮助开发者构建高效可靠的分布式数据库架构。
读写分离是基于数据库主从同步架构的延伸方案,其核心思想是:
简单来说,就是 “写主库,读从库”,让不同节点各司其职,避免单库既承担写入又承接大量查询导致的性能过载。
在高并发业务场景中,单库架构的局限性会逐渐凸显,而读写分离能针对性解决这些问题:
以某短视频平台为例,其用户 “刷视频”(读请求)日均达 10 亿次,而 “发布视频”(写请求)仅 1000 万次。通过部署 1 主 8 从的读写分离架构,主库仅处理发布、点赞等写操作,8 个从库承接所有读请求,系统响应时间从 500ms 降至 50ms 以内,且峰值期无一次服务中断。
根据业务复杂度和技术选型,读写分离的实现架构可分为 “应用层直连”“中间件代理”“云数据库托管” 三类,不同架构的优缺点与适用场景差异显著。
应用程序通过代码逻辑(如自定义数据源路由)直接连接主库和从库,写操作时路由到主库,读操作时轮询或随机分配到从库。
// 1. 定义数据源路由类public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 依据ThreadLocal判断当前操作类型 if (DynamicDataSourceContextHolder.isWrite()) { return "masterDataSource"; // 写操作路由到主库 } else { // 读操作轮询从库(简化逻辑) return "slaveDataSource" + (new Random().nextInt(2) + 1); } }}// 2. 配置数据源@Configurationpublic class DataSourceConfig { // 主库数据源配置 @Bean("masterDataSource") public DataSource masterDataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://master:3306/test"); // 其他配置(用户名、密码等) return new HikariDataSource(config); } // 从库1数据源配置 @Bean("slaveDataSource1") public DataSource slaveDataSource1() { /* 类似主库配置,地址为slave1 */ } // 从库2数据源配置 @Bean("slaveDataSource2") public DataSource slaveDataSource2() { /* 类似主库配置,地址为slave2 */ } // 注册路由数据源 @Bean public DataSource routingDataSource() { ReadWriteRoutingDataSource routingDataSource = new ReadWriteRoutingDataSource(); Map<Object, Object> dataSources = new HashMap<>(); dataSources.put("masterDataSource", masterDataSource()); dataSources.put("slaveDataSource1", slaveDataSource1()); dataSources.put("slaveDataSource2", slaveDataSource2()); routingDataSource.setTargetDataSources(dataSources); routingDataSource.setDefaultTargetDataSource(masterDataSource()); // 默认主库 return routingDataSource; }}// 3. 自定义注解实现读写切换@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface ReadOnly { }// 4. AOP切面控制数据源@Aspect@Componentpublic class ReadWriteAspect { @Before("@annotation(readOnly)") public void setReadMode(ReadOnly readOnly) { DynamicDataSourceContextHolder.setRead(); // 标记为读操作 } @After("@annotation(readOnly)") public void clearReadMode() { DynamicDataSourceContextHolder.clear(); // 清除标记 }}// 5. 业务层使用@Servicepublic class UserService { // 写操作(默认主库) public void createUser(User user) { /* 插入主库逻辑 */ } // 读操作(通过注解路由到从库) @ReadOnly public User getUserById(Long id) { /* 从从库查询逻辑 */ }}中小规模业务、技术团队人数较少、业务逻辑简单的场景(如创业公司初期、内部管理系统)。
在应用程序与数据库之间增加一层 “中间件代理”(如 MyCat、Sharding-JDBC、ProxySQL),应用仅连接代理节点,由代理负责读写分离、负载均衡、故障切换等逻辑,对应用透明。
spring: shardingsphere: datasource: names: master,slave1,slave2 master: # 主库配置 type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://master:3306/test username: root password: 123456 slave1: # 从库1配置(类似主库,地址为slave1) slave2: # 从库2配置(类似主库,地址为slave2) rules: readwrite-splitting: data-sources: test-db: # 数据源名称 type: Static # 静态路由(固定主从节点) props: write-data-source-name: master # 写数据源 read-data-source-names: slave1,slave2 # 读数据源列表 load-balancer-name: round_robin # 负载均衡策略(轮询) props: sql-show: true # 打印SQL路由日志中大规模业务、多语言应用、需要分库分表扩展的场景(如电商、社交、金融支付)。
云服务商(如阿里云 RDS、腾讯云 CDB、AWS RDS)提供现成的读写分离服务,用户无需手动搭建主从集群或中间件,只需在控制台开启读写分离功能,即可获得自动分配的从库和连接地址。
追求效率、无专业 DBA 团队、希望降低运维成本的企业(如中小型互联网公司、传统企业数字化转型项目)。
虽然读写分离能显著提升性能,但在实际落地中,会遇到 “数据一致性”“读负载均衡”“故障切换”“特殊 SQL 处理” 等挑战,需针对性解决。
主库执行写操作后,数据需通过主从同步传输到从库,若同步存在延迟(如 1 秒),此时读从库会拿到旧数据(如用户刚更新昵称,刷新页面仍显示旧昵称)。
对实时性要求高的读请求(如用户个人中心、订单详情),直接路由到主库,牺牲部分主库性能换取一致性。例如:
// 订单详情查询(实时性要求高,强制读主库)public Order getOrderDetail(Long orderId) { DynamicDataSourceContextHolder.setWrite(); // 标记为写操作,路由到主库 try { return orderMapper.selectById(orderId); } finally { DynamicDataSourceContextHolder.clear(); }}在同一事务中,若先执行写操作,后续读操作强制走主库,避免事务内数据不一致。例如 Sharding-JDBC 支持Hint机制:
@Transactionalpublic void updateUserAndQuery(User user) { // 1. 写操作(主库) userMapper.updateById(user); // 2. 事务内读操作,强制走主库 HintManager.getInstance().setWriteRouteOnly(); User updatedUser = userMapper.selectById(user.getId());}监控从库同步延迟(如 MySQL 的Seconds_Behind_Master),仅将读请求路由到延迟低于阈值(如 500ms)的从库,延迟超标的从库暂时排除。例如中间件 MyCat 可配置:
<!-- MyCat配置:仅使用延迟<1秒的从库 --><readHost host="slave1" url="slave1:3306" user="root" password="123456"> <property name="delayThreshold">1000</property> <!-- 延迟阈值(毫秒) --></readHost>若采用简单轮询策略,当从库性能差异较大(如部分从库为高配服务器,部分为低配)或读请求存在热点(如某商品详情页被频繁访问)时,会导致部分从库负载过高(CPU 100%),部分从库资源闲置。
根据从库的 CPU、内存使用率动态调整权重,性能好的从库分配更多请求。例如 Sharding-JDBC 可配置加权负载均衡:
spring: shardingsphere: rules: readwrite-splitting: data-sources: test-db: props: load-balancer-name: weight_round_robin # 加权轮询策略 load-balancers: weight_round_robin: type: WeightRoundRobin props: slave1: 3 # 从库1权重3 slave2: 1 # 从库2权重1(性能较差)对请求的关键参数(如用户 ID、商品 ID)进行哈希计算,将同一参数的请求路由到固定从库,避免缓存穿透(如用户多次查询同一商品,始终从同一从库读取,利用从库缓存)。例如:
// 按用户ID哈希路由到从库private String getSlaveDataSourceByUserId(Long userId) { int slaveCount = 2; // 从库数量 int index = Math.abs(userId.hashCode()) % slaveCount; return "slaveDataSource" + (index + 1);}对热点读请求(如秒杀商品详情),通过 Redis 缓存或 CDN 直接返回结果,不穿透到从库,避免从库因热点请求过载。
中间件或监控系统定期检测从库心跳(如 TCP 连接、SQL 查询select 1),发现故障后自动将其从读数据源列表中移除,故障恢复后重新加入。例如 ProxySQL 通过mysql_servers表管理节点状态,故障节点会被标记为OFFLINE。
采用 “半同步复制 + 自动切换工具” 实现主从切换,步骤如下:
阿里云 RDS 等云数据库已内置该能力,主库故障时可实现秒级自动切换,业务无感知。
部分 SQL 语句看似读操作,实则会触发写操作(如SELECT ... FOR UPDATE行锁、INSERT ... SELECT),若路由到从库会导致锁失败或数据不一致;此外,从库默认可能为 “只读模式”,直接执行写操作会报错。
在中间件或应用层配置 SQL 路由规则,将特殊写操作 SQL 强制路由到主库。例如 Sharding-JDBC 可通过 SQL 注释指定路由:
-- 强制路由到主库(即使是SELECT语句)/* !SHARDINGSPHERE_ROUTE_TO_WRITE_DATASOURCE! */SELECT * FROM user WHERE id = 1 FOR UPDATE;明确设置从库为只读模式,防止误操作写入。例如 MySQL 从库配置:
-- 从库开启只读(超级用户除外,方便主从同步)SET GLOBAL read_only = 1;-- 禁止超级用户写入(可选,更严格)SET GLOBAL super_read_only = 1;并非所有业务场景都适合立即实施读写分离架构。在落地前,需通过业务流量分析、数据库性能监控、成本收益评估三方面综合判断:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。