网上大多读写分离实例,都是需要手动侵入代码修改数据源,本博客主要实现,如何对现有系统,不侵入原有代码,进行读写分离配置。
Mybatis-Plus自带动态数据源配置,但是也需要对手动配置,需要的可以参考官网Mybstis-Plus官网
1、引入依赖
依赖较多,主要还有MybatisPlus单数据源配置的依赖,作者就全部贴上来了。
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
2、配置数据源
################DataSource###############
# Druid
#spring.datasource.master
spring.datasource.master.url=jdbc:mysql://127.0.0.1:7001/wj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.master.username=test
spring.datasource.master.password=123456
#spring.datasource.slave
spring.datasource.slave.url=jdbc:mysql://127.0.0.1:7002/wj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.slave.username=test
spring.datasource.slave.password=123456
#driverClass
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化连接数量
spring.datasource.initial-size=5
# 最小连接数量
spring.datasource.min-idle=5
# 最大同时连接数量
spring.datasource.max-active=20
# 配置获取连接等待超时的时间,单位毫秒ms
spring.datasource.max-wait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒ms
spring.datasource.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.min-evictable-idle-time-millis=300000
# SQL查询,用来验证从连接池取出的连接,在将连接返回给调用者之前.如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录
# from dual:dual是一个虚拟的表,供测试使用
spring.datasource.validation-query=SELECT 1 FROM DUAL
# 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
# 注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串
spring.datasource.test-while-idle=true
# 指明是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个.
# 注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串
spring.datasource.test-on-borrow=false
# 指明是否在归还到池中前进行检验
# 注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串
spring.datasource.test-on-return=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.pool-prepared-statements=true
spring.datasource.max-pool-prepared-statement-per-connection-size=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
#spring.datasource.druid.filter.commons-log.connection-logger-name=stat,wall,log4j
#spring.datasource.druid.filter.stat.log-slow-sql=true
#spring.datasource.druid.filter.stat.slow-sql-millis=2000
spring.datasource.filters=stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
# 合并多个DruidDataSource的监控数据
spring.datasource.use-global-data-source-stat=true
# 配置DruidStatFilter
spring.datasource.web-stat-filter.enabled=true
spring.datasource.web-stat-filter.url-pattern=/*
spring.datasource.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
# 配置DruidStatViewServlet
spring.datasource.stat-view-servlet.url-pattern=/druid/*
# IP白名单(没有配置或者为空,则允许所有访问)
spring.datasource.stat-view-servlet.allow=127.0.0.1
# IP黑名单 (存在共同时,deny优先于allow)
spring.datasource.stat-view-servlet.deny=
# 禁用HTML页面上的“Reset All”功能
spring.datasource.stat-view-servlet.reset-enable=false
# 登录名
spring.datasource.stat-view-servlet.login-username=admin
# 密码
spring.datasource.stat-view-servlet.login-password=123456
3、读写分离配置
DynamicDataSource.java
package com.imooc.wjtest.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @program: wjtest
* @description:
* @author: ZhangKai
* @create: 2019-11-11 10:32
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDbType();
}
}
DynamicDataSourceHolder.java
package com.imooc.wjtest.config;
import lombok.extern.slf4j.Slf4j;
/**
* @program: wjtest
* @description:
* @author: ZhangKai
* @create: 2019-11-11 10:41
**/
@Slf4j
public class DynamicDataSourceHolder {
private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
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;
}
/**
* 设置线程的dbType
*
* @param str
*/
public static void setDbType(String str) {
log.debug("所使用的数据源为:" + str);
contextHolder.set(str);
}
/**
* 清理连接类型
*/
public static void clearDBType() {
contextHolder.remove();
}
}
DynamicDataSourceInterceptor.java
package com.imooc.wjtest.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Locale;
import java.util.Properties;
/**
* @program: wjtest
* @description:
* @author: ZhangKai
* @create: 2019-11-11 15:09
**/
@Slf4j
@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();
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
String lookupKey = DynamicDataSourceHolder.DB_MASTER;
if (synchronizationActive != true) {
// 读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
// selectKey 为自增id查询主键(SELECT LAST_INSERT_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;
}
log.debug("设置方法[{}] use [{}] Strategy, SqlCommanType [{}]..", ms.getId(), lookupKey,
ms.getSqlCommandType().name());
DynamicDataSourceHolder.setDbType(lookupKey);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
MyBatisPlusConfig.java
package com.imooc.wjtest.config;
import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @program: wjtest
* @description:
* @author: ZhangKai
* @create: 2019-11-11 15:30
**/
@Slf4j
@Configuration
@MapperScan(basePackages = "com.imooc.wjtest.mapper")
public class MyBatisPlusConfig {
@Value("${spring.datasource.master.url}")
private String masterUrl;
@Value("${spring.datasource.master.username}")
private String masterUserName;
@Value("${spring.datasource.master.password}")
private String masterPassword;
@Value("${spring.datasource.slave.url}")
private String slaveUrl;
@Value("${spring.datasource.slave.username}")
private String slaveUserName;
@Value("${spring.datasource.slave.password}")
private String slavePassword;
@Value("${spring.datasource.driverClassName}")
private String driver;
/**
* time for connection wait
*/
@Value("${spring.datasource.max-active}")
private int maxActive;
@Value("${spring.datasource.max-wait}")
private int maxWait;
@Value("${spring.datasource.initial-size}")
private int initialSize;
@Value("${spring.datasource.max-pool-prepared-statement-per-connection-size}")
private int maxOpenPreparedStatementConnectionSize;
@Value("${spring.datasource.min-evictable-idle-time-millis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.min-idle}")
private int minIdle;
@Value("${spring.datasource.validation-query}")
private String validationQuery;
@Value("${spring.datasource.test-while-idle}")
private boolean testWhileIdle;
@Value("${spring.datasource.test-on-borrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.test-on-return}")
private boolean testOnReturn;
@Value("${spring.datasource.pool-prepared-statements}")
private boolean poolPreparedStatements;
@Value("${spring.datasource.connectionProperties}")
private String connectionProperties;
@Value("${spring.datasource.filters}")
private String filters;
@Bean(name = "master")
public DataSource master() throws SQLException{
return getDruidDataSource(masterUrl, masterUserName, masterPassword);
}
@Bean(name = "slave")
public DataSource slave() throws SQLException{
return getDruidDataSource(slaveUrl, slaveUserName, slavePassword);
}
private DruidDataSource getDruidDataSource(String url,String userName,String password) throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driver);
druidDataSource.setUrl(url);
druidDataSource.setUsername(userName);
druidDataSource.setPassword(password);
druidDataSource.setMaxActive(maxActive);
druidDataSource.setInitialSize(initialSize);
druidDataSource.setMinIdle(minIdle);
druidDataSource.setMaxWait(maxWait);
druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
druidDataSource.setValidationQuery(validationQuery);
druidDataSource.setTestWhileIdle(testWhileIdle);
druidDataSource.setTestOnBorrow(testOnBorrow);
druidDataSource.setTestOnReturn(testOnReturn);
druidDataSource.setPoolPreparedStatements(poolPreparedStatements);
druidDataSource.setMaxOpenPreparedStatements(maxOpenPreparedStatementConnectionSize);
druidDataSource.setConnectionProperties(connectionProperties);
druidDataSource.setFilters(filters);
List<Filter> filters = new ArrayList<>();
filters.add(createWallFilter());
druidDataSource.setProxyFilters(filters);
return druidDataSource;
}
@Bean(name = "dynamicDataSource")
public DynamicDataSource dynamicDataSource(@Qualifier("master") DataSource master,
@Qualifier("slave") DataSource slave){
Map<Object,Object> targetDataSources = new HashMap<Object, Object>();
targetDataSources.put(DynamicDataSourceHolder.DB_MASTER, master);
targetDataSources.put(DynamicDataSourceHolder.DB_SLAVE, slave);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
return dataSource;
}
/**
* MybatisPlus扩展分页插件
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
@Bean
public DynamicDataSourceInterceptor dynamicDataSourceInterceptor(){
return new DynamicDataSourceInterceptor();
}
/**
* 根据数据源创建SqlSessionFactory
* @return
* @throws Exception
*/
@Bean(name = "SqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory() throws Exception {
//配置mybatis,对应mybatis-config.xml
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
//懒加载
LazyConnectionDataSourceProxy p=new LazyConnectionDataSourceProxy();
p.setTargetDataSource(dynamicDataSource(master(), slave()));
sqlSessionFactory.setDataSource(p);
//需要mapper文件时加入扫描
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
sqlSessionFactory.setTypeAliasesPackage("com.imooc.wjtest.entity");
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setUseGeneratedKeys(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
//加入上面的两个拦截器
Interceptor interceptor[]={paginationInterceptor(),dynamicDataSourceInterceptor()};
sqlSessionFactory.setPlugins(interceptor);
return sqlSessionFactory.getObject();
}
@Bean
public ServletRegistrationBean druidServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
servletRegistrationBean.setServlet(new StatViewServlet());
servletRegistrationBean.addUrlMappings("/druid/*");
Map<String, String> initParameters = new HashMap<String, String>();
// 用户名
initParameters.put("loginUsername", "admin");
// 密码
initParameters.put("loginPassword", "123456");
// 禁用HTML页面上的“Reset All”功能
initParameters.put("resetEnable", "false");
// IP白名单 (没有配置或者为空,则允许所有访问)
initParameters.put("allow", "");
// IP黑名单 (存在共同时,deny优先于allow)
// initParameters.put("deny", "192.168.20.38");
servletRegistrationBean.setInitParameters(initParameters);
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
/**
* 配置事务管理器
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}
/**
* 自定义过滤器
* @return
*/
public WallFilter createWallFilter() {
WallConfig wallConfig = new WallConfig();
// 允许一次执行多条语句
wallConfig.setMultiStatementAllow(true);
wallConfig.setNoneBaseStatementAllow(true);
WallFilter wallFilter = new WallFilter();
wallFilter.setConfig(wallConfig);
return wallFilter;
}
}
4、测试
package com.imooc.wjtest;
import com.imooc.wjtest.entity.User;
import com.imooc.wjtest.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
class WjtestApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void test() {
try {
User user = new User("test", "123");
Integer num = this.userMapper.insert(user);
User user1 = this.userMapper.selectById(1);
log.info(user1.getUsername());
log.info(String.valueOf(num));
}catch (Exception e){
e.printStackTrace();
}
}
}
结果:
5、github
作者偷懒了,只是对配置单纯地贴了代码,如果想进一步了解,或者有不明白的,可以直接访问作者的github下载。该读写分离作者放在一个demo中,下载项目查看即可。码云