为什么要实现这样一个插件
系统运行过程中难免会遇到一些数据量很大的业务,如果简单的将他们的数据全部放置到一张表中,那么经历一段时间之后表的查询会很慢,诚然可以通过索引解决慢的问题,但是当数据量达到一个量级之后索引还是会挂掉,所以必须有一个分表插件。
为什么不直接使用其它的分表分库成熟产品
- 由于项目框架问题,贸然引入其它中间件需要考虑代码侵入,出现问题能否及时解决。
- 不自己造一下轮子,怎么之后轮子到底是方的还是⚪的
实现过程
- 因为当前使用的框架中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 {