SpringCloud 基于 Hint 算法分片策略分库

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 进行沟通。

 

 

你可能感兴趣的:(SpringCloud)