github :https://github.com/lanchengx/dynamic

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生

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

特性

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

约定

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

使用方法

引入dynamic-datasource-spring-boot-starter。

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>

配置数据源。

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
# 多主多从                      纯粹多库(记得设置primary)                   混合配置
spring:                               spring:                               spring:
  datasource:                           datasource:                           datasource:
    dynamic:                              dynamic:                              dynamic:
      datasource:                           datasource:                           datasource:
        master_1:                             mysql:                                master:
        master_2:                             oracle:                               slave_1:
        slave_1:                              sqlserver:                            slave_2:
        slave_2:                              postgresql:                           oracle_1:
        slave_3:                              h2:                                   oracle_2:
使用 @DS 切换数据源。

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
强烈建议只注解在service实现上。

注解 结果
没有@DS 默认数据源
@DS(“dsName”) dsName可以为组名也可以为具体某个库的名称

实例

@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");
  }
}

增加一个aop配置,通过方法名/类名 设置不同的数据源

/**
 * @Author: lancx
 * @Date: 2020/5/10 0010
 */
@Aspect
@Component
@Order(0)
@Lazy(false)
@Log
public class DataSourceAop {

    private static final String MASTER = "master";

    private static final String SLAVE = "slave";

    @Pointcut(" execution(* com.example.dynamic.service..*.*(..)) " +
            "|| execution(* com.baomidou.mybatisplus.extension.service..*.*(..)) " +
            "|| @annotation(org.springframework.transaction.annotation.Transactional)")
    public void pushDataSource() {
    }

    // 这里切到你的方法目录
    @Before("pushDataSource()")
    public void process(JoinPoint joinPoint) throws NoSuchMethodException, SecurityException {
        String methodName = joinPoint.getSignature().getName();
        Class clazz = joinPoint.getTarget().getClass();
        if (clazz.isAnnotationPresent(DS.class)) {
            //获取类上注解
            return;
        }
        String targetName = clazz.getSimpleName();
        Class[] parameterTypes =
                ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
        Method methdo = clazz.getMethod(methodName, parameterTypes);
        if (methdo.isAnnotationPresent(DS.class)) {
            return;
        }
        if (methodName.startsWith("get")
                || methodName.startsWith("count")
                || methodName.startsWith("find")
                || methodName.startsWith("list")
                || methodName.startsWith("select")
                || methodName.startsWith("check")
                || methodName.startsWith("page")) {

            log.info("当前执行的库:" + SLAVE);
            DynamicDataSourceContextHolder.push(SLAVE);
        } else {
            log.info("当前执行的库:" + MASTER);
            DynamicDataSourceContextHolder.push(MASTER);
        }
    }

    @After("pushDataSource()")
    public void afterAdvice() {
        DynamicDataSourceContextHolder.clear();
    }

}

事务问题:

@Transactional
public void method1(){
    service.getById(...);
    ...
    doSomeThing();
}

@DS("master")
@Transactional
public void doSomeThing(){
    service.save()
    service.update()
}

在一个事务内先执行查询getById()方法(使用Slave数据源),后执行doSomeThing()方法,在执行doSomeThing()方法时会使用getById()所使用的数据源。

原因分析:

在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 这个类的源代码中

在开始一个事务前,如果当前上下文的连接对象为空,获取一个连接对象,然后保存起来,下次doBegin再调用时,就直接用这个连接了,根本不做任何切换(类似于缓存命中!)
doSomeThing()方法被调用前,加了一段select方法,相当于已经切换到了slave从库,然后再进入doBegin方法时,就直接拿这个从库的链接了,不再进行切换。

解决办法:

在doSomeThing() 之前手动切换数据库

@Transactional
public void method1(){
    service.getById(...);
    ...
    DBContext.setDBKey("master");//先切换到主库
    doSomeThing();
}

在自定义切面中增加对@Transactional注解的判断,提前使用master库

if (methdo.isAnnotationPresent(Transactional.class)) {
    log.info("当前执行的库:" + MASTER);
    DynamicDataSourceContextHolder.push(MASTER);
    return;
}

在doSomeThing()上使用@Transactional(propagation = Propagation.REQUIRES_NEW)

@DS("master")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomeThing(){
    service.save()
    service.update()
}

自定义切面时间测试

dynamic-datasource 强烈建议只注解在service实现上 , 故测试自定义aop在切换数据源时所消耗的时间。

测试过程:100 get操作的基础上使用不同的切换数据源方式,比较aop切换数据源的耗时情况

作者:Jeebiz  创建时间:2023-01-20 00:29
最后编辑:Jeebiz  更新时间:2024-08-22 10:22