SpringBoot+Mybatis实现读写分离
最近的项目中需要实现简单的读写分离,由于项目中使用的是SpringBoot+Mybatis,简单的实现了一下功能
读写分离的概念已经提过很多次:单个数据库的承受服务请求的压力太大,于是将数据库的读操作和写操作分别执行,由于我们的应用大部分是读操作,写操作频率较低,于是常见的结构是一主多从;(当然生产环境是有主备切换的,这里不赘述)
核心思想
基于Spring的AOP,在controller层进行切面设置,根据请求,动态设置数据源;
代码实现
1.配置文件application.properties
## 主库配置
jdbc.master.url=jdbc:mysql://localhost:3306/sakila?serverTimezone=CTT
jdbc.master.driver=com.mysql.cj.jdbc.Driver
jdbc.master.username=root
jdbc.master.password=123456
## 从库配置 可配置多个
jdbc.slave0.driver=com.mysql.cj.jdbc.Driver
jdbc.slave0.url=jdbc:mysql://localhost:3306/sakila_slave?serverTimezone=CTT
jdbc.slave0.username=root
jdbc.slave0.password=123456
- 数据源配置类DataSourceConfig
package com.xxx.demo.master_slave.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.xxx.demo.master_slave.ds.DynamicDataSource;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* 描述:
*
* @author
* @version 1.0
*/
@Configuration
public class DataSourceConfig {
/*
*Spring的spel表达式,获取spring的配置变量
*/
@Value("${jdbc.master.driver}")
private String masterDriverName;
@Value("${jdbc.master.url}")
private String masterJdbcUrl;
@Value("${jdbc.master.username}")
private String masterUserName;
@Value("${jdbc.master.password}")
private String masterPassWord;
@Value("${jdbc.slave0.driver}")
private String slave0DriverName;
@Value("${jdbc.slave0.url}")
private String slave0JdbcUrl;
@Value("${jdbc.slave0.username}")
private String slave0UserName;
@Value("${jdbc.slave0.password}")
private String slave0PassWord;
@Bean("masterDataSource")
public DataSource getMasterDataSource(){
return getDataSource(masterDriverName,masterJdbcUrl,masterUserName,masterPassWord);
}
@Bean("slave0DataSource")
public DataSource getSlave0DataSource(){
return getDataSource(slave0DriverName,slave0JdbcUrl,slave0UserName,slave0PassWord);
}
@Bean("dynamicDataSource")
public DataSource getDyamicDataSource(){
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master",getMasterDataSource());
targetDataSources.put("slave0",getSlave0DataSource());
DynamicDataSource dataSource = new DynamicDataSource(targetDataSources);
dataSource.setDefaultTargetDataSource(getMasterDataSource());
return dataSource;
}
@Bean
public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
// 配置MapperConfig
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
// 这个配置使全局的映射器启用或禁用缓存
configuration.setCacheEnabled(true);
//配置默认的执行器:
configuration.setDefaultExecutorType(ExecutorType.REUSE);
// 全局启用或禁用延迟加载,禁用时所有关联对象都会即时加载
configuration.setLazyLoadingEnabled(false);
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
private DataSource getDataSource(String driver,String jdbcUrl,String userName,String password){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(jdbcUrl);
dataSource.setUsername(userName);
dataSource.setPassword(password);
return dataSource;
}
}
数据源配置类中配置的数据源是Alibaba的DruidDataSource,生成masterDataSource和slave0DataSource两个数据源,并且将其注入我自己写的DynamicDataSource类中
注意:getSqlSessionFactory()方法必须配置。SpringBoot提供的自动配置,会帮助我们配置sqlSession或者SqlSessionFactory等类,但是前提是使用Spring默认提供的数据源,此时数据源已经改变,所以需要我们手动配置。
3.动态数据源类DynamicDataSource
package com.xxx.demo.master_slave.ds;
import com.xxx.demo.master_slave.context.DataSourceContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.Assert;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* <p>
* 描述:
* 版本1.0: 2019/10/17 14:44 新建
* </p>
*
* @author
* @version 1.0
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
//做从库负载均衡用
private AtomicInteger integer = new AtomicInteger(0);
private DataSource writeDataSource;
private List<DataSource> readDataSources = new ArrayList<>();
private int readDataSourceNum;
public DynamicDataSource(Map<Object, Object> targetDataSources){
Assert.notEmpty(targetDataSources,"至少配置一个数据源");
if(targetDataSources.size() == 1){
writeDataSource = (DataSource) targetDataSources.values().iterator().next();
return;
}
for (Map.Entry<Object, Object> entry : targetDataSources.entrySet()) {
if (entry.getKey().toString().toLowerCase().contains("master")) {
if (writeDataSource != null) {
throw new IllegalStateException("写数据源只能定义一个");
}
this.writeDataSource = (DataSource)entry.getValue();
continue;
}
//若数据源名称不含master,则默认为读数据源
readDataSources.add((DataSource)entry.getValue());
}
if (writeDataSource == null) {
throw new IllegalStateException("至少需要定义一个写数据源");
}
readDataSourceNum = readDataSources.size();
//多数据源配置
setTargetDataSources(targetDataSources);
}
/**
* 获取数据源的key,即配置类中的master/slave0
*/
@Override
protected Object determineCurrentLookupKey() {
//数据源类型上下文中获取数据源类型
String type = DataSourceContextHolder.getDataSourceType();
if("slave".equals(type) && readDataSourceNum > 0){
if(integer.get() > 100000){
integer.set(0);
}
logger.info("计数器的值:{}",integer.get());
int index = integer.getAndIncrement() % readDataSourceNum;
type = type + index;
}
logger.info("数据源类型:{}",type);
return type;
}
}
AbstractRoutingDataSource类是Spring提供的多数据源配置的基础类,允许我们继承并且实现自己的查找数据源的逻辑;determineCurrentLookupKey()方法是查找我们配置的数据源的key,即master/slave0等等;
4.数据源类型上下文类
package com.xxx.demo.master_slave.context;
/**
* <p>
* 描述:
* </p>
* <p>
* 版权所有: 版权所有(C)2001-2019
* </p>
* <p>
* </p>
* <p>
* 版本1.0: 2019/10/17 15:16 新建
* </p>
*
* @author
* @version 1.0
*/
public class DataSourceContextHolder {
/**
* 配置使用本地线程变量ThreadLocal,为每一个线程数据源类型保存自己单独的副本
*/
private static final ThreadLocal<String> dataSource = new ThreadLocal<>();
/**
* 设置当前线程的数据源类型
* @param dataSourceType
*/
public static void setDataSourceType(String dataSourceType){
dataSource.set(dataSourceType);
}
/**
* 获取当前线程的数据源类型
* @return
*/
public static String getDataSourceType() {
return dataSource.get();
}
/**
* 移除本地线程变量
*/
public static void clearDataSourceType() {
dataSource.remove();
}
}
此类主要是保存某个线程中数据源上下文的类,考虑到线程安全问题使用ThreadLocal类包装数据源类型;
ThreadLocal类是jdk提供的一个类,他主要是为每个线程变量保存了自己的一个副本,本质是一个Map结构,key是当前线程,value是线程变量,并发的情况下可以保证线程安全。
5.切面类
package com.xxx.demo.master_slave.aspect;
import com.xxx.demo.master_slave.annotation.DataSource;
import com.xxx.demo.master_slave.context.DataSourceContextHolder;
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.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* <p>
* 描述:
* </p>
* <p>
* 版权所有: 版权所有(C)2001-2019
* </p>
* <p>
* </p>
* <p>
* 版本1.0: 2019/10/17 14:48 新建
* </p>
*
* @author
* @version 1.0
*/
@Aspect
@Component
public class DataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
//切入点:所有使用了com.xxx.demo.master_slave.annotation.DataSource注解的方法
@Pointcut("@annotation(com.xxx.demo.master_slave.annotation.DataSource)")
public void dynamicDataSource(){
}
//切入方法前置调用
@Before("dynamicDataSource()")
public void before(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
logger.info("调用方法:{}",method.getName());
DataSource dataSource = method.getAnnotation(DataSource.class);
if(dataSource != null){
DataSourceContextHolder.setDataSourceType(dataSource.value());
}
}
//切入方法后置调用
@After("dynamicDataSource()")
public void after(){
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 切面注解类
*/
package com.xxx.demo.master_slave.annotation;
import java.lang.annotation.*;
/**
* <p>
* 描述:
* </p>
* <p>
* 版权所有: 版权所有(C)2001-2019
* </p>
* <p>
* </p>
* <p>
* 版本1.0: 2019/10/17 14:47 新建
* </p>
*
* @author Xu yicheng
* @version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSource {
/**
* 切换主从库 master/slave0
* @return
*/
String value();
}
主要逻辑是使用了DataSource注解的方法作为切入点,在加强的前置的调用中设置当前线程的数据源类型master/slave,master/slave由DataSource的value方法指定。
逻辑完成,下面看下测试效果
TestController.java
package com.xxx.demo.controller;
import com.xxx.demo.dao.entity.AccountBalance;
import com.xxx.demo.master_slave.annotation.DataSource;
import com.xxx.demo.scheduled.DyamicTask;
import com.xxx.demo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* <p>
* 描述:
* </p>
* <p>
* 版权所有: 版权所有(C)2001-2019
* </p>
* <p>
* </p>
* <p>
* 版本1.0: 2019/9/24 20:20 新建
* </p>
*
* @version 1.0
*/
@RequestMapping
@RestController
public class TestController {
@Autowired
private TestService testService;
@ResponseBody
@RequestMapping("/testQuery")
@DataSource("slave")
public String testQuery(){
List<AccountBalance> list = testService.queryList();
if(list ==null){
list = new ArrayList<>();
}
return Arrays.toString(list.toArray());
}
@ResponseBody
@RequestMapping("/testInsert")
@DataSource("master")
public String testInsert(){
AccountBalance accountBalance = new AccountBalance();
accountBalance.setCustomerId(1001);
accountBalance.setCustomerNm("master");
accountBalance.setBalance(new BigDecimal(1.00));
accountBalance.setAddDate(new Date());
testService.insert(accountBalance);
return "hello world";
}
}
普通的controller类,唯一的不同点是在testQuery()方法上使用了DataSource注解,并指定slave和在testInsert()方法上使用了DataSource注解,并指定master;
TestService.java
package com.xxx.demo.service;
import com.xxx.demo.dao.TestDao;
import com.xxx.demo.dao.entity.AccountBalance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 描述:
* </p>
* <p>
* 版权所有: 版权所有(C)2001-2019
* </p>
* <p>
* </p>
* <p>
* 版本1.0: 2019/10/17 16:42 新建
* </p>
*
* @author
* @version 1.0
*/
@Service
public class TestService {
@Autowired
private TestDao dao;
public List<AccountBalance> queryList(){
return dao.queryList();
}
public void insert(AccountBalance accountBalance){
dao.insert(accountBalance);
}
}
普通的service类;
TestDao.java
package com.xxx.demo.dao;
import com.xxx.demo.dao.entity.AccountBalance;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* 描述:
* </p>
* <p>
* 版权所有: 版权所有(C)2001-2019
* </p>
* <p>
* </p>
* <p>
* 版本1.0: 2019/10/17 16:44 新建
* </p>
*
* @version 1.0
*/
@Mapper
@Repository
public interface TestDao {
@Select("select * from account_balance")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "customerId", property = "customerId"),
@Result(column = "customernm", property = "customerNm"),
@Result(column = "balance", property = "balance"),
@Result(column = "adddate", property = "addDate")
})
List<AccountBalance> queryList();
@Insert("insert into account_balance(customerId,customernm,balance,adddate) values (#{accountBalance.customerId},#{accountBalance.customerNm},#{accountBalance.balance},#{accountBalance.addDate})")
void insert(@Param("accountBalance") AccountBalance accountBalance);
}
普通的mybatis 的mapper类;
测试:
http://localhost:8080/testInsert http://localhost:8080/testQuery
亲测可行!