通过springaop实现hibernate的分表

为什么要实现这样一个插件

系统运行过程中难免会遇到一些数据量很大的业务,如果简单的将他们的数据全部放置到一张表中,那么经历一段时间之后表的查询会很慢,诚然可以通过索引解决慢的问题,但是当数据量达到一个量级之后索引还是会挂掉,所以必须有一个分表插件。

为什么不直接使用其它的分表分库成熟产品

  1. 由于项目框架问题,贸然引入其它中间件需要考虑代码侵入,出现问题能否及时解决。
  2. 不自己造一下轮子,怎么之后轮子到底是方的还是⚪的

实现过程

  1. 因为当前使用的框架中orm层主要使用的是hibernate,所以我们需要在hibernate提供的时机对sql进行修改。和度娘一番深入交流之后发现EmptyInterceptor正是Hibernate为我们开的一扇小门,通过重写这个类我们就可以实sql自定义修改。
	// 参数持有缓存
	public final static WsdThreadLocalHolder<Map<String, Object>> params_holder = new WsdThreadLocalHolder<>();
	// 策略持有缓存
    public final static WsdThreadLocalHolder<WsdHibernateSqlInterceptorStrategy> strategyHolder = new WsdThreadLocalHolder<>();
 @Override
    public String onPrepareStatement(String sql) {
        WsdHibernateSqlInterceptorStrategy strategy = strategyHolder.get();
        if (strategy != null) {
            Map<String, Object> params = params_holder.get();
            if (params != null) {
                // 禁止本此拦截
                Object disable = params.get(DISABLE_CURRENT_INTERCEPTOR);
                if (disable == null || disable != true) {
                    // 注意 需要手动清除缓存 因为当调用分页方法的时候 本质上会调用2次sql  select count 和select
                    // 也就是说不能在select count(*) 执行之后直接清除缓存
                    return strategy.handle(sql, params);
                }
            }
        }
        return super.onPrepareStatement(sql);
    }

    /**
     * 事务提交/回滚之后清楚本地缓存的数据
     *
     * @param tx
     */
    @Override
    public void afterTransactionCompletion(Transaction tx) {
        super.afterTransactionCompletion(tx);
        clearHolder();
    }
  1. ImportBeanDefinitionRegistrar
    我们的hibernate拦截器代码编写好了之后又遇到一个比较操蛋的事情,因为LocalSessionFactoryBean对象是在框架内部(jar)通过xml注入的,我们不可能因为一些个性化需求去修改框架(修改框架之前一定要三思,尤其是很多项目在用的框架,这也是为什么本插件通过aop的方式实现,而不是直接插入到框架代码中),所以我们需要通过registrar 给该bean设置属性 entityInterceptor
public class WsdHibernateSessionFactoryRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinition bd = registry.getBeanDefinition("sessionFactoryAssemble");
        // 给这个bean定义添加一个属性
        bd.getPropertyValues().add("entityInterceptor",new WsdHibernateSqlInterceptor());
    }
}
  1. 接下来就是要实现如何切表名,其实网上有很多帖子很详细的描述了如何切表名,但是由于Hibernate拦截器中的onPrepareStatement只为我们提供了一个sql参数,然后我们又要干一堆很操蛋的事情,传递参数。基于易于拓展(总有一些人有些奇怪的分表需求,所以此处面向接口编程),在onPrepareStatement修改sql的过程中 其实我们干了2件事情,1.如何分表,2. 依据什么分表,所以这个里面使用到了2个接口

    分表策略

    分表策略指的是如何将旧表名替换成为新表名,这里面不用考虑如何获取新表名

    public interface WsdHibernateSqlInterceptorStrategy extends WsdHibernateSqlConstants{
    
    /** 处理sql */
    String handle(String sql, Map<String, Object> params);
    
    }
    

    参数解析器

    参数解析器的作用是构造分表策略分表时候使用的参数

    public interface WsdHibernateSqlInterceptorParameterResolver extends WsdHibernateSqlConstants {
    
    
    /**
     * 解析新增/删除/修改
     *
     * @param entity
     */
    void resolveCud(WsdBasicEntity entity);
    
    /**
     * 解析查询
     * <p>
     * <code>
     * WsdCriteria wci = new WsdCriteria();
     * Date date = new Date();
     * wci.add(WsdRestrictions.between("createdDate", WyDateUtils.firstOfMonth(date),WyDateUtils.lastOfMonth(date)));
     * List<HikvisionDoorEntity> pos = wsdToolDao.list(HikvisionDoorEntity.class, wci);
     * </code>
     */
    <ENTITY extends WsdBasicEntity> void resolveSelect(Class<ENTITY> entityClass, WsdCriteria wci);
    
    /**
     * 解析查询
     *
     * @param sr
     */
    <ENTITY extends WsdBasicEntity> void resolveSelect(Class<ENTITY> entityClass, SearchRequest sr);
    
    
    /**
     * 对数据进行分组
     *
     * @param args
     * @param <ENTITY>
     * @return
     */
    <ENTITY extends WsdBasicEntity> Map<String, List<ENTITY>> groupBy(List<ENTITY> args);
    
    /**
     * 获取sql中需要替换的旧表名
     *
     * @param entityClass
     * @param <ENTITY>
     * @return
     */
    <ENTITY extends WsdBasicEntity> String getOldTableName(Class<ENTITY> entityClass);
    
    /**
     * 获取实体在数据库中分表的所有表名
     *
     * @param entityClass
     * @return
     */
    List<String> getAllTableNames(Class entityClass);
    
    /**
     * 新增分表数据到缓存中 为定时器使用
     *
     * @param entityClassName
     * @param newTableName
     * @return
     */
    List<String> addNewTableNamesToCache(String entityClassName, String newTableName);
    }
    
  2. 现在我们应该考虑一个问题,如何在开发的过程中感知不到分表的动作,也就是进行统一入口封装。通过分析框架原有代码,我们发现框架虽然有很多方法可以访问db层,但是究其根本,底层方法就那几个,所以我们可以使用aop增强这几个方法,aop使用起来比较麻烦的地方主要有2点:

  1. 需要分析原有代码 找到合适的切点
  2. 在增强该切点的时候要考虑到能否向下兼容,也就是新加的切面是否会干扰到以前的正常业务,
    是否支持随意插拔
/**
 *
 * <p> 主要需要切掉下面这几个方法 但是需要注意的一点是需要使用实体名才能获取到配置信息
 * @see WsdToolDao#list(java.lang.String)
 * @see WsdToolDao#listByNameParams(java.lang.String, java.lang.String[], java.lang.Object[])
 * @see WsdToolDao#list(java.lang.String, java.lang.Object[])
 * @see WsdToolDao#createBatch(List)
 * @see WsdToolDao#updateBatch(List)
 * @see WsdToolDao#removeBatch(List)
 * <p>
 * 还需要切查询方法
 * @see this#aroundDoSearch
 * <p>
 * 值得注意的是wsdToolDao 中的listByIds 和getById 2个方法,这2个方法需要查询所有的表
 * 因为id上没有明确的标识属于哪个表或者使用另外的方法生成id ,而不是使用通常的uuid
 * @see this#aroundGetById(ProceedingJoinPoint)
 * @see this#aroundListByIds(ProceedingJoinPoint)
 **/

@Aspect
@Component("wsdHibernateSqlInterceptorAdvice")
public class WsdHibernateSqlInterceptorAdvice {

}

  1. 我们该如何让切面方法知道当前业务需要开启插件,最简单的方式是使用注解
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WsdSplitTable {

    /**
     * 指定分表策略和参数解析器
     * @return
     */
    WsdHibernateSqlInterceptorHolder value() default WsdHibernateSqlInterceptorHolder.General;
}

// 分表策略和参数解析器持有对象
public enum WsdHibernateSqlInterceptorHolder {
    NONE(null,null),
    General(new WsdHibernateGeneralStrategy(),new WsdHibernateGeneralResolver());
    public WsdHibernateSqlInterceptorStrategy strategy;
    public WsdHibernateSqlInterceptorParameterResolver resolver;

    WsdHibernateSqlInterceptorHolder(WsdHibernateSqlInterceptorStrategy strategy,WsdHibernateSqlInterceptorParameterResolver resolver) {
        this.strategy = strategy;
        this.resolver = resolver;
    }
}
  1. 分表实现的逻辑至此大致思路已经结束,但是我们还需要想另一个问题,也就是表从哪里来,
    本插件中定义2张配置表,然后通过定时器去提前创建表,即可解决这个问题

SYS_SPLITS_TABLE_CONFIG
指定分表创建策略的表,由定时器每天执行一次,获取下次创建时间在当天的表
在这里插入图片描述

SYS_SPLITS_TABLE_INFO
分表信息 里面只有2个字段configId(分表配置id) tableName(表名)

使用

@EntityDesc(desc = "门禁事件日志")
@Entity
@Table(name = "HIKVISION_EVENTS_DOOR")
@WsdSplitTable // 只用在实体类上加一个注解即可实现对该业务的分表
public class HikvisionDoorEntity extends WsdGenericEntity {

插件结构图

在这里插入图片描述

代码下载地址,只提供一个思路,具体的实现逻辑还是要基于具体的业务开发

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值