Springboot+Mysql+AOP的读写分离
- 相关依赖
<!--引入springboot的web支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot+mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!-- Aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- set get方法 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!-- Junit测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringWeb Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--⽂件⽀持-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!--集成Redis二级缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- log4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.12</version>
</dependency>
- application.yaml
#服务基本配置
server:
port: 8888
servlet:
context-path: /
#解决get乱码
tomcat:
uri-encoding: UTF-8
#数据源
spring:
datasource:
master:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://10.15.0.10:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
username: root
password: root
slave1:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://10.15.0.11:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
username: root
password: root
slave2:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://10.15.0.12:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
username: root
password: root
#文件上传
servlet:
multipart:
enabled: true
max-file-size: 10MB
location: C:/Users/YT/Desktop/uploadTmpDir
#post表单提交乱码
http:
encoding:
charset: utf-8
#配置redis
redis:
host: hbase
port: 6379
timeout: 5s
lettuce:
shutdown-timeout: 100s
pool:
max-active: 10
max-idle: 8
max-wait: 5ms
min-idle: 1
#配置mapper
mybatis:
#开启Mybatis的二级缓存
configuration:
cache-enabled: true
- UserDefinDatasourceConfig
自定义一个数据源配置类 交由工厂管理
@Configuration
public class UserDefinDatasourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource() {
return DataSourceBuilder.create().build();
}
/**
* 当⾃定义数据源,⽤户必须覆盖SqlSessionFactory创建
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory
sqlSessionFactory(@Qualifier("proxyDataSource") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
//实体类所在的包
sqlSessionFactoryBean.setTypeAliasesPackage("com.baizhi.entity");
//Mapper类所放置的位置
sqlSessionFactoryBean.setMapperLocations(new
PathMatchingResourcePatternResolver().getResources("classpath*:com/baizhi/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
//中间件
public DataSource proxyDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource){
DataSourceProxy proxy = new DataSourceProxy();
//设置默认数据源
proxy.setDefaultTargetDataSource(masterDataSource);
Map<Object, Object> map = new HashMap<>();
map.put("master",masterDataSource);
map.put("slave-01",slave1DataSource);
map.put("slave-02",slave2DataSource);
//注册所有数据源
proxy.setTargetDataSources(map);
return proxy;
}
/**
* 当⾃定义数据源,⽤户必须覆盖SqlSessionTemplate,开启BATCH处理模式
* @param sqlSessionFactory
* @return
*/
@Bean
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory")
SqlSessionFactory sqlSessionFactory) {
//开启Mybatis的批处理模式
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
}
/***
* 当⾃定义数据源,⽤户必须注⼊,否则事务控制不⽣效
* @param dataSource
* @return
*/
@Bean
public PlatformTransactionManager platformTransactionManager(@Qualifier("proxyDataSource")
DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
- 定义一个中间类 DataSourceProxy
代理类 程序自动识别返回值是master 还是slave01和slave02 判断当前执行的业务是读操作还是写操作
public class DataSourceProxy extends AbstractRoutingDataSource {
private static final Logger log = LoggerFactory.getLogger(DataSourceProxy.class);
private String masterDBKey = "master";
private List<String> slaveDBKey = Arrays.asList("slave-01","slave-02");
//轮循
private static final AtomicInteger round = new AtomicInteger(0);
/***
* 需要在该方法中,判断当前用户是读操作还是写操作
* 如果是写操作@return master
* 读操作则@return slaveDBKey
*/
@Override
//determineCurrentLookupKey()返回查询TargeDataSources的数据源 操作数据库
protected Object determineCurrentLookupKey() {
System.out.println("DataSource代理类---------------------");
String dbKey = null;
OperType operType = OperTypeContextHolder.getOperType();
System.out.println(operType+"operType操作类型");
//如果当前操作类型是写操作
if(operType.equals(OperType.WRIRTE)){
dbKey=masterDBKey;
System.out.println(dbKey+"这里应该是写操作");
}else{
//轮循返回 0 1 2 3 4
int value = round.getAndIncrement();
if(value<0){
round.set(0);
}
Integer index=round.get()%slaveDBKey.size();
System.out.println(index+"------------------------------------------");
dbKey=slaveDBKey.get(index);
System.out.println(dbKey+"这里应该是读操作");
}
log.debug("当前DBKey:"+dbKey);
return dbKey;
}
}
此时,我们需要定义一个操作类型,方便代理类根据操作类型来判断读写操作等逻辑
- 定义一个枚举类型
这里我们将定义一个枚举类型
/**
* 枚举定义操作类型 写操作和读操作
* */
public enum OperType {
WRIRTE,READ;
}
到了这一步,我们需要通过线程 进行本地变量来传递操作类型
- 通过线程本地变量传递操作类型
同一个线程返回同一个值
/**
* 通过线程本地变量传递操作类型
* */
public class OperTypeContextHolder {
private static final ThreadLocal<OperType> OPER_TYPE_THREAD_LOCAL=new ThreadLocal<>();
//存操作类型
public static void setOperType(OperType operType){
OPER_TYPE_THREAD_LOCAL.set(operType);
System.out.println(operType+"操作类型上下文");
}
//取操作类型
public static OperType getOperType(){
return OPER_TYPE_THREAD_LOCAL.get();
}
//清除操作类型
public static void clear(){
OPER_TYPE_THREAD_LOCAL.remove();
}
}
- 定义一个注释类
用来区分出当前执行的业务是读操作还是写操作
/**
* 定义注释
* 标记当前业务方法是否是读操作
* */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
public @interface SlaveDB {
}
在userServiceImpl类中的读操作上添加此注解
@Service("UserService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDao;
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
@Override
@SlaveDB
public List<User> queryUserByPage(Integer pageNow, Integer pageSize, String column, Object value) {
List<User> users = userDao.queryUserByPage(pageNow, pageSize, column, value);
return users;
}
}
- AOP
//定义一个切面类
@Aspect
/**
* 默认切面运行在事务之后
* @Order(0)注解值越小优先级越高
* */
@Order(0)//在事务之前运行切面
@Component//将普通类实例化到Spring容器中
public class ServiceMethodAOP {
private static final Logger log = LoggerFactory.getLogger(ServiceMethodAOP.class);
@Around("execution(* com.baizhi.service..*.*(..))")
public Object methodInterceptor(ProceedingJoinPoint pjp){
System.out.println("--------------AOP------------------");
Object result = null;
try {
//获取当前的方法信息
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
//获取方法对象
Method method = methodSignature.getMethod();
log.debug("方法名:"+method.getName());
//判断方法是否存在注解@SlaveDB
boolean present = method.isAnnotationPresent(SlaveDB.class);
System.out.println(present+"注解是否为空");
OperType operType = null;
//如果是写方法
if(!present){
operType=OperType.WRIRTE;
}else{
//如果是读方法
operType=OperType.READ;
}
OperTypeContextHolder.setOperType(operType);
//当前操作类型
log.debug("当前操作"+operType);
//调用目标方法
result = pjp.proceed();
//清除线程变量
OperTypeContextHolder.clear();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
注意: 切面类必须要在事务之前执行,默认是在之后执行,加上@Order(0)注解方可解决此问题
执行流程
① Spring工厂启动时,会将加有@Configuration注解的类交给工厂.在这个类里面,工厂会根据你的yml配置文件,加载所有的数据源.并成一个key-value 集合类型,以DataSource保存在JVM中.
② 当接口AbstractRoutingDataSource(抽象的路由选择数据源)的实现类返回一个key时,系统会根据这个key找到相应的数据源.系统就会连接这个数据源.
③ 程序执行到业务层时,会走AOP层,在执行业务之前,会获取当前操作的读写属性并存储到我们定义的状态持有者类中.环绕通知再调用proceed之前,要拿到连接(即数据源).
④ 这时,我们定义的接口实现类发挥作用,通过注解来判断操作类型是读操作还是写操作,将状态持有者中获取的操作状态,返回对应的key,系统自动根据key返回对应的数据源,执行业务.