首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >开发经验总结: 读写分离简单实现

开发经验总结: 读写分离简单实现

作者头像
李福春
发布2025-07-01 19:11:33
发布2025-07-01 19:11:33
1820
举报

背景

使用mysql的代理中间件,某些接口如果主从同步延迟大,容易出现逻辑问题。所以程序中没有直接使用这个中间件。

依赖程序逻辑,如果有一些接口可以走读库,需要一个可以显示指定读库的方式来连接读库,降低主库的压力。

所以需要一个能配置多个数据源,通过注解或者代码方式设置读还是写数据源的方法。

实现方式

使用MybatisPlus的多数据源切换的能力。

github地址:https://github.com/baomidou/dynamic-datasource

已经封装好了。直接使用就行。

介绍

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。

其支持 Jdk 1.7+, SpringBoot 1.5.x 2.x.x 3.x.x。JPA用户不建议使用,JPA自带事务,无法连续切库。

特性:

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息 加密(可自定义) ENC()。
  • 支持每个数据库独立初始化表结构schema和数据库database。
  • 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  • 支持 自定义注解 ,需继承DS(3.2.0+)。
  • 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
  • 提供 自定义数据源来源 方案(如全从数据库加载)。
  • 提供项目启动后 动态增加移除数据源 方案。
  • 提供Mybatis环境下的 纯读写分离 方案。
  • 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
  • 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 基于seata的分布式事务方案
  • 提供 本地多数据源事务方案。

我们需要的是纯读写分离 方案,它已经内置了。

功能范围:

  1. 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
  2. 配置文件所有以下划线 <font style="color:rgb(82, 82, 82);background-color:rgb(247, 247, 247);">_</font> 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
  3. 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
  4. 默认的数据源名称为 master ,你可以通过 <font style="color:rgb(82, 82, 82);background-color:rgb(247, 247, 247);">spring.datasource.dynamic.primary</font> 修改。
  5. 方法上的注解优先于类上注解。
  6. DS支持继承抽象类上的DS,暂不支持继承接口上的DS。

使用步骤

1.引入依赖。

代码语言:javascript
复制
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>

2.配置数据源

代码语言:javascript
复制
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 内置加密,使用请查看详细文档
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
       #......省略
       #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2

3.使用注解切换数据源。

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解

代码语言:javascript
复制
@Service
@DS("slave")
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List selectAll() {
    return  jdbcTemplate.queryForList("select * from user");
  }

  @Override
  @DS("slave_1")
  public List selectByCondition() {
    return  jdbcTemplate.queryForList("select * from user where age >10");
  }
}

业务集成

我们的程序使用的经典的三层结构,即MVC .

图片
图片

看到我们的框架是已经集成了dynamicSource的。如果没有,则加一下依赖即可。

以crm服务为例子。写了一些测试代码,进行验证测试。

代码语言:javascript
复制
package com.pig4cloud.pigx.crm.controller;


import com.baomidou.dynamic.datasource.annotation.DS;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import com.pig4cloud.pigx.crm.mapper.AiAgentMapper;
import com.pig4cloud.pigx.crm.service.AiAgentService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequiredArgsConstructor
@Tag(description = "test read write", name = "测试读写分离")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
@Valid
@Slf4j
public class TestDsController {

    private final AiAgentMapper aiAgentMapper;

    private final AiAgentService aiAgentService;

    @GetMapping("/testD")
    @ResponseBody
    //@Inner(value = false)
    public R testD() {
        return R.ok(aiAgentMapper.selectList(null));
    }


    @GetMapping("/testRead")
    @ResponseBody
    //@Inner(value = false)
    @DS("read")
    public R testRead() {
        return R.ok(aiAgentMapper.selectList(null));
    }

    @GetMapping("/testMaster")
    @ResponseBody
    //@Inner(value = false)
    @DS("master")
    public R testMaster() {
        return R.ok(aiAgentMapper.selectList(null));
    }

    @GetMapping("/testHe")
    @ResponseBody
    //@Inner(value = false)
    @DS("master")
    public R testHe() {
        return R.ok(aiAgentService.listTest());
    }

    @GetMapping("/testHe2")
    @ResponseBody
    //@Inner(value = false)
    @DS("read")
    public R testHe2() {
        return R.ok(aiAgentService.listTest2());
    }


    @GetMapping(value = "/testHe3",produces = "application/json")
    @ResponseBody
    //@Inner(value = false)
    public R testHe3() {
        Map<String, Object> dataMap = new HashMap<>();

        dataMap.put("read", aiAgentService.listTest());
        dataMap.put("master", aiAgentService.listTest2());

        return R.ok(dataMap);
    }


}

对应的Service实现代码:

代码语言:javascript
复制
@Service
@Slf4j
@RequiredArgsConstructor
public class AiAgentServiceImpl extends ServiceImpl<AiAgentMapper, AiAgentEntity> implements AiAgentService {

    @Override
    @DS("read")
    public List<AiAgentEntity> listTest() {
        return baseMapper.selectList(null);
    }

    @Override
    @DS("master")
    public List<AiAgentEntity> listTest2() {
        return baseMapper.selectList(null);
    }
}

测试用例和结果:

测试接口和场景

测试说明

预期结果

测试结果

/testD

不加任何注解

走主库

- [x] 走主库

/testRead

添加了读库注解

走读库

- [x] 走读库

/testMaster

添加了主库注解

走主库

- [x] 走主库

/testHe

上层加主库注解,下层加读库注解

走读库

- [x] 走读库

/testHe2

上层加读库注解,下层加写库注解

走主库

- [x] 走主库

/testHe3

在中层的方法分别加读库和写库注解

分别走读库和主库

- [x] 分别走读库和读库

图片
图片
图片
图片
图片
图片
图片
图片
图片
图片
图片
图片

目前需要增加读写分离的服务。

服务

增加读写分离的原因

是否完成增加

crm

后台列表查询页面, openAPI的读接口,直接走读库;

- [x]

email

后台列表查询页面, openAPI的读接口,直接走读库;

- [ ]

phone

后台列表查询页面, openAPI的读接口,直接走读库;

- [ ]

ssm

后台列表查询页面, openAPI的读接口,直接走读库;

- [ ]

upms

后台列表查询页面, openAPI的读接口,直接走读库;

- [ ]

源码研究

3.6.0版本。基于这个进行分析。

1 自动装配类

图片
图片

配置属性:DynamicDataSourceProperties

属性

说明

primary

默认的库

strict

严格检查

p6spy

日志输出

seata

是否开启seata

lazy

是否懒加载数据源

setaMode

AT模式

publicKey

加密key

datasource

数据源,可以单独配置;

strategy

负载均衡策略

druid

数据源

hikari

数据源

beecp

数据源

dbcp2

数据源

aop

aop配置,有默认值

DataSourceProperty

属性说明

说明

poolName

连接池名称

type

连接类型

driverClassName

驱动名称

url

连接url

username

用户名

password

密码

jndiName

jndi中的名称

init

初始化配置

p6spy

日志输出

seata

是否开启seata

lazy

是否懒加载数据源

publicKey

加密key

druid

数据源

hikari

数据源

beecp

数据源

dbcp2

数据源

DatasourceInitProperties

属性名

说明

schema

建表脚本

data

数据脚本

continueOnError

错误是否继续

separator

分隔符

2 自动装配逻辑

基于切面实现。

小结

集成使用了mybatisPlus自带的多数据源实现了读写分离,并探究了源码实现过程。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-09-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 李福春持续输出 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 实现方式
    • 介绍
    • 使用步骤
  • 业务集成
  • 源码研究
    • 1 自动装配类
    • 2 自动装配逻辑
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档