众所周知,当应用的访问量变大的时候,数据库的压力也会随之增大,这个时候除了可以使用缓存为数据库分担压力外,还可以使用备库做读写分离来分担主库压力。
最近项目可能要实现读写分离,看了不少资料,先实践实践,这里使用springboot整合mybatis、druid以AOP+自定义注解方式实现多数据源,读写分离。demo项目大致实现如下:
首先,新建一个maven项目:DynamicDataSource,在pom文件中引入springboot、mybatis、druid、AOP、connector相关依赖,具体如下:
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
org.mybatis.generator
mybatis-generator-core
1.3.5
com.alibaba
druid
1.1.0
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-aop
接着新建application.yml配置文件,并配置两个数据源,如下:
server:
port: 8080
servlet:
context-path: /demo
spring:
datasource:
# master config
master:
url: jdbc:mysql://192.168.1.102:3306/test?characterEncoding=utf-8
username: root
password:
driver-class-name: com.mysql.jdbc.Driver
# slave config
slave:
url: jdbc:mysql://192.168.1.103:3306/test?characterEncoding=utf-8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
同时新建数据源配置类:DataSourceConfig,具体如下:
/**
* 数据源相关配置
*/
@Configuration
@MapperScan(basePackages = "com.spring.boot.demo.DynamicDataSource.mapper")
public class DataSourceConfig {
@Bean(name = "dataSourceMaster")
@Primary // 当有多个实现类型时,注解了@Primary则优先使用
public DataSource dataSourceMaster(MasterConfig masterConfig) throws Exception {
DruidDataSource druidDataSourceMaster = new DruidDataSource();
druidDataSourceMaster.setUrl(masterConfig.getUrl());
druidDataSourceMaster.setUsername(masterConfig.getUsername());
druidDataSourceMaster.setPassword(masterConfig.getPassword());
druidDataSourceMaster.setDriverClassName(masterConfig.getDriverClassName());
return druidDataSourceMaster;
}
@Bean(name = "dataSourceSlave")
public DataSource dataSourceSlave(SlaveConfig slaveConfig) throws Exception {
DruidDataSource druidDataSourceSalve = new DruidDataSource();
druidDataSourceSalve.setUrl(slaveConfig.getUrl());
druidDataSourceSalve.setUsername(slaveConfig.getUsername());
druidDataSourceSalve.setPassword(slaveConfig.getPassword());
druidDataSourceSalve.setDriverClassName(slaveConfig.getDriverClassName());
return druidDataSourceSalve;
}
/**
* 动态数据源,配置需要使用到的多个数据源
* @param master
* @param slave
* @return
*/
@Bean
public DynamicDataSource dynamicDataSource(@Qualifier("dataSourceMaster") DataSource master, @Qualifier("dataSourceSlave") DataSource slave) {
Map
MasterConfig、SlaveConfig是两个配置文件类,这里略过,DataSourceEnum是数据源枚举类,如下:
/**
* 数据源枚举
*/
public enum DataSourceEnum {
/**
* 主库源
*/
DATA_SOURCE_MASTER,
/**
* 从库源
*/
DATA_SOURCE_SLAVE;
}
DynamicDataSourceContextHolder是一个线程的上下文类,为每个线程保存要使用的数据源类型,如下:
public class DynamicDataSourceContextHolder {
// 定义一个ThreadLocal变量,保存数据源类型(保证线程安全,多个线程之间互不影响)
private static final ThreadLocal DATA_SOURCE_CONTEXT_HOLDER = new ThreadLocal<>();
public static final DataSourceEnum DEFAULT_DATA_SOURCE = DataSourceEnum.DATA_SOURCE_MASTER;
static {
setDefaultDataSource(); // 默认指定主库
}
public static void setDefaultDataSource() {
DATA_SOURCE_CONTEXT_HOLDER.set(DEFAULT_DATA_SOURCE);
}
public static void setDataSource(DataSourceEnum dataSourceEnum) {
DATA_SOURCE_CONTEXT_HOLDER.set(dataSourceEnum);
}
public static DataSourceEnum getDataSource() {
return DATA_SOURCE_CONTEXT_HOLDER.get();
}
}
DynamicDataSource是自定义的动态数据源类,继承Spring的AbstractRoutingDataSource类(DataSource的路由类),重写determineCurrentLookupKey()方法,Spring通过此方法的返回值从targetDataSources中获取数据源(即动态切换数据源),如下:
/**
* 自定义动态数据源类
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
DataSourceEnum dataSourceEnum = DynamicDataSourceContextHolder.getDataSource();
logger.info("当前使用数据源为:{}", dataSourceEnum);
return dataSourceEnum;
}
}
然后新建数据源注解DataSourceAnnotation,并增加属性value,通过value指定要使用的数据源,默认值是主库,如下:
/**
* 自定义数据源注解
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceAnnotation {
DataSourceEnum value() default DataSourceEnum.DATA_SOURCE_MASTER;
}
新建数据源注解的切面实现类:DynamicDataSourceAspect,通过切面获取注解的数据源类型,并保存,具体实现如下:
/**
* 数据源注解切面实现
*/
@Aspect
@Component
public class DynamicDataSourceAspect {
@Before("@annotation(dataSourceAnnotation)")
public void before(JoinPoint point, DataSourceAnnotation dataSourceAnnotation) {
Class> clazz = point.getTarget().getClass();
MethodSignature signature = (MethodSignature) point.getSignature();
try {
Method method = clazz.getMethod(signature.getName(), signature.getParameterTypes());
if (method.isAnnotationPresent(DataSourceAnnotation.class)) {
// 根据注解设置数据源
DataSourceAnnotation annotation = method.getAnnotation(DataSourceAnnotation.class);
DynamicDataSourceContextHolder.setDataSource(annotation.value());
}
} catch (Exception e) {
e.printStackTrace();
}
}
@After("@annotation(dataSourceAnnotation)")
public void after(JoinPoint point, DataSourceAnnotation dataSourceAnnotation) {
DynamicDataSourceContextHolder.setDefaultDataSource();
}
}
到这里可以看到AOP+注解实现多数据源主要实现思路如下:
1、通过AOP获取注解的数据源类型并保存到ThreadLocal中(主要实现类:DynamicDataSourceAspect)
2、Spring就可以通过ThreadLocal获取当前线程的数据源类型进行切换(主要实现类:DynamicDataSource)
最后编写一个测试的service,使用上面的数据源注解,如下:
@Service
public class TestServiceImpl implements ITestService {
@Autowired
private TestMapper testMapper;
@Override
@DataSourceAnnotation(DataSourceEnum.DATA_SOURCE_MASTER) // 指定主库
public int insert(Test test) {
// TODO Auto-generated method stub
return testMapper.insert(test);
}
@Override
@DataSourceAnnotation(DataSourceEnum.DATA_SOURCE_SLAVE) // 指定从库
public Test select(Long id) {
// TODO Auto-generated method stub
return testMapper.selectByPrimaryKey(id);
}
@Override
@DataSourceAnnotation // 不指定,默认主库
public int updateByPrimaryKey(Test test) {
// TODO Auto-generated method stub
return testMapper.updateByPrimaryKey(test);
}
@Override // 无注解,默认主库
public int delete(Long id) {
// TODO Auto-generated method stub
return testMapper.delete(id);
}
}
同时修改Application启动类,注入service,测试测试效果,如下:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) // 去掉Spring的数据源自动配置
public class DynamicDateSourceApplication {
public static void main(String[] args) {
SpringApplication.run(DynamicDateSourceApplication.class, args);
}
@Autowired
private ITestService iTestService;
@PostConstruct
public void initTest() {
// init data
Test insert = new Test();
insert.setName("insert");
// insert
iTestService.insert(insert);
Long id = insert.getId();
System.out.println(id);
// select
Test select = iTestService.select(id);
System.out.println(select.toString());
// update
select.setName("update");
int update = iTestService.updateByPrimaryKey(select);
System.out.println(update);
// delete
int delete = iTestService.delete(id);
System.out.println(delete);
}
}
运行效果如下:
demo:https://github.com/191720653/DynamicDataSource.git