1、添加 aop 、sharding-jdbc 依赖
io.shardingsphere
sharding-jdbc-spring-boot-starter
com.google.guava
guava
2、yml文件配置
# 数据源
sharding.jdbc.datasource.names=xxx-pub,xxx-ha,xxx-sd,xxx-zj,xxx-hq,xxx-tw,xxx-js,xxx-fj
# 河南
sharding.jdbc.datasource.xxx-ha.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.xxx-ha.driver-class-name=com.mysql.cj.jdbc.Driver
sharding.jdbc.datasource.xxx-ha.jdbcUrl=jdbc:mysql://localhost:3306/xxx_ha?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
sharding.jdbc.datasource.xxx-ha.username=root
sharding.jdbc.datasource.xxx-ha.password=root
# 山东
sharding.jdbc.datasource.xxx-sd.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.xxx-sd.driver-class-name=com.mysql.cj.jdbc.Driver
sharding.jdbc.datasource.xxx-sd.jdbcUrl=jdbc:mysql://localhost:3306/xxx_sd?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
sharding.jdbc.datasource.xxx-sd.username=root
sharding.jdbc.datasource.xxx-sd.password=root
# 浙江
sharding.jdbc.datasource.xxx-zj.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.xxx-zj.driver-class-name=com.mysql.cj.jdbc.Driver
sharding.jdbc.datasource.xxx-zj.jdbcUrl=jdbc:mysql://localhost:3306/xxx_zj?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
sharding.jdbc.datasource.xxx-zj.username=root
sharding.jdbc.datasource.xxx-zj.password=root
3、编写 DatasourceRouteAspect 切面类
package com.xxx.xxx.xxx.config.shard;
import com.xxx.xxx.common.shard.RequestShardValueHolder;
import io.shardingsphere.api.HintManager;
import io.shardingsphere.core.hint.HintManagerHolder;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Description: 数据源强制路由切面,在进入DAO之前,强制根据省分库方案,使用sharding-jdbc实现强制路由
*/
@Component
@Aspect
public class DatasourceRouteAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(DatasourceRouteAspect.class);
/**
* DAO层切面
*/
//@Pointcut("execution( * com.xxx.xxx.xxx..*.dao.*Mapper.*(..))")
@Pointcut("execution( * com.xxx.xxx.xxx..*.*Mapper.*(..))")
public void pointCut() {
}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
HintManagerHolder.clear();
HintManager hintManager = HintManager.getInstance();
String province = RequestShardValueHolder.get();
LOGGER.info("aspect:current request thread id:{}, province code:{}", Thread.currentThread().getId(), province);
hintManager.setDatabaseShardingValue(province);
}
/**
* 完成之后清理shardvalue
*
* @param joinPoint
*/
@After("pointCut()")
public void after(JoinPoint joinPoint) {
HintManagerHolder.clear();
}
}
4、编写分库路由值实体类
package com.xxx.xxx.common.shard;
/**
* Description: 分库路由值(省份编码)暂存器,提供线程封闭环境
*/
public class RequestShardValueHolder {
/**
* 当前目标数据源键值
*/
private static final ThreadLocal CURRENT_SHARD_VALUE = new ThreadLocal<>();
/**
* 插入目标数据源键值
*
* @param provinceCode 省份编码
*/
public static void set(String provinceCode) {
CURRENT_SHARD_VALUE.set(provinceCode);
}
/**
* 获取目标数据源键值
*
* @return 目标数据源键值
*/
public static String get() {
return CURRENT_SHARD_VALUE.get();
}
public static void remove() {
CURRENT_SHARD_VALUE.remove();
}
}
5、编写 config
package com.xxx.xxx.xxx.service.config;
import com.xxx.xxx.common.shard.RequestShardValueInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Description:
*/
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
@Bean
public RequestShardValueInterceptor requestShardValueInterceptor() {
return new RequestShardValueInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestShardValueInterceptor()).addPathPatterns("/**").excludePathPatterns("/api/bill/**").excludePathPatterns("/**/InquiryNmfaXxxResultSrv");
}
}
6、编写拦截器,每次请求都拦截,从session 中获取用户省份,set 到 RequestShardValueHolder 中。
package com.xxx.xxx.common.shard;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.xxx.xxx.common.model.sys.SysUserVo;
import com.xxx.xxx.common.utils.SessionUtil;
/**
*/
public class RequestShardValueInterceptor extends HandlerInterceptorAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(RequestShardValueInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO 从request中header中获取province参数作为键值
SysUserVo userVo = SessionUtil.getSessionUserInfo(request);
if (userVo == null) {
LOGGER.warn("无法从http请求的header中解析出用户信息,request url:{},has no shardvalue", request.getRequestURI());
return true;
}
String province = userVo.getPrvCode();
// 集团用户直接从header中取值
if (userVo.getLevel() == 0) {
province = SessionUtil.getUserProvince(request);
LOGGER.info("---------集团用户,分库取值{}", province);
}
if (province == null) {
LOGGER.warn("user:{},has no shardvalue", userVo);
return true;
}
LOGGER.info("filter:current request thread id:{}, province code:{}", Thread.currentThread().getId(), province);
// 在Threalocal中添加当前线程的province code 作为目标数据源键值
RequestShardValueHolder.set(province.toLowerCase());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
String province = RequestShardValueHolder.get();
LOGGER.info("interceptor:current request thread id:{}, province code:{}", Thread.currentThread().getId(),
province);
RequestShardValueHolder.remove();
LOGGER.info("afterCompletion");
}
}
7、编写Hint分片策略类(自定义分片策略)
package com.xxx.xxx.xxx.config.share;
import io.shardingsphere.api.algorithm.sharding.ListShardingValue;
import io.shardingsphere.api.algorithm.sharding.ShardingValue;
import io.shardingsphere.api.algorithm.sharding.hint.HintShardingAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.HashSet;
/**
* Description: 自定义hint分片策略,使用提示而不是SQL来指示分片结果。
*/
public class ProvinceHintShardingAlgorithm implements HintShardingAlgorithm {
private static final Logger LOGGER = LoggerFactory.getLogger(ProvinceHintShardingAlgorithm.class);
private static final String DEFAULT_DATA_SOURCE = "xxx-pub";
@Override
public Collection doSharding(Collection availableTargetNames, ShardingValue shardingValue) {
Collection result = new HashSet<>();
if (shardingValue instanceof ListShardingValue) {
Collection shardingValues = ((ListShardingValue) shardingValue).getValues();
//TODO 解析出对应省份代码的数据源
if (availableTargetNames != null && !availableTargetNames.isEmpty() && shardingValues != null && !shardingValues.isEmpty()) {
for (String availableName : availableTargetNames) {
for (Object value : shardingValues) {
if (value != null && availableName.endsWith(value.toString().toLowerCase())) {
result.add(availableName);
}
}
}
}
}
//匹配到目标库,直接返回目标库
if (!result.isEmpty()) {
LOGGER.info("force route data source to:{}", result);
} else {
LOGGER.warn("has no target datasource, route to datasource: {}", DEFAULT_DATA_SOURCE);
result.add(DEFAULT_DATA_SOURCE);
}
return result;
}
}
最终:前端访问接口,在 mapper 每次访问数据库的时候,会先执行 aspect 切面,去确定执行哪个省份库,系统才会对对应的库进行 CRUD 操作。
目前是采用Hint分片策略来进行分库,但是 Mycat 进行分库好像更好一点,后期继续学习分享。
我是进阶的球儿,大家一起2019年的爬坑历程。感觉分享很给力的话给个赞,谢谢!!!有问题也可以下方留言或者加本人QQ:313989006 进行沟通。