"Project documentary" -- mybatisplus multi data source read-write separation

Why is there such a need

Recently, we have an online project. Due to the large amount of read-write data, the pressure on the database increases, so now we plan to adopt the scheme of read-write separation.

How to solve it

The framework we use is springboot+mybatisplus, and it is an online project. It can not be transformed in large quantities, and the cost is too high, which is the prerequisite.

At present, there are two schemes,

​ 1. The method of directly introducing multiple data sources of mybatisplus uses the annotation @ DS("xxx") at the method or class level. This scheme is more flexible, but many of our online codes use the single table addition, deletion, modification and query interfaces encapsulated by mybatisplus. These interfaces cannot use annotations. If they are modified alone, it will lead to a large workload of changes.

​ 2. Customize the data source, obtain the sql type through the interceptor of mybatis, follow the database data source for query statements, add, delete and change to the main database data source.

After comparing our needs, we finally decided to do it in the second way.

Start test

Create demo

Create 2 library demos_ master,demo_ slave

Create a table user

Insert some data

CREATE TABLE user
(
	id BIGINT(20) NOT NULL COMMENT 'Primary key ID',
	name VARCHAR(30) NULL DEFAULT NULL COMMENT 'full name',
	age INT(11) NULL DEFAULT NULL COMMENT 'Age',
	email VARCHAR(50) NULL DEFAULT NULL COMMENT 'mailbox',
	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');

Initialize a maven project and introduce related dependencies

<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>

Configure data source

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

Generate entity, mapper,service, etc

Next, create a tool class DynamicDataSourceHolder for switching data sources

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("Currently set data source as" + str);
        contextHolder.set(str);
    }
 
    public static void clearDbType() {
        contextHolder.remove();
    }
 
}

Implement a dynamic data source that inherits AbstractRoutingDataSource. This class runs and we select the current data source according to the defined rules

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

Add an interceptor to intercept the execution of sql

@Component
@Slf4j
//Specify which methods to intercept. update includes addition, deletion and modification
@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("Whether the current execution statement has a transaction:{}",synchronizationActive);
		String lookupKey = DynamicDataSourceHolder.DB_MASTER;
		if (!synchronizationActive) {
			Object[] objects = invocation.getArgs();
			MappedStatement ms = (MappedStatement) objects[0];
			if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
				// If the selectKey is a self incrementing id query primary key, the primary database is used
				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 {
						// Here, if there are multiple slave databases, add the selection process
						lookupKey = DynamicDataSourceHolder.DB_SLAVE;
					}
				}
			}
		} else {
			lookupKey = DynamicDataSourceHolder.DB_MASTER;
		}
		DynamicDataSourceHolder.setDBType(lookupKey);
		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		// The interception of additions, deletions and modifications is then handled by intercept
		if (target instanceof Executor) {
			return Plugin.wrap(target, this);
		} else {
			return target;
		}
	}

	@Override
	public void setProperties(Properties properties) {

	}

Then create a configuration class to create data sources and configure transaction managers

@Configuration  
@MapperScan(basePackages = "demo01.mapper")
public class MyBatisPlusConfig {
    /**
     * Configure data source
     * @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;
    }
 
  
 
    /**
     * Configure transaction manager
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }
 
}

Start testing

We need a few simple test cases

Use caseexpectactual
Call query interfaceThe query is from the library
Call the new data interface once, and then call the query interface againWhen the master database is added, the slave database is queried, and there is no new data in the slave database
In an interface, add first and then query, without transaction annotationWhen the master database is added, the slave database is queried, and there is no new data in the slave database
In an interface, add first and then query, with transaction annotationsWhen adding a primary database, you can query the primary database with corresponding data

Keywords: Java MySQL Mybatis Spring Boot

Added by MrXander on Sat, 29 Jan 2022 23:20:12 +0200