sharding分表应用笔记(三)——多数据源路由

发布时间 2023-11-20 09:45:46作者: pjgyr

sharding分表应用笔记(三)——多数据源路由

1 背景

应用背景:物理数据源只有一个;对于部分数据量大的表实行按月分表处理,其他的表仍然保持原先的模式不变。因为sharding对一些sql语句可能有兼容问题,所以为了预防生产问题,决定直接通过原数据源对未分表的表格进行操作,只有特定的表格才通过sharding数据源进行操作。所以需要进行多数据源路由。本篇记录sharding分表的多数据源路由实现。

环境:spring

2 配置

2.1 命名空间配置

<!-- 多数据源配置 -->
<bean id="multipleDataSource" class="com.example.dbConfig.MultipleDataSource">
    <!-- 默认数据源 -->
    <property name="defaultTargetDataSource" ref="dataSource" />
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="DEFAULT" value-ref="dataSource"/>
            <entry key="SHARDING" value-ref="shardingDataSource"/>
        </map>
    </property>
</bean>

之后用多数据源的BeanId(multipleDataSource)替换各种数据库配置中原先的数据源BeanId(dataSource)

2.2 spring-jdbc路由配置

继承实现spring-jdbc的AbstractRoutingDataSource多数据源路由接口。实现该接口后,每次进行数据库操作时会通过determineCurrentLookupKey方法获取当前设定的数据库key,然后通过命名空间配置的映射启用对应的数据源。

/**
 * @ClassName: MultipleDataSource
 * @Description: 数据源路由实现类
 * @Author: nagiumi
 * @Date: 2023/6/5
 * @Version:1.0
 */
public class MultipleDataSource extends AbstractRoutingDataSource {
    private static final Logger log = LoggerFactory.getLogger(MultipleDataSource.class);
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceConfig = MultipleDataSourceHandler.getRouteKey();
        return dataSourceConfig;
    }
}
/**
 * @ClassName: MultipleDataSourceHandler
 * @Description: 多数据源持有类,数据源路由
 * @Author: nagiumi
 * @Date: 2023/6/5
 * @Version:1.0
 */
public class MultipleDataSourceHandler {
    private static final Logger logger = LoggerFactory.getLogger(MultipleDataSourceHandler.class);
    /**
     * 线程副本,保证线程安全
     */
    private static ThreadLocal<String> routeKey = new ThreadLocal<String>();

     /**
     *@Description: 绑定当前线程数据源路由的key 使用完成后必须调用removeRouteKey()方法删除
     *@Param:  * @Param: key
     *@return:
     *@Author: nagiumi
     *@date: 2023/6/5
     */
    public static void setRouteKey(String key){
        logger.warn("系统切换到[{}]数据源",key);
        routeKey.set(key);
    }

    /**
    *@Description: 获取当前线程的路由
    *@Param:  * @Param: null
    *@return: 
    *@Author: nagiumi
    *@date: 2023/6/5
    */
    public static String getRouteKey(){
        return routeKey.get();
    }

    /**
    *@Description: 删除与当前线程绑定的路由key
    *@Param:  * @Param: null
    *@return: 
    *@Author: nagiumi
    *@date: 2023/6/5
    */
    public static void removeRouteKey(){
        logger.warn("系统重置数据源");
        routeKey.remove();
    }
}

3 指定路由

3.1 自定义注解

自定义一个指定路由数据库的注解,通过在表的service类上添加注解来完成对应表的指定路由操作。

数据源枚举

public interface ContextConstant {
    /**
    *@Description: 数据源枚举
    *@Param:  * @Param: null
    *@return:
    *@Author: nagiumi
    *@date: 2023/6/5
    */
    enum DataSourceType {
        DEFAULT, SHARDING
    }
}

定义注解

/**
 * @ClassName: ContextConstant
 * @Description: 自定义切换数据源注解
 * @Author: nagiumi
 * @Date: 2023/6/5
 * @Version:1.0
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    /**
    *@Description: 默认为上报数据库数据源
    *@Param:  * @Param: null
    *@return: 
    *@Author: nagiumi
    *@date: 2023/6/5
    */
    ContextConstant.DataSourceType value() default ContextConstant.DataSourceType.DEFAULT;
}

3.2 功能实现

定义aop进行注解扫描,然后反射获取注解指定的路由

/**
 * @ClassName: DynamicDataSourceAspect
 * @Description: 切换数据源类
 * @Author: nagiumi
 * @Date: 2023/6/5
 * @Version:1.0
 * 设置为1,优先加载,优先级高于AbstractRoutingDataSource的determineCurrentLookupKey()
 *
 */
@Component
@Aspect
@Order(1)
public class DynamicDataSourceAspect {

    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    /**
    *@Description: 前置切面
    *@Param:  * @Param: null
    *@return:
    *@Author: nagiumi
    *@date: 2023/6/5
    */
    @Before("execution(* com.example.business..*.*(..))")
    public void before(JoinPoint point) {
        try {
            DataSource annotationOfClass = point.getTarget().getClass().getAnnotation(DataSource.class);
            String methodName = point.getSignature().getName();
            Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
            Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
            DataSource methodAnnotation = method.getAnnotation(DataSource.class);
            methodAnnotation = methodAnnotation == null ? annotationOfClass : methodAnnotation;
            ContextConstant.DataSourceType dataSourceType = methodAnnotation != null
                    && methodAnnotation.value() != null ? methodAnnotation.value() : null;
            if (Objects.nonNull(dataSourceType)) {
                log.warn("aspect 准备切换数据源,发起方:{}", point.getTarget().getClass().getName() + "#" + methodName);
                MultipleDataSourceHandler.setRouteKey(dataSourceType.name());
            }
        } catch (NoSuchMethodException e) {
            log.error("aspect error,err={}",e);
        }
    }

    /**
    *@Description: 后置切面
    *@Param:  * @Param: null
    *@return:
    *@Author: nagiumi
    *@date: 2023/6/5
    */
    @After("execution(* com.example.business..*.*(..))")
    public void after(JoinPoint point) {
        String oldKey = MultipleDataSourceHandler.getRouteKey();
        if (StringUtils.isNotBlank(oldKey)) {
            MultipleDataSourceHandler.removeRouteKey();
        }
    }
}

3.3 用例

@Service
@DataSource(ContextConstant.DataSourceType.SHARDING)
public class RepaymentLogBizService {

    @Autowired
    private RepaymentLogMapper repaymentLogMapper;

    //业务方法

}