文章目录
- 前言
- 正文
- 一、项目总览
- 二、核心代码展示
- 2.1 自定义AbstractRoutingDataSource
- 2.2 动态数据源DynamicDataSource
- 2.3 动态数据源自动配置
- 2.4 动态数据源上下文DynamicDataSourceContextHolder
- 2.5 动态数据源修改注解定义
- 2.6 修改切面DynamicDataSourceAspect
- 2.7 动态数据源工具类
- 三、start模块&调试
- 3.1 Mybatis-Plus配置
- 3.2 application.yml配置
- 3.3 启动类
- 3.4 调试
- 3.5 数据库sql
- 四、调试结果
- 4.1 启动项目
- 4.2 执行请求
前言
本文旨在SpringBoot3整合Mybatis-Plus,实现动态数据源切换。
不使用Mybatis-Plus本身的依赖。自己动手造轮子。
之前有写过一个SpringBoot切换动态数据源的文章:
- https://blog.csdn.net/FBB360JAVA/article/details/124610140
本次使用了Java17,SpringBoot3.0.2 ,Mybatis-Spring 3版本。并且自定义starter,提供自定义注解,使用切面实现切换数据源。
本文中对应的代码仓库如下:
- https://gitee.com/fengsoshuai/dynamic-datasource-mp-starter-demo
其中,代码分支master,是多数据源,提供静态切换方法,注解方式切换。
代码分支dev,是动态多数据源,在master的基础上,提供运行时,新增或修改,或删除数据源。
以上,只考虑单机模式(如果是分布式项目,建议使用中间件,如redis等维护数据源信息;或者创建独立项目专门提供数据源必要信息的接口)
正文
一、项目总览
本次使用聚合maven项目,内部包含两个模块:
- dynamic-datasource-mp-starter:自定义starter,实现数据源的基本功能,包含切换数据源等。
- start:启动&测试模块,整合mybatis-plus ,提供代码配置,以及提供测试接口
二、核心代码展示
注意,本节代码展示,只粘贴了dev分支的代码!
2.1 自定义AbstractRoutingDataSource
如果不需要动态新增或修改数据源,则不需要自定义这个类。直接使用spring-jdbc中的该类即可。
package org.feng.datasource.util;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
public AbstractRoutingDataSource() {
}
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
public void setTargetDataSourcesAndRefresh(String newMerchant, DataSource newDataSource) {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.targetDataSources.put(newMerchant, newDataSource);
if (this.resolvedDataSources == null) {
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
}
Object value = this.targetDataSources.get(newMerchant);
Object lookupKey = this.resolveSpecifiedLookupKey(newMerchant);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
}
public void removeDataSourcesByMerchant(String newMerchant) {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.targetDataSources.remove(newMerchant);
this.resolvedDataSources.remove(newMerchant);
}
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
}
public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup();
}
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
}
protected DataSource resolveSpecifiedDataSource(Object dataSourceObject) throws IllegalArgumentException {
if (dataSourceObject instanceof DataSource dataSource) {
return dataSource;
} else if (dataSourceObject instanceof String dataSourceName) {
return this.dataSourceLookup.getDataSource(dataSourceName);
} else {
throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSourceObject);
}
}
public Map<Object, DataSource> getResolvedDataSources() {
Assert.state(this.resolvedDataSources != null, "DataSources not resolved yet - call afterPropertiesSet");
return Collections.unmodifiableMap(this.resolvedDataSources);
}
@Nullable
public DataSource getResolvedDefaultDataSource() {
return this.resolvedDefaultDataSource;
}
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
}
public <T> T unwrap(Class<T> iface) throws SQLException {
return iface.isInstance(this) ? (T) this : this.determineTargetDataSource().unwrap(iface);
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
}
2.2 动态数据源DynamicDataSource
package org.feng.datasource;
import org.feng.datasource.util.AbstractRoutingDataSource;
/**
* 动态数据源
*
* @author feng
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
2.3 动态数据源自动配置
package org.feng.datasource.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.DynamicDataSource;
import org.feng.datasource.constant.DataSourceConstant;
import org.feng.datasource.entity.DataSourceProperties;
import org.feng.datasource.entity.SpringDataSourceProperties;
import org.feng.datasource.util.DataSourceUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 动态数据源自动配置
*
* @author feng
*/
@Data
@Slf4j
@AutoConfiguration
@ConfigurationPropertiesScan({"org.feng.datasource.entity"})
public class DynamicDataSourceAutoConfiguration {
@Resource
private SpringDataSourceProperties springDataSourceProperties;
@Primary
@Bean
public DataSource dynamicDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>(16);
Map<String, DataSourceProperties> dataSourcePropertiesMap = springDataSourceProperties.getConfigMap();
dataSourcePropertiesMap.forEach((merchant, properties) -> dataSourceMap.put(merchant, DataSourceUtil.dataSource(properties)));
// 设置动态数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(dataSourceMap);
// 设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(dataSourceMap.get(DataSourceConstant.MASTER));
return dynamicDataSource;
}
@PostConstruct
private void init() {
Map<String, DataSourceProperties> configMap = springDataSourceProperties.getConfigMap();
configMap.forEach((k, v) -> {
log.info("merchantKey = {}, config = {}", k, v);
});
}
}
2.4 动态数据源上下文DynamicDataSourceContextHolder
package org.feng.datasource;
import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.constant.DataSourceConstant;
import java.util.Optional;
/**
* 动态数据源上下文保持类
*
* @version v1.0
* @author: fengjinsong
* @date: 2022年05月05日 15时19分
*/
@Slf4j
public class DynamicDataSourceContextHolder {
/**
* 动态数据源的上下文
*/
private static final ThreadLocal<String> DATASOURCE_CONTEXT_MERCHANT_HOLDER = new InheritableThreadLocal<>();
/**
* 切换数据源
*
* @param merchant 租户Key
*/
public static void setDataSourceKey(String merchant) {
log.info("切换数据源,merchant 为 {}", merchant);
DATASOURCE_CONTEXT_MERCHANT_HOLDER.set(merchant);
}
/**
* 获取当前数据源名称
*
* @return 当前数据源名称
*/
public static String getDataSourceKey() {
return Optional.ofNullable(DATASOURCE_CONTEXT_MERCHANT_HOLDER.get())
.orElse(DataSourceConstant.MASTER);
}
/**
* 删除当前数据源名称
*/
public static void removeDataSourceKey() {
DATASOURCE_CONTEXT_MERCHANT_HOLDER.remove();
}
}
2.5 动态数据源修改注解定义
package org.feng.datasource.annotation;
import org.feng.datasource.constant.DataSourceConstant;
import java.lang.annotation.*;
/**
* 改变数据源注解
*
* @author feng
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ChangeDataSource {
String merchant() default DataSourceConstant.MASTER;
}
2.6 修改切面DynamicDataSourceAspect
package org.feng.datasource.aop;
import jakarta.annotation.PostConstruct;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.feng.datasource.DynamicDataSourceContextHolder;
import org.feng.datasource.annotation.ChangeDataSource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 动态数据源切面
*
* @author feng
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class DynamicDataSourceAspect {
@SneakyThrows
@Before("@annotation(org.feng.datasource.annotation.ChangeDataSource)")
public void changeDataSource(JoinPoint joinPoint) {
log.info("开始切换数据源...");
// 获取方法名,参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
Class<?>[] paramsTypes = new Class[args.length];
if(args.length > 0) {
for (int i = 0; i < args.length; i++) {
paramsTypes[i] = args[i].getClass();
}
}
// 获取注解
Class<?> aClass = joinPoint.getTarget().getClass();
Method currentMethod = aClass.getDeclaredMethod(methodName, paramsTypes);
ChangeDataSource changeDataSource = currentMethod.getDeclaredAnnotation(ChangeDataSource.class);
// 获取租户
String merchant = changeDataSource.merchant();
log.info("当前数据源租户切换为:{}", merchant);
// 切换数据源
DynamicDataSourceContextHolder.setDataSourceKey(merchant);
}
@After("@annotation(org.feng.datasource.annotation.ChangeDataSource)")
public void changeDataSourceOver() {
log.info("释放数据源...");
DynamicDataSourceContextHolder.removeDataSourceKey();
}
@PostConstruct
private void init() {
log.info("注册动态数据源切换注解");
}
}
2.7 动态数据源工具类
提供动态新增数据源,删除数据源的方法。
因为本身是维护map,所以同时支持修改(根据merchant来新增或修改或删除)
package org.feng.datasource.util;
import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.DynamicDataSource;
import org.feng.datasource.entity.DataSourceProperties;
import org.springframework.beans.BeansException;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
/**
* 数据源工具
*
* @author feng
*/
@Slf4j
@Component
public class DataSourceUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
/**
* 设置数据源,用于动态新增,修改数据源
*
* @param newMerchant 租户
* @param dataSourceProperties 数据源属性对象
*/
public static void setNewDynamicDataSource(String newMerchant, DataSourceProperties dataSourceProperties) {
log.info("merchantKey = {}, config = {}", newMerchant, dataSourceProperties);
DynamicDataSource dataSource = applicationContext.getBean(DynamicDataSource.class);
dataSource.setTargetDataSourcesAndRefresh(newMerchant, dataSource(dataSourceProperties));
}
/**
* 移除数据源
*
* @param newMerchant 租户
*/
public static void removeDynamicDataSource(String newMerchant) {
log.info("正在移除数据源 merchantKey = {}", newMerchant);
DynamicDataSource dataSource = applicationContext.getBean(DynamicDataSource.class);
dataSource.removeDataSourcesByMerchant(newMerchant);
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
DataSourceUtil.applicationContext = applicationContext;
}
/**
* 构建数据源
*
* @param dataSourceProperties 数据源属性配置
* @return 数据源
*/
public static DataSource dataSource(DataSourceProperties dataSourceProperties) {
return DataSourceBuilder.create()
.driverClassName(dataSourceProperties.getDriverClassName())
.url(dataSourceProperties.getJdbcUrl())
.username(dataSourceProperties.getUsername())
.password(dataSourceProperties.getPassword())
.build();
}
}
三、start模块&调试
3.1 Mybatis-Plus配置
package org.feng.start.config;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import lombok.SneakyThrows;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
/**
* mybatis-plus配置
*
* @author feng
*/
@Configuration
public class MybatisPlusConfig {
@Bean
@Primary
@SneakyThrows
public SqlSessionFactory sqlSessionFactory(@Autowired DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
MybatisConfiguration configuration = new MybatisConfiguration();
sqlSessionFactoryBean.setConfiguration(configuration);
// 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
configuration.setMapUnderscoreToCamelCase(true);
// 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
configuration.setCallSettersOnNulls(true);
// 日志
configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class);
// 实体包
sqlSessionFactoryBean.setTypeAliasesPackage("org.feng.start.entity");
// mapper.xml位置
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourceResolver.getResources("classpath*:mapper/**Mapper.xml");
sqlSessionFactoryBean.setMapperLocations(resources);
// 设置数据源
sqlSessionFactoryBean.setDataSource(dataSource);
// 设置GlobalConfig
GlobalConfigUtils.setGlobalConfig(configuration, globalConfig());
// 返回SqlSessionFactory
return sqlSessionFactoryBean.getObject();
}
private GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setIdType(IdType.AUTO);
globalConfig.setDbConfig(dbConfig);
return globalConfig;
}
}
3.2 application.yml配置
spring:
datasource:
config-map:
master:
driver-class-name: "com.mysql.cj.jdbc.Driver"
jdbc-url: "jdbc:mysql://192.168.110.68:3306/db_master?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
username: "root"
password: "root123456"
slave1:
driver-class-name: "com.mysql.cj.jdbc.Driver"
jdbc-url: "jdbc:mysql://192.168.110.68:3306/db_slave1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
username: "root"
password: "root123456"
# slave2:
# driver-class-name: "com.mysql.cj.jdbc.Driver"
# jdbc-url: "jdbc:mysql://192.168.110.68:3306/db_slave2?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
# username: "root"
# password: "root123456"
server:
port: 80
servlet:
context-path: /dynamic_datasource
3.3 启动类
package org.feng.start;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@MapperScan(basePackages = {"org.feng.start.mapper"})
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableTransactionManagement
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class, scanBasePackages = {"org.feng.datasource", "org.feng.start"})
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class, args);
}
}
3.4 调试
定义控制器,提供接口。
package org.feng.start.controller;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.DynamicDataSourceContextHolder;
import org.feng.datasource.annotation.ChangeDataSource;
import org.feng.datasource.entity.DataSourceProperties;
import org.feng.datasource.util.DataSourceUtil;
import org.feng.start.entity.Student;
import org.feng.start.service.IStudentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 学生控制器-测试切换数据源
*
* @author feng
*/
@Slf4j
@RequestMapping("/student")
@RestController
public class StudentController {
@Resource
private IStudentService studentService;
@GetMapping("/list/{merchant}")
public List<Student> list(@PathVariable String merchant) {
try {
// 切换数据源
DynamicDataSourceContextHolder.setDataSourceKey(merchant);
// 查库
List<Student> resultList = studentService.list();
log.info("查询结果:{}", resultList);
return resultList;
} finally {
// 清除当前数据源
DynamicDataSourceContextHolder.removeDataSourceKey();
}
}
@GetMapping("/listStu/master")
public List<Student> listStu() {
// 查库
List<Student> resultList = studentService.list();
log.info("查询结果:{}", resultList);
return resultList;
}
@ChangeDataSource(merchant = "slave1")
@GetMapping("/listStu/slave1")
public List<Student> listStu1() {
// 查库
List<Student> resultList = studentService.list();
log.info("查询结果:{}", resultList);
return resultList;
}
@ChangeDataSource(merchant = "slave2")
@GetMapping("/listStu/slave2")
public List<Student> listStu2() {
// 查库
List<Student> resultList = studentService.list();
log.info("查询结果:{}", resultList);
return resultList;
}
@GetMapping("/listStu/newDataSource")
public List<Student> newDataSource() {
String merchant = "slave2";
DataSourceProperties dataSourceProperties = newSlave2();
DataSourceUtil.setNewDynamicDataSource(merchant, dataSourceProperties);
// 切换数据源
DynamicDataSourceContextHolder.setDataSourceKey(merchant);
// 查库
List<Student> resultList = studentService.list();
log.info("查询结果:{}", resultList);
DynamicDataSourceContextHolder.removeDataSourceKey();
return resultList;
}
private DataSourceProperties newSlave2() {
DataSourceProperties dataSourceProperties = new DataSourceProperties();
dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSourceProperties.setJdbcUrl("jdbc:mysql://192.168.110.68:3306/db_slave2?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8");
dataSourceProperties.setUsername("root");
dataSourceProperties.setPassword("root123456");
return dataSourceProperties;
}
}
3.5 数据库sql
按照自己的需要,创建多个数据库,并创建好自己使用的数据表即可。
本文中测试使用的如下:
CREATE TABLE `tb_student` (
`student_name` varchar(100) DEFAULT NULL,
`id` bigint unsigned NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
四、调试结果
4.1 启动项目
启动项目,可以看到注册了两个数据源。
4.2 执行请求
- Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/master
响应:
[
{
"id": 1,
"studentName": "master_student2312s"
}
]
- Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/slave1
响应:
[
{
"id": 1,
"studentName": "slave1_studew12321"
}
]
- Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/slave2
因为此时还没有slave2,所以默认请求的是master的数据源,查询结果如下:
[
{
"id": 1,
"studentName": "master_student2312s"
}
]
- Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/newDataSource
新增数据源,代码中写的是新增slave2的数据源
日志如下:
查询结果如下:
[
{
"id": 1,
"studentName": "slave2_213dqwa"
}
]
- Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/slave2
此时已经新增了slave2数据源,因此能够切换到slave2中,查询结果如下:
[
{
"id": 1,
"studentName": "slave2_213dqwa"
}
]