项目背景
公司新开的项目,需要连接业务库,业务库是集群部署的,整体结构,一个集群,对应多个分区,每个分区保存多个店铺数据,也有独立部署的店铺,分区就是数据库连接实例
大概思路
1.一集群都有一个公库,先连接公库地址,查询当前集群下面的所有分区数据库连接地址
2.拿到分区的数据连接地址之后,创建分区数据库连接
3.使用SpringBoot多数据源配置,动态切换数据源
上代码
直说大概思路,代码不方面贴出来,A集群,先看原来数据库设计,公库下主要有个cm_db(数据库表)表维护这当前集群下的所有分区连接信息,主要字段有db_id(数据库id),db_host(服务器地址,主从用","隔开),db_user(用户名),db_pwd(密码)和cm_seller(商家表)主要字段seller_id(卖家id),shop_id(店铺id),db_id(数据库id)
配置公库连接信息
spring:
application:
name: order-server # 应用名称
datasource:
druid:
a: #测试库
url: jdbc:mysql://xxx:3306/yyy?allowMultiQueries=true&useUnicode=true&useSSL=true&characterEncoding=utf8&tinyInt1isBit=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
# 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
initialSize: 10
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 100
# 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
maxWait: 60000
# Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的 sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SSELECT 1
# 建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
testWhileIdle: true
# 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间 (以秒数为单位)
remove-abandoned-timeout: 1800
logAbandoned: true
pinGlobalTxToPhysicalConnection: true
b: #测试库
url: jdbc:mysql://xxx:3306/yyy?allowMultiQueries=true&useUnicode=true&useSSL=true&characterEncoding=utf8&tinyInt1isBit=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
# 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
initialSize: 10
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 100
# 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
maxWait: 60000
# Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的 sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SSELECT 1
# 建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
testWhileIdle: true
# 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间 (以秒数为单位)
remove-abandoned-timeout: 1800
logAbandoned: true
pinGlobalTxToPhysicalConnection: true
######配置默认的数据源,配置Druid数据库连接池,配置sql工厂加载mybatis_plus的文件,扫描实体类等
@Slf4j
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {
/**
* a集群
* @return
*/
@Bean(name = "a")
@ConfigurationProperties(prefix = "spring.datasource.druid.a")
public DataSource a() {
return new DruidDataSource();
}
/**
* b集群
* @return
*/
@Bean(name = "b")
@ConfigurationProperties(prefix = "spring.datasource.druid.b")
public DataSource b() {
return new DruidDataSource();
}
/**
* 默认数据源配置和多数据源配置
*
* @return 数据源
*/
@Bean(name = "dynamicDataSource")
@Qualifier("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(masterDateSource());
// 配置多数据源
Map<Object, Object> dataBaseMap = new HashMap<>(16);
//默认数据源
dataBaseMap.put(Constant.DB_MASTER, a());
DruidDataSource fxcs = (DruidDataSource) a();
if (!StringUtils.isEmpty(a.getRawJdbcUrl())) {
log.info("初始化分区标识:{},url:{},用户名:{},密码:{}"
,"fxcs"
, a.getUrl()
, a.getUsername()
, a.getPassword());
dataBaseMap.put("a", a());
}
DruidDataSource fxcp = (DruidDataSource) b();
if (!StringUtils.isEmpty(b.getRawJdbcUrl())) {
log.info("初始化分区标识:{},url:{},用户名:{},密码:{}"
,"fxcp"
, b.getUrl()
, b.getUsername()
, b.getPassword());
dataBaseMap.put("b", b());
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
// 导入mybatissqlsession配置
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
// 指明数据源
sessionFactory.setDataSource(dataSource);
// 指明mapper.xml位置(配置文件中指明的xml位置会失效用此方式代替,具体原因未知)
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml"));
// 指明实体扫描(多个package用逗号或者分号分隔)
sessionFactory.setTypeAliasesPackage("com.xxx.entity");
// 导入mybatis配置
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sessionFactory.setConfiguration(configuration);
// 添加分页功能 乐观锁
sessionFactory.setPlugins(new Interceptor[]{
paginationInterceptor(),optimisticLockerInterceptor()
});
// 导入全局配置
sessionFactory.setGlobalConfig(globalConfiguration());
return sessionFactory.getObject();
}
/**
* 在代码中配置MybatisPlus替换掉application.yml中的配置
*
* @return
*/
@Bean
public GlobalConfiguration globalConfiguration() {
GlobalConfiguration configuration = new GlobalConfiguration();
configuration.setLogicDeleteValue("1");
configuration.setLogicNotDeleteValue("0");
// 主键类型 0:数据库ID自增, 1:用户输入ID,2:全局唯一ID (数字类型唯一ID), 3:全局唯一ID UUID
configuration.setIdType(0);
// 驼峰下划线转换
configuration.setDbColumnUnderline(true);
configuration.setMetaObjectHandler(new MyMetaObjectHandler());
configuration.setSqlInjector(new LogicSqlInjector());
// 是否动态刷新mapper
configuration.setRefresh(true);
return configuration;
}
/**
* mybatis-plus分页插件<br>
* 文档:http://mp.baomidou.com<br>
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setDialectType("mysql");
return paginationInterceptor;
}
/**
* 乐观锁插件
*
* @return
*/
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
/**
* 事务管理
*
* @param dataSource 数据源
* @return 事务管理
*/
@Bean(name = "sqlTransactionManager")
public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
数据源包装对象
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
public class TpDataSource {
private String url;
private String username;
private String password;
private String driveClass;
private String datasourceId;
}
手动切换数据源
@Slf4j
public class DataSourceContextHolder {
/**
* 线程独立
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**
* 获取数据源名
*
* @return 数据库名
*/
public static String getDataBaseType() {
return contextHolder.get();
}
/**
* 设置数据源名(切换数据源)
*在在事务之前设置 否则事务不生效 最好在controller层设置好
* @param dataBase 数据库类型
*/
public static void setDataBaseType(String dataBase) {
log.debug("设置数据源:{}",dataBase);
contextHolder.set(dataBase);
}
/**
* 清除数据源名
*/
public static void clearDataBaseType() {
contextHolder.remove();
}
}
创建数据源
@Data
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
//数据源列表
private Map<Object, Object> dynamicTargetDataSources;
//默认数据源
private Object dynamicDefaultTargetDataSource;
/**
* 获取当前数据源并打印日志记录
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
String datasource = DataSourceContextHolder.getDataBaseType();
if (!StringUtils.isEmpty(datasource)) {
Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
if (!dynamicTargetDataSources2.containsKey(datasource)) {
log.error("不存在的数据源:{}", datasource);
}
}
log.debug("当前数据源:{}",StringUtils.isEmpty(datasource)?"默认数据源":datasource);
return datasource;
}
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
this.dynamicTargetDataSources = targetDataSources;
}
@Override
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
}
public boolean createDataSource(TpDataSource tpDataSource) {
if (!testDatasource(tpDataSource)) {
log.error("测试数据源连接失效==数据源key:{},数据源url:{},账户:{},密码:{}"
,tpDataSource.getDatasourceId()
,tpDataSource.getUrl()
,tpDataSource.getUsername()
,tpDataSource.getPassword());
return false;
}
try {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setName(tpDataSource.getDatasourceId());
druidDataSource.setDriverClassName(tpDataSource.getDriveClass());
druidDataSource.setUrl(tpDataSource.getUrl());
druidDataSource.setUsername(tpDataSource.getUsername());
druidDataSource.setPassword(tpDataSource.getPassword());
druidDataSource.setValidationQuery("SELECT 1");
druidDataSource.init();
this.dynamicTargetDataSources.put(tpDataSource.getDatasourceId(), druidDataSource);
// 将map赋值给父类的TargetDataSources
setTargetDataSources(this.dynamicTargetDataSources);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
log.info("数据源初始化成功:{},{}", tpDataSource.getDatasourceId(), tpDataSource.getUrl());
} catch (SQLException throwables) {
log.error("数据源初始化失败==数据源key:{},数据源url:{},账户:{},密码:{}"
,tpDataSource.getDatasourceId()
,tpDataSource.getUrl()
,tpDataSource.getUsername()
,tpDataSource.getPassword());
log.error(throwables.getMessage(),throwables);
return false;
}
return true;
}
/**
* 测试数据源连接是否有效
*
* @return
*/
public boolean testDatasource(TpDataSource tpDataSource) {
try {
Class.forName(tpDataSource.getDriveClass());
DriverManager.getConnection(tpDataSource.getUrl(), tpDataSource.getUsername(), tpDataSource.getPassword());
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
}
初始化分区数据源
@Component
@Slf4j
public class CommonConfig {
@Resource(name = "dynamicDataSource")
private DynamicDataSource dynamicDataSource;
@Autowired
private DbService dbService;
@PostConstruct
public void init(){
Map<Object, Object> map = dynamicDataSource.getDynamicTargetDataSources();
HashMap<Object, Object> objectObjectHashMap = new HashMap<>(16);
for (Map.Entry<Object, Object> objectObjectEntry : map.entrySet()) {
objectObjectHashMap.put(objectObjectEntry.getKey(),objectObjectEntry.getValue());
}
for (Map.Entry<Object, Object> objectObjectEntry : objectObjectHashMap.entrySet()) {
log.info("数据源:{}",objectObjectEntry.getKey().toString());
try {
createDataSource(objectObjectEntry.getKey());
} catch (Exception e) {
log.error(e.getMessage(),e);
}
}
}
private void createDataSource(Object key){
DataSourceContextHolder.setDataBaseType(key.toString());
List<DbEntity> dbEntities = dbService.selectList(new EntityWrapper<DbEntity>());
for (DbEntity dbEntity : dbEntities) {
log.info("当前集群下:{},分区:{}",key.toString(), JSONObject.toJSONString(dbEntity));
String[] split = dbEntity.getDbHost().split(",");
String[] split1 = dbEntity.getDbUser().split(",");
String[] split2 = dbEntity.getDbPwd().split(",");
for(int a=0;a<split.length;a++){
TpDataSource build = TpDataSource.builder()
.datasourceId(split[a]+"_"+dbEntity.getDbName())
.url("jdbc:mysql://" + split[a] + ":" + dbEntity.getDbPort()
+ "/" + dbEntity.getDbName()
+ "?allowMultiQueries=true&useUnicode=true&useSSL=true&characterEncoding=utf8&tinyInt1isBit=false&rewriteBatchedStatements=true")
.username(split1[a])
.driveClass("com.mysql.jdbc.Driver")
.password(split2[a])
.build();
log.info("集群:{},准备创建数据源key:{},db_id:{},数据源url:{},用户名:{},密码:{}"
,key.toString()
, split[a]+"_"+dbEntity.getDbName()
, dbEntity.getDbId()
, build.getUrl()
, split1[a]
, split2[a]);
dynamicDataSource.createDataSource(build);
}
}
}
}
注解切换数据源
@Target(ElementType.METHOD) // 作用到方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
@Documented
public @interface CutDataSource {
/**
* 默认主库
*/
String source() default Constant.DB_MASTER;
}
AOP切面
@Aspect // 申明是个spring管理的bean
@Component
@Slf4j
@Order(-1) //确保该切面在transaction之前执行
@RequiredArgsConstructor
public class CutDataSourceAspect {
//当前service也是公库的表,维护这店铺分配那个分区
private final SellerService sellerService;
//分区连接信息
private final DbService dbService;
private final RedisUtil redisUtil;
/**
* 申明一个切点 里面是 execution表达式
*/
@Pointcut("@annotation(com.xxx.CutDataSource)")
private void controllerAspect() {
}
/**
* 请求method前打印内容
*
* @param joinPoint
*/
@Before(value = "controllerAspect()")
public void methodBefore(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//分区标识 从网关层传递过来的 可以通过店铺的域名 确定是那个集群下的
String db_tag = request.getHeader("DB_TAG");
String sid = request.getHeader("SID");
if (StringUtils.isEmpty(db_tag) || StringUtils.isEmpty(sid)) {
throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
, ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
}
//查询业务DB 可以适当的缓存
DataSourceContextHolder.setDataBaseType(db_tag);
DbEntity dbEntity;
String s = redisUtil.get(Constant.DB_TAG_SID + db_tag + "_" + sid);
if (StringUtils.isEmpty(s)) {
SellerEntity sellerEntity = sellerService.selectOne(new EntityWrapper<SellerEntity>()
.eq("shop_id", sid));
if (sellerEntity == null) {
log.error("当前公库找不到商铺信息=====IP:{},DB_TAG:{},SID:{},UID:{},请求路径:{},请求方式:{},请求类方法:{},请求类方法参数:{}"
, IpUtils.getIpAddress(request)
, db_tag
, sid
, request.getHeader("UID")
, request.getRequestURL().toString()
, request.getMethod()
, joinPoint.getSignature()
, Arrays.toString(joinPoint.getArgs()));
DataSourceContextHolder.clearDataBaseType();
throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
, ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
}
dbEntity = dbService.selectById(sellerEntity.getDbId());
if (dbEntity == null) {
log.error("当前公库找不到商铺DB信息=====IP:{},DB_TAG:{},SID:{},UID:{},请求路径:{},请求方式:{},请求类方法:{},请求类方法参数:{}"
, IpUtils.getIpAddress(request)
, db_tag
, sid
, request.getHeader("UID")
, request.getRequestURL().toString()
, request.getMethod()
, joinPoint.getSignature()
, Arrays.toString(joinPoint.getArgs()));
DataSourceContextHolder.clearDataBaseType();
throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
, ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
}
redisUtil.set(Constant.DB_TAG_SID + db_tag + "_" + sid, JSONObject.toJSONString(dbEntity), 60 * 5);
} else {
dbEntity = JSONObject.parseObject(s, DbEntity.class);
}
// 获取注解中的参数值
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 获取注解Action
CutDataSource annotation = method.getAnnotation(CutDataSource.class);
String value = annotation.source();
String[] split = dbEntity.getDbHost().split(",");
String datasourceId;
if (Constant.DB_MASTER.equals(value)) {
datasourceId = split[0];
} else if (Constant.DB_SLAVE.equals(value)) {
datasourceId= split.length > 1 ? split[1] : split[0];
} else {
throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
, ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
}
DataSourceContextHolder.setDataBaseType(datasourceId + "_" + dbEntity.getDbName());
log.info("IP:{},DB_TAG:{},数据源key:{},SID:{},UID:{},主从:{},请求路径:{},请求方式:{},请求类方法:{},请求类方法参数:{}"
, IpUtils.getIpAddress(request)
, db_tag
, datasourceId + "_" + dbEntity.getDbName()
, sid
, request.getHeader("UID")
, value
, request.getRequestURL().toString()
, request.getMethod()
, joinPoint.getSignature()
, Arrays.toString(joinPoint.getArgs()));
}
/**
* 清空contextHolder 防止内存溢出
*
* @param joinPoint
*/
@After(value = "controllerAspect()")
public void afterSwitchDataSource(JoinPoint joinPoint) {
DataSourceContextHolder.clearDataBaseType();
}
}
业务使用
*注意,从库是不能插入数据,可以根据自己的业务场景,配置是否主库
@RestController
public class TestController {
@RequestMapping(value = "/test", method = RequestMethod.GET)
@CutDataSource(source = Constant.DB_SLAVE)
public R test() {
return R.ok();
}
}