SpringCloud 基于 Hint 算法分片策略分库
1、添加 aop 、sharding-jdbc 依赖
<!--aop-> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--sharding-jdbc--> <dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </exclusion> </exclusions> </dependency>
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<String> 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<String> doSharding(Collection<String> availableTargetNames, ShardingValue shardingValue) { Collection<String> 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 操作。
本文转载自:https://blog.csdn.net/qq_39415129/article/details/106115503