前言:
现时大家对“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读写分离