文末有demo链接
先上pom文件:
4.0.0
com.carlton
Read-Write-database
0.0.1-SNAPSHOT
jar
Read-Write-database
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
2.0.5.RELEASE
UTF-8
UTF-8
1.8
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
runtime
com.alibaba
druid
1.1.11
org.springframework.boot
spring-boot-starter-aop
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
true
org.springframework.boot
spring-boot-maven-plugin
配置文件:
server:
port: 8080
tomcat:
uri-encoding: UTF-8
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/trymaster?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123123
readSize: 1
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
driver-class-name: com.mysql.jdbc.Driver
filters: stat,wall
initialSize: 5
maxActive: 20
maxPoolPreparedStatementPerConnectionSize: 20
maxWait: 60000
minEvictableIdleTimeMillis: 300000
minIdle: 5
poolPreparedStatements: true
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
timeBetweenEvictionRunsMillis: 60000
useGlobalDataSourceStat: true
validationQuery: SELECT 1 FROM DUAL
slave:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/tryslave1?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123123
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
driver-class-name: com.mysql.jdbc.Driver
filters: stat,wall
initialSize: 5
maxActive: 20
maxPoolPreparedStatementPerConnectionSize: 20
maxWait: 60000
minEvictableIdleTimeMillis: 300000
minIdle: 5
poolPreparedStatements: true
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
timeBetweenEvictionRunsMillis: 60000
useGlobalDataSourceStat: true
validationQuery: SELECT 1 FROM DUAL
messages:
encoding: UTF-8
http:
encoding:
force: true
charset: UTF-8
enabled: true
mybatis:
mapper-locations: com/carlton/demo/mapper/*.xml
type-aliases-package: com.carlton.demo.entity
logging:
level:
com.carlton.demo.mapper: debug
druid:
init:
prefix: /druid/*
allow:
deny: 192.168.1.100
loginUsername: root
loginPassword: 123123
resetEnable: false
下面是切换数据源所需的几个工具类
首先用枚举类型定义数据库的主从(或者多个数据库):DataSourceType.class
package com.carlton.demo.conf.split;
/***
* 枚举区分读写库
*
* @author Carlton
*
*/
public enum DataSourceType {
read("read", "从库"), write("write", "主库");
private String type;
private String name;
DataSourceType(String type, String name) {
this.type = type;
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建一个用于切换数据源的全局变量 DataSourceContextHolder.class
package com.carlton.demo.conf.split;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
/***
* 本地线程全局变量
*
* @author Carlton
*
*/
@Slf4j
public class DataSourceContextHolder {
static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
private static final ThreadLocal local = new ThreadLocal();
public static ThreadLocal getLocal() {
return local;
}
/**
* 读可能是多个库
*/
public static void read() {
local.set(DataSourceType.read.getType());
}
/**
* 写只有一个库
*/
public static void write() {
log.debug("writewritewrite");
local.set(DataSourceType.write.getType());
}
public static String getJdbcType() {
return local.get();
}
}
然后是切入点和切面 DataSourceAop.class
package com.carlton.demo.conf.split;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/***
* 切换数据源
*
* @author Carlton
*
*/
@Aspect
@Component
@Slf4j
public class DataSourceAop {
static Logger log = LoggerFactory.getLogger(DataSourceAop.class);
@Before("execution(* com.carlton.demo.mapper.*.select*(..)) || execution(* com.carlton.demo.mapper.*.count*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.read();
log.info("dataSource切换到:Read");
}
@Before("execution(* com.carlton.demo.mapper.*.insert*(..)) || execution(* com.carlton.demo.mapper.*.update*(..)) || execution(* com.carlton.demo.mapper.*.delete*(..))")
public void setWriteDataSourceType() {
DataSourceContextHolder.write();
log.info("dataSource切换到:write");
}
}
如果配置了多个从库,即多个读库,则需要一个简单的负载均衡来确定读取哪个从库 MyAbstractRoutingDataSource.class
package com.carlton.demo.conf.split;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/***
* 多数据源切换,从库负载均衡
*
* @author Carlton
*
*/
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
private final int dataSourceNumber;
private AtomicInteger count = new AtomicInteger(0);
public MyAbstractRoutingDataSource(int dataSourceNumber) {
this.dataSourceNumber = dataSourceNumber;
}
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getJdbcType();
if (typeKey.equals(DataSourceType.write.getType()))
return DataSourceType.write.getType();
// 读 简单负载均衡
int number = count.getAndAdd(1);
int lookupKey = number % dataSourceNumber;
Integer i = lookupKey;
return i;
}
}
然后是 druid 数据库配置类:DataSourceConfiguration.class
package com.carlton.demo.conf.split;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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.context.annotation.Primary;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import lombok.extern.slf4j.Slf4j;
/**
* 数据库配置
*/
@Slf4j
@Configuration
public class DataSourceConfiguration {
protected Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class);
@Value("${spring.datasource.type}")
private Class extends DataSource> dataSourceType;
@Value("${druid.init.prefix}")
private String prefix;
@Value("${druid.init.allow}")
private String allow;
@Value("${druid.init.deny}")
private String deny;
@Value("${druid.init.loginUsername}")
private String loginUsername;
@Value("${druid.init.loginPassword}")
private String loginPassword;
@Value("${druid.init.resetEnable}")
private String resetEnable;
@Bean
public ServletRegistrationBean druidServlet() {
log.info("init Druid Servlet Configuration ");
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), prefix);
// IP白名单
servletRegistrationBean.addInitParameter("allow", allow);
// IP黑名单(共同存在时,deny优先于allow)
servletRegistrationBean.addInitParameter("deny", deny);
// 控制台管理用户
servletRegistrationBean.addInitParameter("loginUsername", loginUsername);
servletRegistrationBean.addInitParameter("loginPassword", loginPassword);
// 是否能够重置数据 禁用HTML页面上的“Reset All”功能
servletRegistrationBean.addInitParameter("resetEnable", resetEnable);
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico," + prefix);
return filterRegistrationBean;
}
@Bean(name = "writeDataSource", destroyMethod = "close", initMethod = "init")
@Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource writeDataSource() {
log.info("-------------------- writeDataSource init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* 有多少个从库就要配置多少个
*
* @return
*/
@Bean(name = "readDataSource1")
@ConfigurationProperties(prefix = "spring.slave")
public DataSource readDataSourceOne() {
log.info("-------------------- readDataSourceOne init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean("readDataSources")
public List readDataSources() {
List dataSources = new ArrayList<>();
dataSources.add(readDataSourceOne());
return dataSources;
}
}
最后是配置事务管理 DataSourceTransactionManager.class
package com.carlton.demo.conf.split;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import lombok.extern.slf4j.Slf4j;
/***
* 自定义事务
*
* @author Carlton
*
*/
@Configuration
@EnableTransactionManagement
@Slf4j
public class DataSourceTransactionManager extends DataSourceTransactionManagerAutoConfiguration {
static Logger log = LoggerFactory.getLogger(DataSourceTransactionManager.class);
@Resource(name = "writeDataSource")
private DataSource dataSource;
/**
* 自定义事务
* MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。
*
* @return
*/
@Bean(name = "transactionManager")
public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManagers() {
log.info("-------------------- transactionManager init ---------------------");
return new org.springframework.jdbc.datasource.DataSourceTransactionManager(dataSource);
}
}
到这里,读写分离主要步骤都已经完成,下面就是建表写测试代码:
在两个库中新建两张一样的表,user表,实体类如下:
package com.carlton.demo.entity;
public class User {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
-----mapper 类 和 xml ----省略,
再然后新建controller和service,写测试代码。
注意要使事务管理生效,需在相应service方法上加上@Transactional 注解
完整demo:
https://gitee.com/carltonq/read_write_separation_demo.git