当前位置: 首页 > 工具软件 > Daisy > 使用案例 >

【daisy-framework】SpringBoot+MyBatis+Druid 多数据源

洪河
2023-12-01

前言

Github:https://github.com/yihonglei/daisy-framework/tree/master/daisy-springboot-framework(daisy工程)

概述

多数据源主要解决高并发读写分离或多库在一个应用处理系统业务。

多数据源中心思想一样,但是根据自己业务情况有很多种写法实现多数据源,也有很多知名或不知名的

开源分布式 ORM 框架设计时候就按照多数据源设计的,使用时候天然就支持。

多数据源个人觉得要满足如下基本使用功能:

1、有多个业务库数据源时,需要能够明确切换到具体的操作业务数据库;

2、主从读写分离,默认写走主库,读走从库,同时可以指定读强制走主库,因为有时候为了避免主从延迟,

部分读操作能够支持指定强制走主库;

一 前置准备

假如我有两个库,jpeony 和 order 库,其中 jpeony 业务用的一主两从,order 用的一主一从,具体配置格式如下。

application.yml

数据源配置文件,未放置 druid 的其他属性,具体代码见 Github。

# DataSource
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    druid:
      jpeony:
        master:
          url: jdbc:mysql://127.0.0.1:3306/jpeony?characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456
        slave01:
          url: jdbc:mysql://127.0.0.1:3306/jpeony?characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456
        slave02:
          url: jdbc:mysql://127.0.0.1:3306/jpeony?characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456
      order:
        master:
          url: jdbc:mysql://127.0.0.1:3306/order?characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456
        slave:
          url: jdbc:mysql://127.0.0.1:3306/order?characterEncoding=utf-8&useSSL=false
          username: root
          password: 123456

DruidProperties

SpringBoot 工程读取 druid 的相关属性配置的值。

package com.jpeony.common.config.properties;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * druid 配置属性
 *
 * @author yihonglei
 */
@Configuration
public class DruidProperties {
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    public DruidDataSource dataSource(DruidDataSource datasource) {
        /* 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /* 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /* 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /* 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /*
         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
         */
        datasource.setValidationQuery(validationQuery);
        /* 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
        datasource.setTestWhileIdle(testWhileIdle);
        /* 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /* 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }
}

二 多数据源配置

DataSourceTypeEnum

数据源枚举。

package com.jpeony.common.enums;

/**
 * @author yihonglei
 */
public enum DataSourceTypeEnum {
    JPEONY_MASTER,
    JPEONY_SLAVE01,
    JPEONY_SLAVE02,
    ORDER_MASTER,
    ORDER_SLAVE
}

MultipleDataSourceContextHolder

线程持有的数据源设置。

package com.jpeony.common.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author yihonglei
 */
public class MultipleDataSourceContextHolder {
    public static final Logger logger = LoggerFactory.getLogger(MultipleDataSourceContextHolder.class);
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceType(String dsType) {
        CONTEXT_HOLDER.remove();
        CONTEXT_HOLDER.set(dsType);
    }

    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

MultipleDataSource

多数据源路由器。

package com.jpeony.common.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author yihonglei
 */
public class MultipleDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return MultipleDataSourceContextHolder.getDataSourceType();
    }
}

DruidDataSourceConfig

druid 多数据源配置,将配置文件中的数据源构建为 DataSource,最后放入 AbstractRoutingDataSource 路由中。

package com.jpeony.common.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.jpeony.common.config.properties.DruidProperties;
import com.jpeony.common.datasource.MultipleDataSource;
import com.jpeony.common.enums.DataSourceTypeEnum;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.servlet.*;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author yihonglei
 */
@Configuration
public class DruidDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.jpeony.master")
    public DataSource jpeonyMasterDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.jpeony.slave01")
    public DataSource jpeonySlave01DataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.jpeony.slave02")
    public DataSource jpeonySlave02DataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.order.master")
    public DataSource orderMasterDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.order.slave")
    public DataSource orderSlaveDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean(name = "multipleDataSource")
    @Primary
    public MultipleDataSource dataSource(DataSource jpeonyMasterDataSource, DataSource jpeonySlave01DataSource,
                                         DataSource jpeonySlave02DataSource, DataSource orderMasterDataSource,
                                         DataSource orderSlaveDataSource) {
        // 数据源
        Map<Object, Object> targetDataSources = new HashMap<>(16);
        targetDataSources.put(DataSourceTypeEnum.JPEONY_MASTER, jpeonyMasterDataSource);
        targetDataSources.put(DataSourceTypeEnum.JPEONY_SLAVE01, jpeonySlave01DataSource);
        targetDataSources.put(DataSourceTypeEnum.JPEONY_SLAVE02, jpeonySlave02DataSource);
        targetDataSources.put(DataSourceTypeEnum.ORDER_MASTER, orderMasterDataSource);
        targetDataSources.put(DataSourceTypeEnum.ORDER_SLAVE, orderSlaveDataSource);

        // 路由数据源
        MultipleDataSource multipleDataSource = new MultipleDataSource();
        multipleDataSource.setTargetDataSources(targetDataSources);
        return multipleDataSource;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Bean
    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
        // 获取web监控页面的参数
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        // 提取common.js的配置路径
        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
        final String filePath = "support/http/resources/js/common.js";
        // 创建filter进行过滤
        Filter filter = new Filter() {
            @Override
            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
            }

            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                    throws IOException, ServletException {
                chain.doFilter(request, response);
                // 重置缓冲区,响应头不会被重置
                response.resetBuffer();
                // 获取common.js
                String text = Utils.readFromResource(filePath);
                // 正则替换banner, 除去底部的广告信息
                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
                text = text.replaceAll("powered.*?shrek.wang</a>", "");
                response.getWriter().write(text);
            }

            @Override
            public void destroy() {
            }
        };
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns(commonJsPattern);
        return registrationBean;
    }
}

三 AOP 拦截动态切换

在正式分析路由实现前,需要先明确自定义的注解。

这里自己定义了一个 DB 注解,用于在 Mapper 上面指定库,还要一个 UseMaster 注解,用于强制走主库指定。

package com.jpeony.common.annotation;

import java.lang.annotation.*;

/**
 * 修饰 Mapper 接口,拦截指定数据源
 *
 * @author yihonglei
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DB {
    /**
     * 数据源名称
     */
    String name() default "";
}
package com.jpeony.common.annotation;

import java.lang.annotation.*;

/**
 * 强制使用主库
 *
 * @author yihonglei
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UseMaster {
}

Mapper 上关于 DB 和 UseMaster 注解的使用。

package com.jpeony.core.mapper;

import com.jpeony.common.annotation.DB;
import com.jpeony.common.annotation.UseMaster;
import com.jpeony.common.constant.DBConstant;
import com.jpeony.core.pojo.domain.TestDO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * Mapper接口
 *
 * @author yihonglei
 */
@DB(name = DBConstant.JPEONY)
public interface TestMapper {
    /**
     * MyBatis 注解形式
     */
    @Select("select * from test where id = #{id}")
    TestDO queryTestById(@Param("id") int id);

    @UseMaster
    @Select("select * from test where id = #{id}")
    TestDO queryTestByIdMaster(@Param("id") int id);

    @Update("update test set test_name = #{testName} where id = #{id}")
    int updateTestById(@Param("id") int id, @Param("testName") String testName);

    /**
     * MyBatis XML方式
     */
    TestDO queryTestByIdXml(@Param("id") int id);
}

DBConstant 用于配置对应的数据库名,在 Mapper 上使用的时候,需要进行显示指定用的库,因为在是路由的依据。

package com.jpeony.common.constant;

/**
 * 数据库名
 *
 * @author yihonglei
 */
public class DBConstant {
    public final static String JPEONY = "jpeony";

    public final static String ORDER = "order";
}

DataSourceAop

根据上面这些配置,通过 AOP 拦截 Mapper,然后路由切换数据源的逻辑。

1、aop 拦截 mapper 下的所有类方法;

2、通过反射,获取 Mapper 上 DB 指定的数据库名,用于匹配对应配置的主库或从库;

3、如果方法上使用了 UseMaster 强制走主或者 update、insert、delete 写操作,直接通过数据库名匹配找到对应的主库,

如果是走从库的,需要匹配出数据库对应的多个从库,然后随机选择一个从库,实现强制主或写操作走主库,其余默认走从库操作。

4、这里还做了一个兼容,当不配置主库的时候,默认选择一个从库做主库使用,

其实当没配置主库的时候,可以强制抛异常处理,也没有必要切换到从库连接,这个切换应该是由 数据库主从本身自己去做。

5、这个路由有一个弊端,当使用 MyBatis 的 xml 配置方式时,如果是写操作,需要显示的去指定走主库,要不然切不到主库去。

处理方式可以从方法名着手,比如 updateXXX,insertXXX,deleteXXX,editXXX,这样去命名方法名然后切主库,

就是写起代码来规范太多了,规范太多烦人。

package com.jpeony.common.datasource;

import com.jpeony.common.annotation.DB;
import com.jpeony.common.annotation.UseMaster;
import com.jpeony.common.enums.DataSourceTypeEnum;
import com.jpeony.common.enums.ErrorCodeEnum;
import com.jpeony.common.exception.DBException;
import com.jpeony.common.utils.MatchUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Update;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Random;

/**
 * @author yihonglei
 */
@Aspect
@Order(-1)// 在 Spring 事务生成代理对象之前指定数据源
@Component
public class DataSourceAop {
    protected Logger logger = LoggerFactory.getLogger(getClass());
    private final Random random = new Random();

    @Pointcut("execution(* com.jpeony.core.mapper..*.*(..))")
    public void dsPointCut() {
    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        String targetDataSource = getTargetDataSource(point);
        MultipleDataSourceContextHolder.setDataSourceType(targetDataSource);

        try {
            return point.proceed();
        } finally {
            MultipleDataSourceContextHolder.clearDataSourceType();
        }
    }

    private String getTargetDataSource(ProceedingJoinPoint point) {
        String dbName = getDBName(point).toUpperCase();
        boolean useMaster = getUseMaster(point);
        String targetDataSource = StringUtils.EMPTY;

        DataSourceTypeEnum[] values = DataSourceTypeEnum.values();
        ArrayList<String> slaves = new ArrayList<>(values.length);
        for (DataSourceTypeEnum dst : values) {
            if (useMaster) {
                boolean match = MatchUtils.matchDataSource(dbName + MatchUtils.PATTERN_MATCH_MASTER, dst.name());
                if (match) {
                    targetDataSource = dst.name();
                    break;
                }
            } else {
                // Match all slaves
                boolean match = MatchUtils.matchDataSource(dbName + MatchUtils.PATTERN_MATCH_SLAVE, dst.name());
                if (match) {
                    slaves.add(dst.name());
                }
            }
        }

        if (StringUtils.isNotBlank(targetDataSource)) {
            return targetDataSource;
        }

        if (CollectionUtils.isEmpty(slaves)) {
            throw new DBException(ErrorCodeEnum.DATA_SOURCE_ERROR);
        }

        return slaves.get(random.nextInt(slaves.size()));
    }

    private String getDBName(ProceedingJoinPoint point) {
        Class<?> targetClass = point.getTarget().getClass();
        DB dbName = AnnotationUtils.findAnnotation(targetClass, DB.class);
        if (dbName == null) {
            throw new DBException(targetClass.getName() + ", no specified database");
        }
        return dbName.name();
    }

    private boolean getUseMaster(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        UseMaster useMaster = method.getAnnotation(UseMaster.class);
        Update update = method.getAnnotation(Update.class);
        Insert insert = method.getAnnotation(Insert.class);
        Delete delete = method.getAnnotation(Delete.class);

        return (useMaster != null || update != null || insert != null || delete != null);
    }
}

 四 测试

package com.jpeony.test.mapper;

import com.jpeony.core.mapper.TestMapper;
import com.jpeony.core.pojo.domain.TestDO;
import com.jpeony.test.BaseServletTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Mapper测试
 *
 * @author yihonglei
 */
@Slf4j
public class MapperTest extends BaseServletTest {
    @Autowired
    private TestMapper testMapper;

    @Test
    public void testAnnotation() {
        TestDO testDO = testMapper.queryTestById(1);
        log.info("testDO annotation={}", testDO);
    }

    @Test
    public void testUseMaster() {
        TestDO testDO = testMapper.queryTestByIdMaster(1);
        log.info("testDO useMaster={}", testDO);
    }

    @Test
    public void testUpdate() {
        int i = testMapper.updateTestById(1, "oneone");
        log.info("testUpdate={}", i);
    }

    @Test
    public void testXml() {
        TestDO testDO = testMapper.queryTestByIdXml(1);
        log.info("testDO xml={}", testDO);
    }
}

五 总结

1、如果要新在配置一个数据源需要修改那些地方?

a. application.yml 按要求配置上你的数据源;

b. DBConstant 定义数据库名,比如 X;

c. DataSourceTypeEnum 加上你的数据源类型主从命名,X_MASTER,X_SLAVE01,X_SLAVE02 依次类推;

d. DruidDataSourceConfig 将新数据源构建 DataSource,加入到 数据源路由中;

e. Mapper 里面直接指定用就行了;

支持多个库数据源,支持一主多从,自动读写分离,也可强制走主,注意 Mapper XML 配置方式,

需要使用 UseMaster 强制指定走主,一般实际业务系统,单表处理业务多一些,用MyBatis注解方式会

更简洁方便些,像后台管理哪些关联大查询,用 MyBatis XML 好一些。

 

 

 类似资料: