皆非的万事屋

Mybatis结合Redis缓存以注解开发时的查询优化方案——通过AOP和反射重新实现@One、@Many注解

前言

[scode type="share"]现在算是终于有空来写这篇文章了,由于考研的问题一直没有时间来补这篇文章[/scode]

@Select("SELECT id,dept_id,username,nick_name,gender,phone,email,avatar_path,password," +
            "is_admin,enabled,create_by,update_by,pwd_reset_time,create_time,update_time" +
            " FROM sys_user u WHERE u.id = #{id} AND is_deleted=0")
    @Results({
            @Result(column = "id", property = "id"),
            @Result(column = "dept_id", property = "deptId"),
            @Result(column = "dept_id", property = "dept",
                    one = @One(select = "marchsoft.modules.system.mapper.DeptMapper.selectById",
                            fetchType = FetchType.EAGER)),
            @Result(column = "id", property = "roles",
                    many = @Many(select = "marchsoft.modules.system.mapper.RoleMapper.findWithMenuByUserId",
                            fetchType = FetchType.EAGER)),
            @Result(column = "id", property = "jobs",
                    many = @Many(select = "marchsoft.modules.system.mapper.JobMapper.findByUserId",
                            fetchType = FetchType.EAGER))
    })
    UserBO findUserDetailById(Long id);

但是没有考虑到一句@One就是一条新的sql,一句@Many就又是n条sql,在没有缓存的情况下效率是非常低的,这就牵扯到之后的不断探究与讨论了。

探究

[scode type="share"]在之后的不断讨论与探究中,主要从以下几个方面就行抉择:[/scode]
[scode type="blue"]选择 springboot + redis 缓存[/scode]

@Service
@AllArgsConstructor
@CacheConfig(cacheNames = "role")
public class RoleServiceImpl implements IRoleService {

    private final RoleMapper roleMapper;

    @Override
    @Cacheable(key = "'id:' + #p0")
    public Role getOneById(Serializable id) {
        return this.roleMapper.selectById(id);
    }

    @Override
    public List<Role> getRolesById(Serializable id) {
        return roleMapper.getRoleByUser(id);
    }
}

[scode type="blue"]springboot缓存方式与@One、@Many之间的矛盾[/scode]

[scode type="blue"]mybatis的二级缓存[/scode]

@CacheNamespace(implementation = MybatisRedisCache.class, eviction = MybatisRedisCache.class)
@Component
public interface UserMapper extends BasicMapper<User> {}
public class MyRedisCache implements Cache {

    //当前放入缓存的mapper的namespace
    private final String id;

    //必须要有这个构造方法
    public MyRedisCache(String id){
        this.id = id;
    }

    //返回cache唯一标识————即namespace
    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public void putObject(Object key, Object value) {
        System.out.println("key : " + key.toString() + "--- value : " + value.toString());
        //使用redishash类型作为缓存存储模型 key hashkey value
        getRedisTemplate().opsForHash().put(id.toString(), key.toString(), value);
    }

    @Override
    public Object getObject(Object key) {
        return getRedisTemplate().opsForHash().get(id.toString(), key.toString());
    }

    @Override
    public Object removeObject(Object o) {
        return null;
    }

    //清空整个id的所有key缓存,mybatis默认走这个
    @Override
    public void clear() {
        getRedisTemplate().delete(id.toString());
    }

    @Override
    public int getSize() {
        return getRedisTemplate().opsForHash().size(id).intValue();
    }

    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

[scode type="blue"]不使用@One、@Many注解,手动封装结果,调用service层方法走springboot缓存[/scode]

最终方案

[scode type="share"]由于博主本人需要考研的原因,11月之后不再参与框架的更新迭代,缓存的问题也一直遗留了下来,目前第二版框架SMPE已经基本成型,最大的遗留问题就是缓存问题了[/scode]
[scode type="blue"]2021年1月10日博主再次参与框架的更新迭代中,并且再次接手框架缓存的解决方案[/scode]
[scode type="blue"]2021年1月10日晚与同学星星讨论后,定下尝试解决方案——重新实现@One与@Many注解,使新注解可以调用service层/mapper层的方法,从而走springboot缓存,其目标为既可以到达原生@One与@Many的开发效率,又可以使用最理想的缓存存储方案![/scode]

// 重新实现 @One、@Many注解,关联查询注解,用于mapper层查询方法上,仅需关联查询一个属性时,可以直接使用@Query
// 如果需要使用多个@Query,请先使用@Queries套在外层(类似 @Results)参数、作用基本和原@One保持一致
// 既可以用于一对一也可以一对多
//
// @author Wangmingcan
// Date: 2021/01/12 09:35
//
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Queries.class)
public @interface Query {

    /** 被关联的列,一般为id,请保证和实体类中属性名称一致,驼峰下划线都可以
     * 如关联的列为dept_id,填deptId和dept_id都可以*/
    String column() default "";

    /** 关联的属性 ,和实体类中需要封装的属性名称保持一致*/
    String property() default "";

    /** 执行的查询方法,建议填写mapper层的全限定方法名 ,方法返回值必须和property类型一致*/
    String select() default "";

}
//
// 多个关联查询注解,用于mapper层查询方法上
// 使用方法类比 @Results ,里面套多个 @Query
//  Queries({
//        @ Query(),@Query(),...
//  })
//
// Date: 2021/01/12 09:35
//
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Queries {

    Query[] value() default {};

}
// Query注解具体实现
//
// Date: 2021/01/12 09:35
//
@Aspect
@Component
public class QueryAspect {

    /** Query注解配置切面织入点 */
    @Pointcut("@annotation(marchsoft.annotation.Query)")
    public void queryPointcut() {}

    /** Queries注解配置切面织入点 */
    @Pointcut("@annotation(marchsoft.annotation.Queries)")
    public void queriesPointcut() {}

    //
    // Date :  2021-01-12 09:50
    // @param joinPoint 切入点
    // @param result 目标方法返回值
    // 方法执行后切入并修改目标方法返回值
    //
    @AfterReturning(value = "queryPointcut()", returning = "result")
    public void queryAround(JoinPoint joinPoint, Object result)  {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method signatureMethod = signature.getMethod();
        Query query = signatureMethod.getAnnotation(Query.class);
        //如果目标方法返回类型为 Collection的实现类,如List,则通过迭代器遍历
        //如果目标方法返回类型为 IPage,则先获取getRecords()的List,再通过迭代器遍历
        if (result instanceof Collection || result instanceof IPage) {
            Iterator iter;
            if (result instanceof Collection) {
                iter = ((Collection) result).iterator();
            }else {
                iter = ((IPage) result).getRecords().iterator();
            }
            while(iter.hasNext()){
                execute(query, iter.next());
            }
        }else { //如果目标方法返回类型即为实体类
            execute(query, result);
        }
    }

    @AfterReturning(value = "queriesPointcut()", returning = "result")
    public void queriesAround(JoinPoint joinPoint, Object result)  {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method signatureMethod = signature.getMethod();
        Queries queries = signatureMethod.getAnnotation(Queries.class);
        if (result instanceof Collection || result instanceof IPage) {
            Iterator iter;
            if (result instanceof Collection) {
                iter = ((Collection) result).iterator();
            }else {
                iter = ((IPage) result).getRecords().iterator();
            }
            while(iter.hasNext()){
                Object o = iter.next();
                //针对每个实体处理多个@query
                for (Query query : queries.value()) {
                    execute(query, o);
                }
            }
        }else {
            for (Query query : queries.value()) {
                execute(query, result);
            }
        }
    }

    //
    // Date : 2021-01-11 15:38
    // @param query 注解query
    // @param result 目标方法返回值
    //
    private void execute(Query query, Object result) {
        //获取需要被赋值的属性
        String property = query.property();
        //获取需要执行的方法
        String select = query.select();
        //获取关联的列,并转驼峰
        String column = StringUtils.toCamelCase(query.column());
        //关联属性的类型(也为select的参数类型)
        Class<?> columnType;
        //关联列的值
        Serializable columnValue;
        //select方法返回值类型
        Class<?> selectMethodReturnType;
        //select方法返回结果
        Object selectResult;
        try {
            //目标方法返回值类型class
            Class<?> resultClass = result.getClass();
            //通过反射获取get方法并调用来获取关联列的值
            try {
                Method getMethod = resultClass.getMethod("get" +
                        column.substring(0, 1).toUpperCase() + column.substring(1));
                columnValue = (Serializable) getMethod.invoke(result);
                columnType = getMethod.getReturnType();
            }catch (NoSuchMethodException get) {
                throw new BadRequestException("找不到get方法 : " + get.getMessage() +
                        ",请检查column参数" + column + "是否填写正确");
            }
            //通过反射获取该select方法所在的类
            int pointIndex = select.lastIndexOf(".");
            Class<?> selectMethodClass = Class.forName(select.substring(0, pointIndex));
            //通过反射获取需要调用的select方法
            try{
                Method selectMethod = selectMethodClass.getMethod(select.substring(pointIndex+1), columnType);
                //调用并获取select方法的结果
                selectResult = selectMethod.invoke(SpringContextHolder.getBean(selectMethodClass),
                        columnValue);
                selectMethodReturnType = selectMethod.getReturnType();
            }catch (NoSuchMethodException selectExc) {
                throw new BadRequestException("找不到select方法 : " + selectExc.getMessage() +
                        ",请检查select方法的名称和参数类型");
            }
            //通过反射获取set方法并调用来完成赋值
            try{
                Method setMethod = resultClass.getMethod("set" +
                        property.substring(0, 1).toUpperCase() + property.substring(1), selectMethodReturnType);
                setMethod.invoke(result, selectResult);
            }catch (NoSuchMethodException set) {
                throw new BadRequestException("找不到set方法 : " + set.getMessage() +
                        ",请检查" + property + "的类型与方法" + select + "的返回值是否一致");
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
@Select("SELECT id,dept_id,username,nick_name,gender,phone,email,avatar_path,password," +
            "is_admin,enabled,create_by,update_by,pwd_reset_time,create_time,update_time" +
            " FROM sys_user u WHERE u.id = #{id} AND is_deleted=0")
    @Results({
            @Result(column = "id", property = "id"),
            @Result(column = "dept_id", property = "deptId"),
    })
    @Queries({
            @Query(column = "id", property = "roles",
                    select = "marchsoft.modules.system.mapper.RoleMapper.findWithMenuByUserId"),
            @Query(column = "id", property = "jobs",
                    select = "marchsoft.modules.system.mapper.JobMapper.findByUserId"),
            @Query(column = "dept_id", property = "dept",
                    select = "marchsoft.modules.system.mapper.DeptMapper.selectById")
    })
    UserBO findUserDetailById(Long id);
@Component
@CacheConfig(cacheNames = "dept")
public interface DeptMapper extends BasicMapper<Dept> {

    @Cacheable(key = "'id:' + #p0")
    Dept selectById(Long id);
}

总结

缓存的添加与维护

情况一:单一实体缓存,key和value都仅与该实体相关(1:1)

@CacheConfig(cacheNames = "job")
public interface JobMapper extends BasicMapper<Job> {
@Cacheable(key = "'id:' + #p0")
Job selectById(Long id);
public interface CacheKey {
    /**
     * 用户
     */
    String USER_ID = "user::id:";
    /**
     * 数据
     */
    String DATA_USER = "data::user:";
    /**
     * 菜单
     */
    String MENU_ID = "menu::id:";
    String MENU_USER = "menu::user:";
    String MENU_ROLE = "menu::role:";
    /**
     * 角色授权
     */
    String ROLE_AUTH = "role::auth:";
    String ROLE_USER = "role::user:";
    /**
     * 角色信息
     */
    String ROLE_ID = "role::id:";
    /**
     * 部门信息
     */
    String DEPT_ID = "dept::id:";
    String DEPT_ROLE = "dept::role:";
    /**
     * 岗位信息
     */
    String JOB_ID = "job::id:";      //示例
    String JOB_USER = "job::user:";
}

[scode type="blue"]在对应的service层手动维护缓存[/scode]


    /**
     * 清理缓存
     * @param id job_id
     */
private void delCaches(Long id){
    redisUtils.del(CacheKey.JOB_ID + id);
}
@CacheEvict(key = "'id:' + #p0.id")
public void update(Job resources) {}

[scode type="yellow"]对于单一实体的缓存,仅缓存 selectById方法,对于该实体的分页查询等其他条件查询一般不做缓存,除非有特殊要求[/scode]

情况二:缓存内容关联两个及以上的实体(1:n,n:m)

//UserMapper中查询一个用户
@Select("SELECT id,dept_id,username,nick_name,gender,phone,email,avatar_path,password," +
            "is_admin,enabled,create_by,update_by,pwd_reset_time,create_time,update_time" +
            " FROM sys_user u WHERE u.id = #{id} AND is_deleted=0")
    @Results({
            @Result(column = "id", property = "id"),
            @Result(column = "dept_id", property = "deptId"),
    })
    @Queries({
            @Query(column = "id", property = "roles",
                    select = "marchsoft.modules.system.mapper.RoleMapper.findWithMenuByUserId"),
            @Query(column = "id", property = "jobs",
                    select = "marchsoft.modules.system.mapper.JobMapper.findByUserId"),  //这里
            @Query(column = "dept_id", property = "dept",
                    select = "marchsoft.modules.system.mapper.DeptMapper.selectById")
    })
    @Cacheable(key = "'id:' + #p0")
    UserBO findUserDetailById(Long id);
//JobMapper中根据用户id查询岗位
@Select("SELECT j.id, j.name, j.enabled, j.job_sort, j.create_by, j.update_by, j.create_time, j.update_time " +
            "FROM sys_job j, sys_users_jobs uj WHERE j.id = uj.job_id AND uj.user_id = ${id} AND j.is_deleted=0")
@Cacheable(key = "'user:' + #p0")
Set<Job> findByUserId(Long id);
//UserMapper下
//根据job_id查询用户Id (清理job缓存时调用)
@Select("SELECT user_id FROM sys_users_jobs WHERE job_id = #{id} group by user_id")
List<Long> findByJobId(Long id);
//JobServiceImpl
private void delCaches(Long id){
    List<Long> userIds = userMapper.findByJobId(id);
    redisUtils.delByKeys(CacheKey.JOB_USER, new HashSet<>(userIds)); //第二种情况所删的缓存
    redisUtils.del(CacheKey.JOB_ID + id);   //第一种情况所删的缓存
}

[scode type="blue"]当中间表sys_users_jobs发生变动时,一定要清理缓存![/scode]

//UserServiceImpl下的更新user方法
public void updateUserWithDetail(UserInsertOrUpdateDTO userInsertOrUpdateDTO) {
//...
//如果岗位发生变化
   if (! CollectionUtils.isEqualCollection(jobIds, userInsertOrUpdateDTO.getJobs())) {
      //...
      //清除缓存
      redisUtils.del(CacheKey.JOB_USER + userInsertOrUpdateDTO.getId());
   }
//...
}

[scode type="yellow"]对于情况一的缓存只涉及一个实体(表),缓存维护比较好实现,针对第二种情况需要我们考虑全面,在哪些地方需要去维护,这个需要考虑清除,不要随意加缓存[/scode]
[scode type="red"]谁加了哪个缓存,谁就有义务去维护它![/scode]

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »