通过springaop实现hibernate的分表

    科技2022-09-06  104

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

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

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

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

    实现过程

    因为当前使用的框架中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(); } 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()); } }

    接下来就是要实现如何切表名,其实网上有很多帖子很详细的描述了如何切表名,但是由于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); }

    现在我们应该考虑一个问题,如何在开发的过程中感知不到分表的动作,也就是进行统一入口封装。通过分析框架原有代码,我们发现框架虽然有很多方法可以访问db层,但是究其根本,底层方法就那几个,所以我们可以使用aop增强这几个方法,aop使用起来比较麻烦的地方主要有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 { } 我们该如何让切面方法知道当前业务需要开启插件,最简单的方式是使用注解 @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; } } 分表实现的逻辑至此大致思路已经结束,但是我们还需要想另一个问题,也就是表从哪里来, 本插件中定义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 {

    插件结构图

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

    Processed: 0.013, SQL: 12