SpringBoot集成多数据源的几种方式

唠嗑部分

前置说明:本次多数据源整合依赖于之前搭建的MySQL主从复制

前置文章:MySQL搭建主从复制

言归正传
1、多环境源环境准备

作为后端开发者,使用多数据源是必不可少的技能

登录MySQL主从,创建开发账号

# 创建账号并赋予用户spring-boot-dynamic-datasource-demo库的所有操作权限
grant all on `spring-boot-dynamic-datasource-demo`.* to 'dynamic-datasource-user'@'%' identified by '@Aa1234567890';
# 刷新权限
flush privileges;
2、原理说明

Mybatis在进行数据库操作的时候,最终获取的连接是由DataSource接口的getConnection方法获取连接,整合多数据源的目的就是根据业务场景的不同,使用不同的数据源进行操作,举个例子,

比如MySQL的读写分离,在进行查询的时候,我们需要让MyBatis使用从库,进行增删改的时候,需要让MyBatis使用主库,那我们可不可以从这个方法下手呢(自定义一个DataSource的实现类,重写getConnection()方法,根据需求切换数据源,返回不同的Connection)

这个方法也贴一下

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
    throws SQLException;
}

这个肯定是不行的,虽然可以达到读写分离的效果,(亲测可行),但是实现这个接口需要重写很多方法,我们只能处理getConnection这个方法,其他的方法我们无法处理,如果Spring调用其他方法的话,那后果就可想而知了,其实Spring已经考虑到开发者会整合多数据源,帮我们处理好了

AbstractRoutingDataSource这个类是Spring帮我们提供的多数据源整合的抽象类,下面我们简单说说这个类,我截取了主要的一部分代码,

主要的是3个成员变量,一个抽象方法

targetDataSources:所有数据源,即我们的所有主从节点,需要我们手动设置

defaultTargetDataSource:默认数据源,当根据determineCurrentLookupKey()返回的key找不到就是默认的,看下图,很清楚了

image-20230427131502343

resolvedDataSources:这个变量不需要我们手动配置,在afterPropertiesSet()方法中将targetDataSources复制了一份给resolvedDataSources,至于为什么,不需要我们操心

determineCurrentLookupKey():这个方法需要我们重写,主要是给一个数据源的标识,比如W–写, R–读

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;
    
    @Nullable
    private Object defaultTargetDataSource;
    
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    
    @Nullable
    protected abstract Object determineCurrentLookupKey();
    
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }
    
}

原理图

image-20230427131637667

3、SpringBoot整合多数据源-Mybatis插件方式

前置知识说明

这种方式需要对于Mybatis及插件有一定了解程度

上面说了AbstractRoutingDataSource类,中有个determineCurrentLookupKey()方法,需要我们返回一个数据源标识,那如何去实现呢,进行Select的时候设置为R,增删改的时候设置为W,这里使用Mybatis插件的方式实现

创建项目&导入依赖

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.4</version>
    </dependency>
    <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>persistence-api</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

application.yml

server:
  port: 2022
spring:
  datasource-w:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://10.10.10.10/spring-boot-dynamic-datasource-demo?characterEncoding=utf8&useSSL=false
    username: dynamic-datasource-user
    password: "@Aa1234567890"
  datasource-r:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://10.10.10.20/spring-boot-dynamic-datasource-demo?characterEncoding=utf8&useSSL=false
    username: dynamic-datasource-user
    password: "@Aa1234567890"
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
  type-aliases-package: com.cxs.model
  mapper-locations: classpath:mapper/*.xml

DynamicDataSource

AbstractRoutingDataSourcede实现类,主要做两件事,

  1. 设置所有数据源及默认数据源
  2. 重写determineCurrentLookupKey(),返回数据源标识
/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Primary
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Autowired
    private DataSource master;

    @Autowired
    private DataSource slave;
    
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDatasourceLocalUtil.getLocalCache();
    }

    @Override
    public void afterPropertiesSet() {
        super.setDefaultTargetDataSource(master);
        Map<Object, Object> map = new HashMap<>();
        map.put(TypeEnum.W.name(), master);
        map.put(TypeEnum.R.name(), slave);
        super.setTargetDataSources(map);
        super.afterPropertiesSet();
    }
}

DynamicDatasourceLocalUtil

需要记录全局的数据源标识,这里使用ThreadLocal

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
public class DynamicDatasourceLocalUtil {
    private static final ThreadLocal<String> localCache = new ThreadLocal<>();

    public static void setLocalCache(TypeEnum typeEnum) {
        localCache.set(typeEnum.name());
    }

    public static String getLocalCache() {
        return localCache.get();
    }

    public static void removeLocalCache() {
        localCache.remove();
    }
}

DynamicDataSourcePlugin

Mybatis插件,帮助我们设置数据源标识,插件的使用方式就不多说了,这里将数据源标识封装成一个枚举中,规范代码,防止硬编码

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class DynamicDataSourcePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        // SqlCommandType就是对数据库操作的类型
        SqlCommandType sqlCommandType = statement.getSqlCommandType();
        if (sqlCommandType.equals(SqlCommandType.SELECT)) {
            DynamicDatasourceLocalUtil.setLocalCache(TypeEnum.R);
        } else {
            DynamicDatasourceLocalUtil.setLocalCache(TypeEnum.W);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
}

测试接口

/user/list: 查询用户列表,查询操作,预期走从库,(10.10.10.20)

/user/insert: 插入一条数据,DML操作,预期走主库,(10.10.10.10)

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/list")
    public Object list(){
        return userService.list();
    }

    @GetMapping("/insert")
    public Object insert(){
        User user = new User();
        user.setUserName("admin");
        return userService.insert(user);
    }
}
/user/list
1
/user/insert
2

验证通过

补充

上述功能已经实现,但是存在一个问题,我们使用了ThreadLocal,弊端就不多说了,解决方式,新建一个拦截器

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
public class ClearDynamicKeyInterCeptor implements HandlerInterceptor {
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理DynamicLocal
        DynamicDatasourceLocalUtil.removeLocalCache();
    }
}
4、SpringBoot整合多数据源-Aop方式

前置知识说明

这种方式需要对于Spring的切面有一定了解程度

上面说了使用mybatis插件实现数据源切换,那么有没有弊端,当然是有的,举个例子,首先表明一下,多数据源不一定都是主从

有这个场景,有两个数据源,分别是order库,shop库。订单库和商品库,查询商品的时候需要查询shop库,下订单的时候我要连order库,这种业务场景就不适用于插件方式了

总结一下哈:

插件方式对于主从结构来说,比较方便,但是多个数据源就会有瓶颈

AOP比较灵活,适用于多个数据源的业务场景

数据库环境搭建

aop方式就不适用主从结构了,实际场景应该是两个不同机器的主库,我这为了方便,同一台机器创建连个库,一个shop库,一个order库,想想哈,其实是一样的

在主库执行

create database `spring-boot-dynamic-datasource-aop-order` character set 'utf8mb4';
create database `spring-boot-dynamic-datasource-aop-shop` character set 'utf8mb4';
grant all on `spring-boot-dynamic-datasource-aop-order`.* to 'dynamic-datasource-user'@'%' identified by '@Aa1234567890';
grant all on `spring-boot-dynamic-datasource-aop-shop`.* to 'dynamic-datasource-user'@'%' identified by '@Aa1234567890';
flush privileges;

在spring-boot-dynamic-datasource-aop-shop库建立t_product表

create table t_product(
  id int primary key auto_increment,
    product_name varchar(50),
    price int
) comment '商品表';
insert into t_product values(1, '华为手机', 3000);

在spring-boot-dynamic-datasource-aop-order表创建t_order表

create table t_order(
  id int primary key auto_increment,
    product_id int,
    create_time datetime
) comment '订单表';
insert into t_order values(1, 1, '2022-12-27 12:30:15');

创建项目&导入依赖

依赖于上述类似,新增aop依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

application.yml

当前场景使用两个主库

server:
  port: 2022
spring:
  datasource-order:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://10.10.10.10/spring-boot-dynamic-datasource-aop-order?characterEncoding=utf8&useSSL=false
    username: dynamic-datasource-user
    password: "@Aa1234567890"
  datasource-shop:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://10.10.10.10/spring-boot-dynamic-datasource-aop-shop?characterEncoding=utf8&useSSL=false
    username: dynamic-datasource-user
    password: "@Aa1234567890"
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
  type-aliases-package: com.cxs.model
  mapper-locations: classpath:mapper/*.xml

MybatisConfig

多个数据源配置,配置两个(可自由配置)

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Configuration
@MapperScan(basePackages = "com.cxs.mapper")
public class MybatisConfig {

    @Bean("dataSourceOrder")
    @ConfigurationProperties(prefix = "spring.datasource-order")
    public DataSource dataSourceOrder(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean("dataSourceShop")
    @ConfigurationProperties(prefix = "spring.datasource-shop")
    public DataSource dataSourceShop(){
        return DruidDataSourceBuilder.create().build();
    }

}

DynamicDataSource

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Primary
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Autowired
    private DataSource dataSourceShop;

    @Autowired
    private DataSource dataSourceOrder;

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDatasourceLocalUtil.getLocalCache();
    }

    @Override
    public void afterPropertiesSet() {
        Map<Object, Object> map = new HashMap<>();
        super.setDefaultTargetDataSource(dataSourceShop);
        map.put(TypeEnum.T_ORDER, dataSourceOrder);
        map.put(TypeEnum.T_SHOP, dataSourceShop);
        super.setTargetDataSources(map);
        super.afterPropertiesSet();
    }
}

自定义注解&切面实现

aop方式可以在方法执行完毕后,清除ThreadLocal,无需建立拦截器处理

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceType {
    TypeEnum value();
}
/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Aspect
@Component
public class DynamicDataSourceAspect {

    @Pointcut(value = "within(com.cxs.service.impl.*)")
    public void pointCut(){}

    @Around(value = "pointCut() && @annotation(dataSourceType)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint, DataSourceType dataSourceType){
        Object result = null;
        try {
            // 设置当前处理线程的数据源标识
            DynamicDatasourceLocalUtil.setLocalCache(dataSourceType.value());
            result = proceedingJoinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            // 清除ThreadLocal,防止内存泄漏
            DynamicDatasourceLocalUtil.removeLocalCache();
        }
        return result;
    }
}

业务实现

以ProductServiceImpl为例

@DataSourceType(TypeEnum.T_SHOP) 指定当前业务的数据源

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    @DataSourceType(TypeEnum.T_SHOP)
    public List<Product> productList() {
        return productMapper.selectList();
    }
}

GlobalController

两个接口:

/shop/list:使用spring-boot-dynamic-datasource-aop-shop库

/order/add: 使用spring-boot-dynamic-datasource-aop-order库

/*
 * @Project:spring-boot-dynamic-datasource-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@RestController
public class GlobalController {

    @Autowired
    private OrderService orderService;

    @Autowired
    private ProductService productService;

    @GetMapping("/shop/list")
    public Object list(){
        return productService.productList();
    }

    @GetMapping("/order/add")
    public Object add(){
        Order order = new Order();
        order.setProductId(1);
        order.setCreateTime(LocalDateTime.now());
        return orderService.addOrder(order);
    }
}
/shop/list
图片
/order/add
图片
关于后续

整合多数据源的方式不止这些,由于篇幅原因,就不继续了,完整笔记&整合源码已放置公众号后台,自己获取即可,目录如下

image-20230427132922595

1、制作不易,一键三连再走吧,您的支持永远是我最大的动力!

3、Java全栈技术交流Q群:941095490,欢迎您的加入,案例代码及笔记见群文件!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈小白.

感谢老板,祝老板今年发大财!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值