springboot框架项目实践应用九(多数据源路由)

1.引言

在项目中,有这么一些场景,需要去考虑数据源路由的事情,比如说

  • 因为系统绝对并发量太高,单个数据库实例难以应对,需要分库
  • 因为系统数据量太大,单表难以应对,需要分表
  • 大多数系统呈现出读多写少业务特点,为了缓解存储层压力,通过搭建主从集群(一写多读),实现读写分离,提升系统响应能力,进一步提升用户体验
  • 报表类应用,可能会需要聚合多个数据源,从而获取数据

你看到了,以上不管是分库、分表、还是主从实现读写分离,都表现出了一个应用,对应多个数据库实例的场景。

那么这个时候,我们就需要去考虑多数据源路由的事情了。在具体实现方案上,有两种比较常见的选择方案

  • 自己在应用层面,去实现数据源路由,比如说spring框架提供了AbstractRoutingDataSource,我们只要扩展它就可以了
  • 或者在应用层,与数据源之间,增加代理proxy的实现方案,比如说业界使用较多的MyCat、ShardingSphere等

这两种方案,具体该如何去选择呢?如果我们是中小型的团队,应用规模也不是很大,那么适合选择在应用层面实现数据源路由的方案,主要理由

  • 运维、DBA小伙伴成员不多,且对proxy方案中的代理中间件没有深入的研究,没有办法做到一旦碰到问题,可以迅速解决(甚至可能解决不了)
  • 在应用层面自己实现的数据源路由,代码都是自己的写的,一旦有什么问题,研发小伙伴直接就解决了

基于以上,反过来我们就可以考虑采用proxy的代理方案了。方便你理解,还是上一个图吧,典型的主从集群,读写分离架构

2.准备环境

通过以上描述,我们知道在实现多数据路由上,有两种可选的方案,本篇文章我们分享方案一的实现,即扩展spring提供的AbstractRoutingDataSource。

首先来准备环境,我在本地准备了两个数据库实例

  • 127.0.0.1:3320 training
  • 127.0.0.1:3310 user-center

这两个库虽然本身没有直接的关系,我们假设这是一个报表类应用(聚合多个数据源),且我们的重点是实现在数据源之间的路由,因此不影响。

在本案例的实现中

  • 将user-center库作为读库,即当有读操作的请求进来,将请求路由到user-center库
  • 将training库作为写库,即当有更新请求进来,将请求路由到training库

导入依赖

xml 复制代码
<dependencies>
    <!--web 依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--aop 依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!--jdbc 依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!--mybatis 依赖-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <!--druid 依赖-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.9</version>
    </dependency>
    <!-- mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!--lombok依赖-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

3.核心配置

本案例持久层,选择的是mybatis框架,相关核心配置如下

3.1.application.yml

yaml 复制代码
server:
  port: 8080
spring:
  application:
    name: follow-me-springboot-multidatasource

#数据源配置
mysql:
  datasource:
    type-aliases-package: cn.edu.anan.entity
    mapper-locations: classpath:mybatis/mapper/*Mapper.xml
    config-location: classpath:mybatis/sqlMapConfig.xml
    write:
      url: jdbc:mysql://127.0.0.1:3320/training?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
      username: root
      password: admin
      driver-class-name: com.mysql.cj.jdbc.Driver
    read:
      url: jdbc:mysql://127.0.0.1:3310/user-center?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
      username: root
      password: admin
      driver-class-name: com.mysql.cj.jdbc.Driver

3.2.sqlMapConfig.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 是否开启缓存 -->
        <setting name="cacheEnabled" value="true" />
        <!-- 是否开启延迟加载 -->
        <setting name="lazyLoadingEnabled" value="true" />
        <!-- 是否开启延迟加载 -->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!-- 是否允许单条sql 返回多个数据集 -->
        <setting name="multipleResultSetsEnabled" value="true" />
        <!-- 是否开启列别名 -->
        <setting name="useColumnLabel" value="true" />
        <!-- 是否使用JDBC自增主键  -->
        <setting name="useGeneratedKeys" value="false" />
        <!-- MyBatis 自动映射策略,NONE:不隐射 PARTIAL:部分  FULL:全部  -->
        <setting name="autoMappingBehavior" value="PARTIAL" />
        <!-- 设置默认执行器Executor -->
        <setting name="defaultExecutorType" value="SIMPLE" />
        <!--语句超时时间-->
        <setting name="defaultStatementTimeout" value="25" />
        <!--默认抓取大小-->
        <setting name="defaultFetchSize" value="100" />
        <!--分页相关-->
        <setting name="safeRowBoundsEnabled" value="false" />
        <!-- 使用驼峰命名转换 -->
        <setting name="mapUnderscoreToCamelCase" value="true" />
        <!-- 设置本地缓存范围 session:就会有数据的共享 -->
        <setting name="localCacheScope" value="SESSION" />
        <!-- 默认为OTHER,为了解决oracle插入null报错的问题要设置为NULL -->
        <setting name="jdbcTypeForNull" value="NULL" />
        <!--延迟加载触发方法列表-->
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
    </settings>

</configuration>

3.3.项目代码结构

4.核心代码

扩展AbstractRoutingDataSource实现多数据源之间路由,核心是

  • 配置各个数据源
  • 通过ThreadLocal实现线程数据源上下文绑定
  • 通过AOP进行线程上下文数据源路由信息设置

4.1.数据源配置DataSourceConfig

java 复制代码
/**
 * 数据源配置
 *
 * @author ThinkPad
 * @version 1.0
 */
@Configuration
@MapperScan(basePackages = "cn.edu.anan.dao", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {

    /**
     * 包扫描别名
     */
    @Value("${mysql.datasource.type-aliases-package}")
    private String typeAliasesPackage;

    /**
     *mapper映射文件位置
     */
    @Value("${mysql.datasource.mapper-locations}")
    private String mapperLocation;

    /**
     *mybatis配置文件位置
     */
    @Value("${mysql.datasource.config-location}")
    private String configLocation;

    /**
     * 写数据源
     * @return
     */
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "mysql.datasource.write")
    public DataSource writeDataSource() {
        return new DruidDataSource();
    }

    /**
     * 读数据源
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = "mysql.datasource.read")
    public DataSource readDataSource() {
        return new DruidDataSource();
    }

    /**
     * 配置sqlSessionFactory
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();

        bean.setDataSource(routingDataSource());
        bean.setTypeAliasesPackage(typeAliasesPackage);

        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resolver.getResources(mapperLocation));
        bean.setConfigLocation(resolver.getResource(configLocation));

        return bean.getObject();
    }

    /**
     * 设置数据源路由表
     * @return
     */
    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>(2);

        targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
        targetDataSources.put(DbContextHolder.READ, readDataSource());

        proxy.setDefaultTargetDataSource(writeDataSource());
        proxy.setTargetDataSources(targetDataSources);

        return proxy;
    }

    /**
     * 配置事务管理器
     * @return
     */
    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        return new DataSourceTransactionManager(routingDataSource());
    }

}

4.2.数据源线程上下文DbContextHolder

java 复制代码
/**
 * 数据源上下文环境
 *
 * @author ThinkPad
 * @version 1.0
 */
@Slf4j
public class DbContextHolder {

    /**
     * 写数据源标识
     */
    public static final String WRITE = "write";
    /**
     * 读数据源标识
     */
    public static final String READ = "read";

    /**
     * 本地线程绑定
     */
    private static ThreadLocal<String> contextHolder= new ThreadLocal<>();

    /**
     * 设置数据源类型
     * @param dbType
     */
    public static void setDbType(String dbType) {
        if (dbType == null) {
            log.error("dbType为空");
            throw new NullPointerException();
        }
        log.info("设置dbType为:{}",dbType);
        contextHolder.set(dbType);
    }

    /**
     * 获取数据源类型
     * @return
     */
    public static String getDbType() {
        return contextHolder.get() == null ? WRITE : contextHolder.get();
    }

    /**
     * 清除ThreadLocal
     */
    public static void clearDbType() {
        contextHolder.remove();
    }

}

4.3.扩展数据源路由MyAbstractRoutingDataSource

java 复制代码
/**
 * 数据源路由,扩展AbstractRoutingDataSource
 *
 * @author ThinkPad
 * @version 1.0
 */
@Slf4j
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource{

    /**
     * 返回数据源路由key
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        String dbKey = DbContextHolder.getDbType();
        if (dbKey == DbContextHolder.WRITE) {
            log.info("当前更新动作,走主库");
            return dbKey;
        }

        log.info("当前读取操作,走从库");
        return DbContextHolder.READ;
    }
}

4.4.注解DataSourceSwitcher

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSwitcher {
    /**
     * 默认数据源
     * @return
     */
    String value() default "write";

    /**
     * 清除
     * @return
     */
    boolean clear() default true;

}

4.5.切面ReadOnlyAspect

java 复制代码
/**
 * 读数据源切面
 *
 * @author ThinkPad
 * @version 1.0
 */
@Aspect
@Component
@Slf4j
public class ReadOnlyAspect implements Ordered{

    /**
     * 线程上下文设置读数据源
     * @param pjp
     * @param read
     * @return
     * @throws Throwable
     */
    @Around("@annotation(read)")
    public Object setRead(ProceedingJoinPoint pjp, DataSourceSwitcher read) throws Throwable{
        try{
            DbContextHolder.setDbType(DbContextHolder.READ);
            return pjp.proceed();
        }finally {
            DbContextHolder.clearDbType();
            log.info("清除threadLocal");
        }
    }

    /**
     * 顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

5.使用案例

在数据源路由配置中,设置了默认的数据源是:写数据源

如果是写入操作,默认应用代码不需要特殊处理;如果读操作,应用代码方法上,需要加上@DataSourceSwitcher(value="read")注解,比如

那么在运行时,通过切面绑定线程上下文数据源信息

准备两个测试案例

  • 写入订单
  • 读取全部用户列表
java 复制代码
/**
 * controller
 *
 * @author ThinkPad
 * @version 1.0
 */
@RestController
@RequestMapping("route")
@Slf4j
public class MultiDataSourceController {

    @Autowired
    private UserService userService;

    @Autowired
    private OrderService orderService;

    /**
     * 写数据源测试:写入一个订单
     * @param order
     * @return
     */
    @RequestMapping("write")
    public Order write(@RequestBody Order order){
        orderService.insertOne(order);
        return order;

    }

    /**
     * 读数据源测试:查询全部用户列表数据
     * @return
     */
    @RequestMapping("read")
    public List<User> read(@RequestBody User user){
        log.info("查询条件:{}", user);
        return userService.selectAll(user);
    }

启动应用,分别访问端点

观察控制台输出

案例输出读操作,走从库;写操作,走主库。我们看到已经实现多数据源路由,最后本文源码,请参考:gitee.com/yanghouhua/...,子模块: follow-me-springboot-multidatasource

相关推荐
yechaoa1 小时前
【揭秘大厂】技术专项落地全流程
android·前端·后端
Sendingab1 小时前
3.8 Spring Boot监控:Actuator+Prometheus+Grafana可视化
spring boot·grafana·prometheus
逛逛GitHub1 小时前
推荐 10 个受欢迎的 OCR 开源项目
前端·后端·github
gzgenius1 小时前
独立部署DeepSeek 大语言模型(如 DeepSeek Coder、DeepSeek LLM)可以采用什么框架?
人工智能·学习·架构·deepseek
loveking61 小时前
SpringBoot实现发邮件功能+邮件内容带模版
java·spring boot·后端
liuyang___2 小时前
Spring boot+mybatis的批量删除
java·spring boot·mybatis
szxinmai主板定制专家2 小时前
基于FPGA的3U机箱模拟量高速采样板ADI板卡,应用于轨道交通/电力储能等
arm开发·人工智能·fpga开发·架构
ningmengjing_2 小时前
django小案例-2
后端·python·django
Asthenia04122 小时前
Spring 中 Bean 初始化过程的扩展点详解
后端
Asthenia04122 小时前
从单体到微服务:接口鉴权的演进与优化
后端