如何更优雅的做缓存

    本博文主要讲解如何更加优雅的做缓存,缓存服务器与基础客户端连接、缓存代码在博主的博文《MemCached的安装和JAVA客户端连接Memcached示例代码》有介绍。优雅缓存的代码都是基于那篇博文基础上完成的。主要业务代码有如下:

/**  
* 缓存客户端接口类 
*/  
public interface CacheClient {  
   .......
}

/**  
* memcached缓存客户端持有者,交于Spring实例化
*/  
@Component  
public class MemcachedClientHolder implements CacheClient{
   //实现缓存接口
}
    有了如上的代码,就可以在业务代码中使用缓存功能了。

    但是我们可以想想,是否可以更加简单优雅的做缓存呢,是否可以让业务方更加简单的编写缓存使用的业务代码? 答案当然是可以的,请看如下分析。


业务场景描述

    例如网易新闻客户端,刚开始打开体育版块的时候,每个人看到的内容都是一样(暂不考虑千人千面推荐的情况),这个时候就非常适用缓存,但是业务代码中使用缓存就显得有些冗余,其实完全可以在Web应用RestApi接口级别做缓存。大概思路如下:

1:即自定义编写一个注解,用于收集缓存的时间、key前缀、排除的参数等信息。

2:在业务接口方法头中,设置好注解。这样就可以不用编写任何使用缓存的业务代码了。

各种购物网站首页的 banner、icon都是一样的道理,只是缓存的时间不同而已。如下详细讲解如上两点如何去做。


自定义注解

package com.cloud.practice.demo.cache.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 对Controller接口进行缓存
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cachable {

    /**
     * Expire time in seconds. default 取配置文件中的expire时间
     * @return  expire time 时间为秒,默认60秒
     */
    int expire() default  60;
    
    /**
     * 缓存Key的前缀,以防不同Controller相同接口相同参数导致Key/Value混乱
     * @return  默认tt
     */
    String prefix() default  "tt";

    /**
     * 排除掉那些参数
     * @return 要排除的参数次序,从0开始
     */
    int[] excludeArgs() default {};

}



在自定义注解上做AOP切面编程

package com.cloud.practice.demo.cache.aop;


/**
 * 对Controller的接口方法进行AOP,实现HTTP 接口的cache。 请求的处理流程:
 * 1.从cache中找,看是否hit,如果hit,直接返回结果
 * 2.如果没有命中,调用服务,调用完成后写入cacle中
 */
@Aspect
@Component
public class ControllerCacheAop {

	
	@Value("${cache.servers.memcached.expire:60}")
	private int memcachedServiceExpire ;
	
  @Autowired
  CacheClient memecacheClientHolder;
    
	@Autowired
	private HttpSession session;

    //定义一个切面在Controller上面
    @Pointcut("within(@org.springframework.stereotype.Controller *)")
    public void controllerPointcut() {
    }
    @Pointcut("within(@ org.springframework.web.bind.annotation.RestController *)")
    public void restControllerPointcut() {
    }

		//定义一个切面在@Cachable注解上面
    @Pointcut("@annotation(com.cloud.tasktrack.core.aop.Cachable)")
    public void cacheAnnotationPointCut() {
    }


    /**
     * 先从cache中取数据,找不到则调用controller原来的实现,后在写入缓存
     */
    @Around("(controllerPointcut() || restControllerPointcut()) &&  cacheAnnotationPointCut() ")
    @Order(1)
    public Object cacheAop(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //返回类型
        final Class<?> returnType = signature.getMethod().getReturnType();

    	//获得方法名、类名
    	String intefaceName=signature.getMethod().getName();
    	String className=joinPoint.getTarget().getClass().toString();
    	if(className.contains(".")){
				String[] _classNameTmp=className.split("\\.");
				className=_classNameTmp[_classNameTmp.length-1];
			}
    	long start= System.currentTimeMillis();
        
        //获得@Cachable注解的相关信息
        Cachable cacheAnnotation= signature.getMethod().getAnnotation(Cachable.class);

        final String cacheKey = this.getCacheKey(joinPoint, cacheAnnotation);
        //从缓存中获取数据
        try {
            Object _obj = this.memecacheClientHolder.get(cacheKey, returnType);
            if (_obj != null) {
            	long usedTimeMs= System.currentTimeMillis()-start;
                
        		User operationUser=(User)session.getAttribute("user");
        		String operationUserWebId=null,operationUserId=null;
        		if(null!=operationUser){
        			operationUserWebId=String.valueOf(operationUser.getWebId());
        			operationUserId=String.valueOf(operationUser.getId());
        		}
        		log.debug("============ Cache log: "+"webId "+ operationUserWebId + " userId "+ operationUserId);
        		
        		log.debug("============ Cache hit for key [" + cacheKey + "]" + " in " + usedTimeMs + "ms. " + " value is [" + _obj +"]");
                           
            return _obj;
            }
        }catch (Exception e){
        	log.warn("============ Cache exception for key[" + cacheKey + "]"+" Exception is : "+e.getMessage());
        }

        //缓存没有命中: 调用原来的请求,然后将结果缓存到cache中。
        Object intefaceCallResult;
        try {
        	intefaceCallResult = joinPoint.proceed();
            if (intefaceCallResult != null) {
                //一级缓存失效的时间,如果指定了,则使用指定的值。如果没有指定,则使用配置的值
                int expire = cacheAnnotation.expire();
                if (expire <= 0) {
                    expire = this.memcachedServiceExpire;
                }
                this.memecacheClientHolder.set(cacheKey, expire, intefaceCallResult);
                log.debug("============ Set cache value for key[" + cacheKey + "]" + " value in[" + intefaceCallResult +"]");
            }
        } catch (Exception e) {
        	intefaceCallResult=new ApiResponse.ApiResponseBuilder().code(ApiResponse.SERVICE_ERROR_CODE).message("服务调用异常"+e.getMessage()).build() ;
        }
        return intefaceCallResult;
    }

    /**
     * 根据拦截的方法的参数,生成cache的key.  prefix:METHOD_NAME:METHOD_PARAM
     * @param joinPoint   拦截点
     * @param cacheExpire
     * @return key
     */
    private String getCacheKey(ProceedingJoinPoint joinPoint, Cachable cacheExpire) {

        //获得要排除的方法参数
        int[] excludeParams = cacheExpire.excludeArgs();
        //获得要换成Key的前缀
        String prefix=cacheExpire.prefix();
        String cacheKeyPrefix = prefix+":" + joinPoint.getSignature().getName();

        //把参数连接起来
        List<String> methodParams = new LinkedList<>();
        Object arguments[] = joinPoint.getArgs();
        if (ArrayUtils.isNotEmpty(arguments)) {
            for (int i = 0; i < arguments.length; i++) {
                //排除掉某些参数
                if (ArrayUtils.contains(excludeParams, i)) {
                    continue;
                }
                Object arg = arguments[i];
                //fix key contain ' ' || b == '\n' || b == '\r' || b == 0
                String param = (arg == null ? "" : String.valueOf(arg));
                methodParams.add(param);
            }
        }
        return cacheKeyPrefix + ":" + String.join("-", methodParams);
    }
}


业务代码层面如何使用此种优雅缓存

package com.cloud.practice.demo.cache;

@Controller
@RequestMapping("/post")
public class CacheUsedDemoController {

    @Autowired
    private PostService postService;



    /**
     * 根据站点ID,站点ID(可有可无),查询所有岗位信息,需要附带返回站点下面的用户信息
     * @param request
     * @return
     */
    @RequestMapping("/getPostUser")
    @Cachable(expire = 120, prefix = "post", excludeArgs = 2)  //这种就使用了此种缓存
    @Loggerable(interfaceDesc = "查询岗位与岗位下用户信息")
    @ResponseBody
    public ApiResponse getPostUser(HttpServletRequest request) {
        ApiResponse apiResponse;
        int webId = CloudRequestPropertyUtils.getWebIdFromRequest(request);
        if (webId<1) {
            apiResponse = new ApiResponse.ApiResponseBuilder().code(ApiResponse.DEFAULT_CODE).message("站点ID不能为空,请重新输入").build();
        } else {
            List<Post> postUsers = postService.getPostUser(webId);
            apiResponse = new ApiResponse.ApiResponseBuilder().code(ApiResponse.DEFAULT_CODE).message(ApiResponse.DEFAULT_MSG)
                    .data(postUsers).dataCount(postUsers.size()).build();
        }
     		return apiResponse;
     }
}


    是不是感觉很棒很方便,大家不妨自己试试,的确很方便。此种思想也可以应用到日志记录等功能当中。

业务场景描述

表示java虚拟机堆区内存初始内存分配的大小,通常为操作系统可用内存的1/64大小即可,但仍需按照实际情况进行分配。
表示java虚拟机堆区内存可被分配的最大上限,通常为操作系统可用内存的1/4大小。

猜你喜欢

转载自blog.csdn.net/chengyun19830206/article/details/71048606