应用场景
由于公司业务存在大量数据需要做筛选查询,使用drools作为内存筛选器,需要将所有数据放到内存进行筛选,当数据达到一个量级时就不适用直接从数据库查询,效率极低;
于是我牵头设计了Redis一级缓存,和本地内存二级缓存的缓存模型,二级缓存使用guava包里的Cache 实现不多说了,这里主要介绍一下Redis缓存;
为了保证缓存的实时性,我选择在对数据进行操作的微服务上进行切面监听其对应的mapper。如果是update, insert 就进行缓存的更新,每个用户的数据使用单独的hash 存在Redis里面。
具体实现
首先我定义了一个自定义注解,可以扩展至任何实体类的缓存处理上;
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface RedisAutoUpdate {
}
SqlHelper工具类 获取sql类型
package com.knx.organization.dao.util;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
/**
* Mybatis - 获取Mybatis查询sql工具
*
*/
public class KnxSqlHelper {
/**
* 获取sql 类型
* @param session
* @param namespace
* @return
*/
public static String getSqlCommandType(SqlSession session, String namespace) {
Configuration configuration = session.getConfiguration();
MappedStatement mappedStatement = configuration.getMappedStatement(namespace);
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
return sqlCommandType.name();
}
}
然后对注解切面获取对应的mapper 代理切面:
package com.knx.organization.aspect;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
/**
* @author joker
* 自定缓存
* @date 2021/6/9
*/
@Aspect
@Component
@Slf4j
public class MapperAspect {
@Autowired
//我们自己定义的RedisUtil 推荐使用RedisTemplate
OrgRedisUtil orgRedisUtil;
@Autowired
SqlSessionFactory sqlSessionFactory;
@Autowired
SqlSession sqlSession;
private static String INSERT_STRING = "insert";
private static String UPDATE_STRING = "update";
@Pointcut("execution(* com.***.***.dao.mapper.*.*(..))")
public void sql() {
}
@After("sql()")
public void after(JoinPoint pjp) throws Throwable {
//获取mapper 代理
Object target = pjp.getTarget();
//获取代理的mapper
Type[] type = target.getClass().getGenericInterfaces();
Class ct1 = Class.forName(type[0].getTypeName());
//拿到mapper上的注解
Annotation[] declaredAnnotations = ct1.getDeclaredAnnotations();
//获取mapper 对应的实体名
String mapperName = ct1.getGenericInterfaces()[0].getTypeName();
String[] names = mapperName.split("<");
//去掉末尾 有个 ">"
String entityName = names[names.length - 1].substring(0, names[names.length - 1].length() - 1);
for (int i = 0; i < declaredAnnotations.length; i++) {
if (declaredAnnotations[i].annotationType().equals(RedisAutoUpdate.class)) {
updateCache(pjp, ct1, entityName);
}
}
}
// 判断是update,insert 具体更新缓存的方法
private void updateCache(JoinPoint pjp, Class ct, String entityName){
Class entityClass = null;
try {
entityClass = Class.forName(entityName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//获取sql 类型
String sqlCommandType = KnxSqlHelper.getSqlCommandType(sqlSession, ct.getName() + "." + pjp.getSignature().getName());
if (sqlCommandType.toLowerCase().startsWith(INSERT_STRING) || sqlCommandType.toLowerCase().startsWith(UPDATE_STRING)) {
// 拿到入参里面的对象id 进行更新 为保证数据准确性 我选择拿到id 再从数据查询更新缓存
Object[] args = pjp.getArgs();
List<String> ids = Lists.newArrayList();
for (int i1 = 0; i1 < args.length; i1++) {
//批量修改 更新缓存
if (args[i1] instanceof Collection) {
List<BaseModel> list = (List<BaseModel>) args[i1];
list.forEach(e -> ids.add(e.getId()));
}
//更新单条
if (args[i1].getClass().getName().equals(entityName)) {
BaseModel baseModel = (BaseModel) args[i1];
ids.add(baseModel.getId());
}
}
// 最后 根据 ids更新缓存 TODO 根据你自己的业务场景 更新对应实体id 的缓存 就不贴我的实现了
}
}
}