如何用Mybatis-Plus优雅的实现多数据源动态切换?

1,710 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情 >>


哈喽,大家好,我是一条。

今天聊聊如何动态切换数据源,简单点说,就是一个服务配置多个数据库,在读写分离,分库分表都有应用。

假设有个用户表,,根据id水平分库分表,id为偶数,存在一个库,id为奇数存在另一个库,如何实现根据id查询用户详细的接口?

一块来看一下吧!

准备工作

1.一个 SpringBoot + Mybatis-Plus 的项目,我是用以前的练手项目改造的,本文不赘述建项目的过程。

2.准备两个库和用户表,sql 如下:

create table t_commerce_user
(
    id          bigint auto_increment comment '自增主键'
        primary key,
    username    varchar(64)   default ''                    not null comment '用户名',
    password    varchar(256)  default ''                    not null comment 'MD5 加密之后的密码',
    extra_info  varchar(1024) default ''                    not null comment '额外的信息',
    create_time datetime      default '0000-01-01 00:00:00' not null comment '创建时间',
    update_time datetime      default '0000-01-01 00:00:00' not null comment '更新时间',
    balance     bigint        default 0                     not null comment '余额',
    constraint username
        unique (username)
)
    comment '用户表_分表_1' charset = utf8;
    
INSERT INTO cloud_commerce_0.t_commerce_user (id, username, password, extra_info, create_time, update_time, balance) VALUES (2, 'test2', 'test', '{}', '2022-06-20 17:23:18', '2022-06-20 17:23:18', 0);
INSERT INTO cloud_commerce_0.t_commerce_user (id, username, password, extra_info, create_time, update_time, balance) VALUES (4, 'test4', 'test', '{}', '2022-06-20 17:23:18', '2022-06-20 17:23:18', 0);
INSERT INTO cloud_commerce_0.t_commerce_user (id, username, password, extra_info, create_time, update_time, balance) VALUES (6, 'test6', 'test', '{}', '2022-06-20 17:23:18', '2022-06-20 17:23:18', 0);
INSERT INTO cloud_commerce_0.t_commerce_user (id, username, password, extra_info, create_time, update_time, balance) VALUES (8, 'test8', 'test', '{}', '2022-06-20 17:23:18', '2022-06-20 17:23:18', 0);
INSERT INTO cloud_commerce_0.t_commerce_user (id, username, password, extra_info, create_time, update_time, balance) VALUES (10, 'test10', 'test', '{}', '2022-06-20 17:23:18', '2022-06-20 17:23:18', 0);

运行完成这个样子就行:

image-20220817165049961

配置和依赖

Mybatis-Plus 的多数据源需要再添加一个依赖:

 <!--动态数据源-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

数据源配置

server:
  port: 6001
  servlet:
    context-path: /commerce-user
spring:
  application:
    name: cloud-commerce-user
  datasource:
    # 动态数据源
    dynamic:
      primary: master
      strict: false
      datasource:
        master:
          url: jdbc:mysql://1.0.0.0:3306/cloud_commerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
          username: root
          password: 123
          type: com.zaxxer.hikari.HikariDataSource
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave0:
          url: jdbc:mysql://1.0.0.0:3306/cloud_commerce_0?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
          username: root
          password: 23
          type: com.zaxxer.hikari.HikariDataSource
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave1:
          url: jdbc:mysql://1.0.0.0:3306/cloud_commerce_1?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
          username: root
          password: 123
          type: com.zaxxer.hikari.HikariDataSource
          driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接池
    hikari:
      maximum-pool-size: 8
      minimum-idle: 4
      idle-timeout: 30000
      connection-timeout: 30000
      max-lifetime: 45000
      auto-commit: true
      pool-name: EcommerceHikariCP


mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:*/mapper/*.xml

master、slave0、slave1 可以自己定义名字。

基于注解的切换

Mybatis-Plus 提供了非常好用的注解来切换数据源,可以加在类或方法上。@DS("dsName")

UserService

做数据源策略和业务逻辑代码

@Service
@Slf4j
@RequiredArgsConstructor
public class UserService {
    private final CommerceUserMapper userMapper;
​
    private final UserDsService userDsService;
​
    public CommerceUser getUserById(String id) {
      
        return Long.parseLong(id) % 2 == 0
                ? userDsService.getUserByIdWithDbKey0(id)
               : userDsService.getUserByIdWithDbKey1(id);
​
    }
}

UserDsService

真正切换数据源的类

@Service
@Slf4j
@RequiredArgsConstructor
public class UserDsService {
​
    private final CommerceUserMapper userMapper;
​
    @DS("slave0")
    public CommerceUser getUserByIdWithDbKey0(String id) {
        return userMapper.selectById(id);
    }
​
    @DS("slave1")
    public CommerceUser getUserByIdWithDbKey1(String id) {
        return userMapper.selectById(id);
    }
}

注意:这两个类一定要分开,类似事务注解,采用的代理,不分开会失效。

测试

启动项目,先看下日志的变化:

数据源都添加进来了,看看能不能切换呢?

查询个奇数,再查个偶数,都有结果,说明可以切换:

如何更优雅?

看到这是不是以为本文已经结束了。

现在思考,假如我们有10个库(夸张了有点),UserService 一共10个方法,都需要分库,以前只需要写10个方法,现在得多写100个方法,这也太不优雅了,这得把人逼疯。

怎么解决呢?

不写注解就好了,全部动态编码,不管多少库,还是一个service,只是加一段逻辑判断。

简单点说就是把注解做的事,我们在代码里自己写。

@DS做了什么

这里简单说下背后的原理。

首先,项目启动的时候,把配置文件里数据源配置加载进来,存在一个map里,key就是我们自定义的master、slave0。

再真正执行查询前,会有一个拦截器,把注解的value,也就是数据源的key,存储到一个ThreadLocal里,用栈存储。

获取数据库连接的时候,直接拿栈顶的数据集配置,这样就正好是我们配置的。

最后记得清空ThreadLocal,防止内存泄漏。

先看代码:

public CommerceUser getUserById(String id) {
​
        DynamicDataSourceContextHolder.push(String.format("slave%s", Long.parseLong(id) % 2));
        CommerceUser user = userMapper.selectById(id);
        DynamicDataSourceContextHolder.clear();
​
        return user;
    }

简直太tm优雅了!

关于这块详细的源码分析参考:blog.csdn.net/labulaka24/…

点赞吧

\