需求
假设项目中有一个几千万数据的大表,业务写入和读取都比较频繁。这个表随着数据的增多,数据库单表执行缓慢,逐渐成为应用瓶颈。所以需要迫切的解决这个问题。
思路
一般我们会有如下几个方案:
- 搭建一个只读从库,实现SpringBoot多数据源。(成本最小、对业务代码几乎无影响)
- 分库分表,引入sharding-jdbc,或独立部署的sharding-proxy。(引入成本高、分表需要改造sql代码)
- 引入Mycat,利用Mycat实现读写分离。(引入成本高,增加系统复杂度)
综合思考后,利用SpringBoot多数据源实现读写分离改造成本最低,对业务代码也基本无影响,而且可以对部分不需要读写分离的业务场景做灵活处理。
编码实现
一般项目会引入阿里开源的Druid管理数据库连接池,所以我们需要重新定义Druid的配置类,注入读和写两个数据源,实现AbstractRoutingDataSource数据源切换,定义切面在代码执行时切换数据源。
1.重写Druid的DataSource封装类
为了获取自定义数据源配,我们需要重写druid-spring-boot-starter的DataSource封装类,代码如下:
@ConfigurationProperties("spring.datasource.druid")
public class DruidDataSourceWrapperX extends DruidDataSource implements InitializingBean {
@Autowired
private DataSourceProperties basicProperties;
/* 只读数据源 */
private String url2;
private String username2;
private String password2;
@Override
public void afterPropertiesSet() throws Exception {
if (super.getUsername() == null) {
super.setUsername(StringUtils.isNotBlank(this.username2) ? this.username2 : this.basicProperties.determineUsername());
}
if (super.getPassword() == null) {
super.setPassword(StringUtils.isNotBlank(this.password2) ? this.password2 : this.basicProperties.determinePassword());
}
if (super.getUrl() == null) {
super.setUrl(StringUtils.isNotBlank(this.url2) ? this.url2 : this.basicProperties.determineUrl());
}
if (super.getDriverClassName() == null) {
super.setDriverClassName(this.basicProperties.getDriverClassName());
}
}
@Autowired(
required = false
)
public void autoAddFilters(List<Filter> filters) {
super.filters.addAll(filters);
}
@Override
public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
try {
super.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
} catch (IllegalArgumentException var4) {
super.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
}
}
public void setUrl2(String url2) {
this.url2 = url2;
}
public void setUserName2(String userName2) {
this.username2 = userName2;
}
public void setPassword2(String password2) {
this.password2 = password2;
}
}
2.继承AbstractRoutingDataSource实现动态路由
Spring提供了AbstractRoutingDataSource,用户可以继承实现自己的动态路由切换,详细可以查看spring的官方文档。
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<DBSourceEnum> CONTEXT_HOLDER = new ThreadLocal<>();
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
public static void setDataSource(DBSourceEnum dbSourceEnum) {
CONTEXT_HOLDER.set(dbSourceEnum);
}
public static DBSourceEnum getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
定义数据源枚举类:
public enum DBSourceEnum {
/**
master datasource
*/
MASTER,
/**
slave datasource
*/
SLAVE
}
3.实现DataSourceAutoconfig
完成读和写两个数据的bean定义,代码如下:
@Configuration
@Component
public class DruidDataSourceAutoConfigureX {
private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigureX.class);
public DruidDataSourceAutoConfigureX() {
}
@Bean
public DataSource masterDataSource() {
LOGGER.info("Init Master DruidDataSource");
return new DruidDataSourceWrapperX();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
LOGGER.info("Init Slave DruidDataSource");
return new DruidDataSourceWrapperX();
}
@Bean
@Primary
@DependsOn({"masterDataSource","slaveDataSource"})
public DynamicDataSource myDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
LOGGER.info("Init DynamicDataSource");
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DBSourceEnum.MASTER, masterDataSource);
targetDataSources.put(DBSourceEnum.SLAVE, slaveDataSource);
return new DynamicDataSource(masterDataSource, targetDataSources);
}
}
4.定义切面实现运行时动态切切换数据源
定义注解SlaveDataSource和切面DataSourceAspect,在dao层方法添加SlaveDataSource注解时,使用只读数据源。
@Target(ElementType.METHOD)
@RetentionPolicy.RUNTIME)
@Documented
public @interface SlaveDatasource{
String name() default "SLAVE";
}
@Aspect
@Component
public class DataSourceAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);
@Pointcut("@annotation(*.SlaveDatasource)" +
"&& execution(* *.dao.*.*(..))")
public void dataSourcePointCut(){}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
SlaveDatasource slaveDatasource = method.getDeclaredAnnotation(SlaveDatasource.class);
if (slaveDatasource != null) {
LOGGER.info("Use Slave DataSource!");
DynamicDataSource.setDataSource(DBSourceEnum.SLAVE);
}
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
}
5.mybatis接口方法添加注解
示例:
@Repository
public interface orderMapper {
@SlaveDatasource
Order getOrderById(Long id);
}
6.配置文件
主数据源还是按spring.datasource配置,在此基础上增加salve的配置:
spring:
datasource:
slave:
url2: xxx
username2: xxx
password: xxxx
总结
本次主要是利用Spring的数据源动态路由类和切面实现了读写分离,因为使用了druid连接池,所以druid相关配置类也做了重写。本文比较谨慎的使用注解判定是否使用只读数据源,读者也可以通过判断方法名是否含“query”等判定使用只读数据源,或根据业务灵活处理。