SpringBoot+AOP构建多数据源的切换实践+公共库存储数据源信息+MyBatis
【前言】
原本是考虑用baomidou实现的,但实际引用之后,发现不太适合我的场景,在看了一天源码和百度类似场景案例后,左抄抄,右改改,调试出适配我的案例,所以很多代码跟baomidou源码很像,也有一些做法跟百度大佬们类似,记录一下笔记,勿喷
【场景描述】
当全国业务量扩展,订单等表数据暴增之后,考虑到分库分表缓解数据库压力。
简单的说,分库分表分为两种水平分库和垂直分库,该场景很明显为水平分库,但由于分表的数目过多,且数据源多个(本文场景是按区分库,如华南区就一个分库,上海区就一个分库),所以,结合常规做法的基础上,将数据源信息存储在公共库中,在接口访问请求之后,通过aop拦截controller,获得参数值,通过该参数值转换成,以此扩展开来。
【知识储备】
1.了解AbstractRoutingDataSource
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。更加具体的,大家还是去看源码或者参考其他博客,这一方面很详细。
在这里只提三个方法:
(1)determineTargetDataSource:该方法就是用来切换数据源的
(2)setTargetDataSources(Map<Object, Object> targetDataSources):这个方法大有用处,我们在启动时候,就可以通过查询公共库,将数据源的信息都查出,通过该方法进行设置
(3)afterPropertiesSet:重点列出该方法,是因为大家在调试数据源的时候,可以通过观察该方法来实现。resolvedDataSources值就是存储数据源的,也是在该方法中实现,该值还在determineTargetDataSource中用到,切数据源就是从该值中取出来的。
2.了解mybatis在springboot中的配置
在这里就不啰嗦,百度一大把。
【具体实现】
1.pom.xml
(1)数据库依赖:公司用sqlserver,mysql和oracle的话,自己百度,应该没啥难度
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
<version>6.4.0.jre8</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<version>4.0</version>
<scope>test</scope>
</dependency>
(2)aop和mybatis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
2.bootstrap.yml
(1)mybtis配置:由于后面我写了MyBatisConfig配置类,所以这里的值就是一个参数而已,可以读取。当然,不写配置类,mybatis这样配置也是可以生效的
mybatis:
mapper-locations: classpath:mapper/**/*Mapper.xml
type-aliases-package: com.mylnet.purple.module.apply.entity
(2)数据库配置:同理,正常这样配置是可以使用的了,但是我们自己写配置,所以这里数据源信息就是参数的作用
#公共库
spring:
datasource:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
url: jdbc:sqlserver://ip:端口;DatabaseName=公共库名
username: *****
password: *****
#type: com.alibaba.druid.pool.DruidDataSource
3.DynamicDataSource动态数据源实现类:就是前面讲到的,实现AbstractRoutingDataSource,重写其方法,来完成我们的需求,比较重要的一步,但也是千篇一律的一步
package com.mylnet.purple.module.apply.dataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/**
* (切换数据源必须在调用service之前进行,也就是开启事务之前)
* 动态数据源实现类
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
* 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
*/
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
/**
* 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
/**
* 设置默认数据源
* @param defaultDataSource
*/
public void setDefaultDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
/**
* 设置数据源
* @param dataSources
*/
public void setDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
// 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
}
}
4.DynamicDataSourceContextHolder动态上下文类:使用时候,调用这个类就行
package com.mylnet.purple.module.apply.dataSource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* (切换数据源必须在调用service之前进行,也就是开启事务之前)
* 动态数据源上下文
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 将 master 数据源的 key作为默认数据源的 key
*/
@Override
protected String initialValue() {
return "master";
}
};
/**
* 数据源的 key集合,用于切换时判断数据源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* 切换数据源
* @param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* 获取数据源
* @return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 重置数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
/**
* 判断是否包含数据源
* @param key 数据源key
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* 添加数据源keys
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
5.至此,我们就可以开始进行场景设计和开发了:结合我的场景,我需要写个MyBatisConfig配置类,去实现mybatis的配置,并在这个过程中,做一件最重要的事情,将我们的实现类DynamicDataSource通知给SqlSessionFactoryBean,这个点百度上有8成的文章没有讲,有点坑。
总结一下,该配置类做了哪些事情:
(1)将DynamicDataSource通知给SqlSessionFactoryBean
(2)配置事务管理
(3)自定义数据源,就是查询公共库,将所有分库信息查出,进而就可以通过DynamicDataSourceContextHolder进行添加数据源了
(4)配置mybatis,前面2(1)里面写的参数,就是在这里用到
(5)当然,结合我这边的场景,我要将一些数据进行缓存预热起来,便在这里处理了。
package com.mylnet.purple.module.apply;
import com.mylnet.purple.module.apply.dataSource.DynamicDataSource;
import com.mylnet.purple.module.apply.entity.DbProperties;
import com.mylnet.purple.module.apply.entity.SysDbShardingInfo;
import com.mylnet.purple.module.apply.entity.SysWahoShardingRelation;
import com.zaxxer.hikari.HikariDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* springboot集成mybatis的基本入口 1)创建数据源(如果采用的是默认的tomcat-jdbc数据源,则不需要)
* 2)创建SqlSessionFactory 3)配置事务管理器,除非需要使用事务,否则不用配置
*
* 通过读取application.properties文件生成两个数据源(myTestDbDataSource、myTestDb2DataSource)
使用以上生成的两个数据源构造动态数据源dataSource
@Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错(一般用于多数据源的情况下)
@Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有两个DataSource类型的实例,需要指定名称注入)
@Bean:生成的bean实例的名称是方法名(例如上边的@Qualifier注解中使用的名称是前边两个数据源的方法名,而这两个数据源也是使用@Bean注解进行注入的)
通过动态数据源构造SqlSessionFactory和事务管理器(如果不需要事务,后者可以去掉)
*
*
*/
@Configuration
public class MyBatisConfig {
@Autowired
private Environment env;
@Resource
private DbProperties dbProperties;
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
@Value("${mybatis.type-aliases-package}")
private String typeAliasesPackage;
@Resource
private RedisTemplate redisTemplate;
/**
* 创建主数据源
* @return
*/
@Bean
public DataSource master() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dbProperties.getUrl());
dataSource.setUsername(dbProperties.getUsername());
dataSource.setPassword(dbProperties.getPassword());
dataSource.setDriverClassName(dbProperties.getDriverClassName());
return dataSource;
}
/**
* 生成自定义的数据源
* @return
*/
@Bean("dynamicDataSource")
@Primary
public DataSource dynamicDataSource(){
//数据关联关系进行缓存预热
setDbRelationRedis();
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object,Object> mapDataSource = getBaseDataSource();
mapDataSource.put("master", master());
//将master数据源作为指定的数据源
dynamicDataSource.setDefaultDataSource(master());
dynamicDataSource.setDataSources(mapDataSource);
return dynamicDataSource;
}
@Bean
@Primary
public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
//配置数据源,此处配置为关键配置,如果没有将dynamicDataSource作为数据源则不能实现切换
sessionFactoryBean.setDataSource(dynamicDataSource());
//扫描model-entity的包
sessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
//扫描映射文件
PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
sessionFactoryBean.setMapperLocations(pathMatchingResourcePatternResolver.getResources(mapperLocations));
return sessionFactoryBean;
}
/**
* 配置事务管理,使用事务时哎方法头部添加@Transactional注解即可
* @return
*/
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dynamicDataSource());
}
/**
* 自定义数据源
* @return
*/
@Bean
public Map<Object,Object> getBaseDataSource() {
System.out.println("-------------------------[MyBatisConfig]-------------------------");
System.out.println("【公共库信息:】");
System.out.println("url:" + dbProperties.getUrl());
System.out.println("username:" + dbProperties.getUsername());
System.out.println("password:" + dbProperties.getPassword());
System.out.println("driver-class-name:" + dbProperties.getDriverClassName());
System.out.println("sql:" + dbProperties.getSql());
Map<Object, Object> mapDataSource = new HashMap<Object, Object>();
JdbcTemplate jdbcTemplate = new JdbcTemplate(master());
List<SysDbShardingInfo> sysDbShardingInfoList = jdbcTemplate.query(dbProperties.getSql(), new BeanPropertyRowMapper<SysDbShardingInfo>(SysDbShardingInfo.class));
if (null != sysDbShardingInfoList && sysDbShardingInfoList.size() > 0) {
for (SysDbShardingInfo sydb : sysDbShardingInfoList) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(dbProperties.getDriverClassName());
dataSource.setJdbcUrl(sydb.getShardingURL());
dataSource.setUsername(sydb.getShardingUserName());
dataSource.setPassword(sydb.getShardingPassword());
mapDataSource.put(sydb.getShardingAlias(), dataSource);
}
}
return mapDataSource;
}
/**
* 预热缓存:将仓库id/sapid/组织对应数据源信息的关系缓存起来,每次接口访问的时候,通过缓存找到对应的数据源名称
*/
@Bean
public void setDbRelationRedis() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(master());
String sql = "select wahoID,wahoSapID,organId,shardingAlias from SysWahoShardingRelation";
List<SysWahoShardingRelation> sysWahoShardingRelationList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<SysWahoShardingRelation>(SysWahoShardingRelation.class));
if (null != sysWahoShardingRelationList && sysWahoShardingRelationList.size() > 0) {
for (SysWahoShardingRelation relation : sysWahoShardingRelationList) {
redisTemplate.opsForValue().set("org-" + relation.getOrganId(), relation.getShardingAlias());
redisTemplate.opsForValue().set("waho-" + relation.getWahoID(), relation.getShardingAlias());
redisTemplate.opsForValue().set("sap-" + relation.getWahoSapID(), relation.getShardingAlias());
}
}
}
}
6.启动类两个注解,别漏了,这里就不过多解释了
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@MapperScan(basePackages = { "com.mylnet.purple.module.apply.mapper" }, sqlSessionFactoryRef = "sqlSessionFactory")
7.最后一步,就是实战,也就是aop部分
备注说明,(1)我这里拦截controller(2)解析出来参数的值,因为我们这边有三个参数值是跟数据源有关的,任何一个查到对应的数据源名称即可,进而通过数据源名称就可以设置当前数据源了,这样便完成数据源的切换
package com.mylnet.purple.module.apply.dataSource;
import com.mylnet.purple.module.apply.entity.DbProperties;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
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.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* 接口拦截aop,进行数据源设置
* fangzw
* 2021-04-26
*/
@Slf4j
@Aspect
@Component
@Order(1)
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspect {
@Resource
private DbProperties dbProperties;
@Resource
private RedisTemplate redisTemplate;
@Pointcut("execution(* com.mylnet.purple.module.apply.controller.wcs.*.*(..)) ")
public void pointcut() {}
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) {
// 参数值
Object[] args = joinPoint.getArgs();
// 参数名
String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
if (null != args) {
//仓库id索引
int wahoIDIndex = ArrayUtils.indexOf(argNames, "wahoID");
//sap的id索引
int wahoSapIDIndex = ArrayUtils.indexOf(argNames, "wahoSapID");
//所属组织id索引
int organIdIndex = ArrayUtils.indexOf(argNames, "organId");
//通过仓库id/sapid/organid查询对应所属区,该区名为数据源名
StringBuffer queryDataSourceNameSql = new StringBuffer();
String key = null;
if (-1 != wahoIDIndex && StringUtils.isNotBlank((String)args[wahoIDIndex])) {
//仓库id
String wahoID = (String)args[wahoIDIndex];
queryDataSourceNameSql.append("select shardingAlias from SysWahoShardingRelation where wahoID = '" + wahoID + "'");
key ="waho-" + wahoID;
} else if (-1 != wahoSapIDIndex && StringUtils.isNotBlank((String)args[wahoSapIDIndex])) {
//sapid
String wahoSapID = (String)args[wahoSapIDIndex];
queryDataSourceNameSql.append("select shardingAlias from SysWahoShardingRelation where wahoSapID = '" + wahoSapID + "'");
key = "sap-" + wahoSapID;
} else if (-1 != organIdIndex && StringUtils.isNotBlank((String)args[organIdIndex])) {
//sapid
String organId = (String)args[organIdIndex];
queryDataSourceNameSql.append("select shardingAlias from SysWahoShardingRelation where organId = '" + organId + "'");
key = "org-" + organId;
} else {
throw new RuntimeException("接口不规范,必须仓库id/sapid/organid三个参数必须至少有一个值!");
}
//执行查询:先通过缓存查询,查不到再进行数据查询
if (key != null) {
String dataSourceName = null;
Object dataSourceNameRedis = redisTemplate.opsForValue().get(key);
if (null != dataSourceNameRedis) {
dataSourceName = (String) dataSourceNameRedis;
} else {
JdbcTemplate jdbcTemplate = new JdbcTemplate(pubDataSource());
//数据源名称
dataSourceName = jdbcTemplate.queryForObject(queryDataSourceNameSql.toString(), String.class);
if (null == dataSourceName || !DynamicDataSourceContextHolder.containDataSourceKey(dataSourceName)) {
throw new RuntimeException("数据源不存在!dataSourceName为:" + dataSourceName);
}
}
log.info("------------------设置当前数据源------------------------------");
log.info(dataSourceName);
DynamicDataSourceContextHolder.setDataSourceKey(dataSourceName);
}
}
}
@After("pointcut()")
public void after(JoinPoint point) {
//清理掉当前设置的数据源,让默认的数据源不受影响
DynamicDataSourceContextHolder.clearDataSourceKey();
}
/**
* 公共库数据源
* @return
*/
public DataSource pubDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dbProperties.getUrl());
dataSource.setUsername(dbProperties.getUsername());
dataSource.setPassword(dbProperties.getPassword());
dataSource.setDriverClassName(dbProperties.getDriverClassName());
return dataSource;
}
}
最后,项目不能对外提供,有疑问请留言!