龙空技术网

「项目纪实」——mybatisplus多数据源读写分离

8老月 148

前言:

现时大家对“mybatisplus读写分离”大体比较看重,朋友们都需要知道一些“mybatisplus读写分离”的相关资讯。那么小编同时在网上收集了一些对于“mybatisplus读写分离””的相关内容,希望同学们能喜欢,大家一起来学习一下吧!

为什么有这个需求

最近我们有个线上项目因为读写数据量比较大,导致数据库压力增大,所以现在打算上读写分离的方案。

怎么解决

我们用的框架是springboot+mybatisplus,而且是线上的项目,不能大量改造,各项成本太高,这是前提条件。

目前有2种方案,

1.直接引入mybatisplus的多数据源的方式,在方法或者类层面使用注解@DS("xxx"),这种方案呢比较灵活,但是我们线上很多代码有用了mybatisplus自己封装的单表增删改查接口,这些接口无法使用注解,单独去修改的话会导致改动的工作量比较大。

2.自定义数据源,通过mybatis的拦截器获取sql类型,查询语句走从库数据源,增删改走主库数据源。

对比了我们的需求,最终决定还是用第二个方式来做。

开始试验创建demo

建2个库 demo_master,demo_slave

建一个表 user

插入一些数据

```sql

CREATE TABLE user

(

id BIGINT(20) NOT NULL COMMENT '主键ID',

name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',

age INT(11) NULL DEFAULT NULL COMMENT '年龄',

email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',

PRIMARY KEY (id)

);

INSERT INTO user (id, name, age, email) VALUES

(1, 'Jone', 18, 'test1@baomidou.com'),

(2, 'Jack', 20, 'test2@baomidou.com'),

(3, 'Tom', 28, 'test3@baomidou.com'),

(4, 'Sandy', 21, 'test4@baomidou.com'),

(5, 'Billie', 24, 'test5@baomidou.com');

```

初始化一个maven项目,引入相关依赖

<parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.5.0</version>        <relativePath />    </parent><dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>                 <dependency>            <groupId>com.baomidou</groupId>            <artifactId>mybatis-plus-boot-starter</artifactId>            <version>3.4.3</version>        </dependency>        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>            <scope>runtime</scope>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <scope>provided</scope>        </dependency>    </dependencies>

配置数据源

spring:  datasource:    hikari:      master:        jdbc-url: jdbc:mysql://localhost:3306/demo01_master        username: root        password: 123456        driver-class-name: com.mysql.cj.jdbc.Driver       slave:        jdbc-url: jdbc:mysql://localhost:3306/demo01_slave        username: root        password: 123456        driver-class-name: com.mysql.cj.jdbc.Driver

生成一下entity,mapper,service等

接下来创建一个切换数据源的工具类DynamicDataSourceHolder

public class DynamicDataSourceHolder {    private static ThreadLocal<String> contextHolder = new ThreadLocal<>();    public static final String DB_MASTER = "master";    public static final String DB_SLAVE = "slave";     public static String getDbType() {        String db = contextHolder.get();        if (db == null) {            db = DB_MASTER;        }        return db;    }     public static void setDBType(String str) {        log.info("当前设置数据源为" + str);        contextHolder.set(str);    }     public static void clearDbType() {        contextHolder.remove();    } }

实现一个动态数据源 继承AbstractRoutingDataSource,这个类运行我们根据定义的规则选择当前的数据源

public class DynamicDataSource extends AbstractRoutingDataSource {    @Nullable    @Override    protected Object determineCurrentLookupKey() {        return DynamicDataSourceHolder.getDbType();    }}

添加一个拦截器,用来拦截执行sql

@Component@Slf4j//指定拦截哪些方法,update包括增删改@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,                RowBounds.class, ResultHandler.class }) })public class DynamicDataSourceInterceptor implements Interceptor {    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";    @Override    public Object intercept(Invocation invocation) throws Throwable {                boolean synchronizationActive = TransactionSynchronizationManager.isActualTransactionActive();        log.info("当前执行语句是否有事务:{}",synchronizationActive);        String lookupKey = DynamicDataSourceHolder.DB_MASTER;        if (!synchronizationActive) {            Object[] objects = invocation.getArgs();            MappedStatement ms = (MappedStatement) objects[0];            if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {                // 如果selectKey为自增id查询主键,使用主库                if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {                    lookupKey = DynamicDataSourceHolder.DB_MASTER;                } else {                    BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);                    String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");                    if (sql.matches(REGEX)) {                        lookupKey = DynamicDataSourceHolder.DB_MASTER;                    } else {                        // 这里如果有多个从数据库,则添加挑选过程                        lookupKey = DynamicDataSourceHolder.DB_SLAVE;                    }                }            }        } else {            lookupKey = DynamicDataSourceHolder.DB_MASTER;        }        DynamicDataSourceHolder.setDBType(lookupKey);        return invocation.proceed();    }    @Override    public Object plugin(Object target) {        // 增删改查的拦截,然后交由intercept处理        if (target instanceof Executor) {            return Plugin.wrap(target, this);        } else {            return target;        }    }    @Override    public void setProperties(Properties properties) {    }

再创建一个配置类,用来创建数据源,配置事务管理器等

@Configuration  @MapperScan(basePackages = "demo01.mapper")public class MyBatisPlusConfig {    /**     * 配置数据源     * @return     */    @Bean(name = "master")    @ConfigurationProperties(prefix = "spring.datasource.hikari.master")    public DataSource master() {        return DataSourceBuilder.create().build();    }    @Bean(name = "slave")    @ConfigurationProperties(prefix = "spring.datasource.hikari.slave")    public DataSource slave() {        return DataSourceBuilder.create().build();    }      @Primary    @Bean(name = "dynamicDataSource")    public DynamicDataSource dataSource(@Qualifier("master") DataSource master,                                        @Qualifier("slave") DataSource slave) {        Map<Object, Object> targetDataSource = new HashMap<>();        targetDataSource.put(DynamicDataSourceHolder.DB_MASTER, master);        targetDataSource.put(DynamicDataSourceHolder.DB_SLAVE, slave);        DynamicDataSource dataSource = new DynamicDataSource();        dataSource.setTargetDataSources(targetDataSource);        return dataSource;    }        /**     * 配置事务管理器     */    @Bean    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {        return new DataSourceTransactionManager(dataSource);    } }
开始测试

我们需要几个简单的测试用例

用例

期望

实际

调用查询接口

查询的是从库

调用一次新增数据接口,再调用一次查询接口

主库新增,查询的是从库,从库无新增的数据

一个接口里面,先新增再查询,不带事务注解

主库新增,查询的是从库,从库无新增的数据

一个接口里面,先新增再查询,带事务注解

主库新增,查询的是主库,有对应数据

标签: #mybatisplus读写分离