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

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

为什么有这个需求

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

怎么解决

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

​ 目前有2种方案,

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

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

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

开始试验

创建demo

建2个库 demo_master,demo_slave

建一个表 user

插入一些数据

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);
    }
 
}

开始测试

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

用例 期望 实际
调用查询接口 查询的是从库
调用一次新增数据接口,再调用一次查询接口 主库新增,查询的是从库,从库无新增的数据
一个接口里面,先新增再查询,不带事务注解 主库新增,查询的是从库,从库无新增的数据
一个接口里面,先新增再查询,带事务注解 主库新增,查询的是主库,有对应数据
上一篇:day28_常用模块——hashlib


下一篇:MyBatisPlus实现逻辑删除