简介
随着系统QPS逐渐提高,在一些读取频繁变更较少数据场景下,适当添加缓存不仅能提升用户访问速度还可以减轻系统压力。
使用缓存主要有两个用途:高性能、高并发
高性能:
-
查询mysql耗时需要300ms,存到缓存redis,每次查询仅需要几毫秒,性能瞬间提升百倍。
高并发
-
mysql 单机支撑2K QPS就容易报警了,使用缓存的话,单机支撑的并发量轻松1s几万~十几万。
缓存位于内存,内存对高并发的良好支持。
目的
通过统一缓存方式,规范缓存实现。
缓存命中走缓存。不命中加锁查询防止大量穿透引起雪崩;
通过统一注解方式,也减少了大量的冗余代码 。
减少重复,干掉重复。
本文主要讲解,一款基于自定义注解拦截+切面+redis的通用方法缓存方式 ;
支持场景
业务自定义的查询方法均可使用;
包括无参数的方法,有参数的方法;
可以根据不同的参数值做缓存 ;
技术位面
Redis
Redisson
fastjson
切面(Aspect)
序列化 (Serializable)
SpringBoot (示例工程)
功能示例图:
核心实现
实现原理简介
自定义注解 @CacheData ;
通过切面 CacheAspect 拦截添加自定义注解的方法 ;
通过参数生成Redis缓存key
-
解析方法的参数并按照参数顺序排序
将转换好的参数转换为Json字符串
-
本来想用toString的考虑到继承等没有重写方法就改用了Json
将参数进行MD5,一方面安全性,一方面保证Redis key不要过长,节约空间 ;
代码实现
1.自定义注解
/** * 自定义缓存注解 * * @author 程序员小强 * @see CacheAspect */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface CacheData { /** * 自定义缓存key前缀 * 默认:方法名 */ String keyPrefix() default ""; /** * 缓存过期时间 * 单位默认:秒,不设置默认10分钟 */ int expireTime() default 600; /** * 缓存过期-随机数 * 单位默认:秒,默认0分钟 * 注:设置后实际过期时间, * 会在expireTime基础上继续累积(0~randomExpire)之间的秒数,防止缓存大量失效大面积穿透,造成雪崩 */ int randomExpire() default 0; /** * 是否存储为null 的返回 * 注:防止缓存穿透,默认true,建议查询为空时,也进行缓存 */ boolean storageNullFlag() default true;}
2.拦截切面实现
/** * 统一缓存自定义注解拦截实现 * * @author 程序员小强 */@Aspect@Componentpublic class CacheAspect { private static final Logger log = LoggerFactory.getLogger(CacheAspect.class); private static final String EMPTY = ""; private static final String POINT = "."; private static final String CACHE_KEY_PREFIX = "cache.aspect:"; private static final String LOCK_KEY_PREFIX = "lock."; @Resource private RedisCache redisCache; /** * redisson client对象 */ @Resource private RedissonClient redisson; /** * 匹配所有使用以下注解的方法 * 注意:缓存是基于类+方法+参数内容做的缓存key,重载方法可能会出现问题 * 禁止在同一个类,方法名相同的两个方法使用 * * @see CacheData */ @Pointcut("@annotation(com.example.cache.annotation.CacheData)") public void pointCut() { } /** * 拦截添加缓存注解的方法 * * @param pjpParam * @return * @throws Throwable * @see CacheData */ @Around("pointCut()&&@annotation(cacheData)") public Object logAround(ProceedingJoinPoint pjpParam, CacheData cacheData) throws Throwable { //注解为空 if (null == cacheData) { return pjpParam.proceed(pjpParam.getArgs()); } //仅用于方法 if (null == pjpParam.getSignature() || !(pjpParam.getSignature() instanceof MethodSignature)) { return pjpParam.proceed(pjpParam.getArgs()); } //方法实例 Method method = ((MethodSignature) pjpParam.getSignature()).getMethod(); //方法类名 String className = pjpParam.getSignature().getDeclaringTypeName(); //方法名 String methodName = method.getName(); log.debug("[ CacheAspect ] >> notReadCacheFlag:{} , refreshCacheFlag :{} , method:{} ", CacheContext.notReadCacheFlag.get(), CacheContext.refreshCacheFlag.get(), className + methodName); //已设置-不需要读取缓存 if (null != CacheContext.notReadCacheFlag.get() && CacheContext.notReadCacheFlag.get()) { return pjpParam.proceed(pjpParam.getArgs()); } //生成缓存key String cacheKey = this.getCacheKey(cacheData, className, methodName, pjpParam); //已设置-需要刷新缓存 if (null != CacheContext.refreshCacheFlag.get() && CacheContext.refreshCacheFlag.get()) { Object resultValue = pjpParam.proceed(pjpParam.getArgs()); //刷新缓存 redisCache.putByte(cacheKey, SerializableUtil.serialize(resultValue, cacheKey), this.getExpireTime(cacheData)); return resultValue; } Object value = null; //缓存中读取值 byte[] cacheByteValue = redisCache.getByte(cacheKey); if (null != cacheByteValue && cacheByteValue.length > 0) { //反序列化对象 value = SerializableUtil.deserialize(cacheByteValue); } //缓存中存在并且不为空-直接返回 if (null != value && !cacheKey.equals(value.toString())) { log.info("[ CacheAspect ] >> [ get from cache in first ] method:{} , cacheKey:{}", className + methodName, cacheKey); return value; } //如果设置了允许存储null值,若缓存key存在,并且value与自定义key相同 > 则直接返回 null if (cacheData.storageNullFlag() && redisCache.exists(cacheKey)) { log.info("[ CacheAspect ] >> [ get from cache in first value is null ] method:{} , cacheKey:{}", className + methodName, cacheKey); return null; } //若缓存中不存在 > 则执行方法,并重入缓存 //加锁防止大量穿透 RLock lock = redisson.getLock(LOCK_KEY_PREFIX + cacheKey); lock.lock(); try { //二次尝试从缓存中读取 byte[] cacheByteValueSecond = redisCache.getByte(cacheKey); if (null != cacheByteValueSecond && cacheByteValueSecond.length > 0) { //反序列化对象 value = SerializableUtil.deserialize(cacheByteValueSecond); } //缓存中存在并且不为空-直接返回 if (null != value && !cacheKey.equals(value.toString())) { log.info("[ CacheAspect ] >> [ get from cache in second ] method:{} , cacheKey:{}", className + methodName, cacheKey); return value; } //如果设置了允许存储null值,若缓存key存在,并且value与自定义key相同 > 则直接返回 null if (cacheData.storageNullFlag() && redisCache.exists(cacheKey)) { log.info("[ CacheAspect ] >> [ get from cache in second value is null ] method:{} , cacheKey:{}", className + methodName, cacheKey); return null; } //执行方法-并获得返回值 value = pjpParam.proceed(pjpParam.getArgs()); //返回值不为空-存入缓存并返回 if (null != value) { //存入缓存 redisCache.putByte(cacheKey, SerializableUtil.serialize(value, cacheKey), this.getExpireTime(cacheData)); return value; } //返回值为空-是否设置需要存储空对象 if (cacheData.storageNullFlag()) { //存入缓存,空返回值时value也存储key redisCache.putByte(cacheKey, SerializableUtil.serialize(cacheKey), this.getExpireTime(cacheData)); return null; } return null; } finally { //解锁 lock.unlock(); log.info("[ CacheAspect ] >> un lock method:{} , cacheKey:{}", className + methodName, cacheKey); } } /** * 生成缓存key * * @param cacheData * @param className * @param methodName * @param pjpParam * @return */ private String getCacheKey(CacheData cacheData, String className, String methodName, ProceedingJoinPoint pjpParam) { //缓存key前缀 String keyPrefix = cacheData.keyPrefix(); if (EMPTY.equals(keyPrefix)) { keyPrefix = methodName; } //方法全路径(类名+方法名) String methodPath = className + POINT + methodName; //若方法参数为空 if (pjpParam.getArgs() == null || pjpParam.getArgs().length == 0) { return CACHE_KEY_PREFIX + keyPrefix + POINT + DigestUtils.md5Hex(methodPath); } //参数序号 int i = 0; //按照参数顺序,拼接方法参数 Map paramMap = new LinkedHashMap<>(pjpParam.getArgs().length); for (Object obj : pjpParam.getArgs()) { i++; if (obj != null) { paramMap.put(obj.getClass().getName() + i, obj); } else { paramMap.put("NULL" + i, "NULL"); } } String paramJson = JSON.toJSONString(paramMap); log.debug("[ CacheAspect ] >> param-JSON:{}", JSON.toJSONString(paramMap)); return CACHE_KEY_PREFIX + keyPrefix + POINT + DigestUtils.md5Hex(paramJson); } /** * 计算过期时间 如果缓存设置了需要延迟失效, * 取设置的延迟时间1-2倍之间的一个随机值作为真正的延迟时间值 */ private int getExpireTime(CacheData cacheData) { int expire = cacheData.expireTime(); //设置为0,则默认效期到当天的截止 if (expire == 0) { expire = (int) (60 * 60 * 24 - ((System.currentTimeMillis() / 1000 + 8 * 3600) % (60 * 60 * 24))); } int randomExpire = 0; //若设置>0 , 失效时间加上(0~设置)的值内的随机数 if (cacheData.randomExpire() > 0) { randomExpire = new Random().nextInt(cacheData.randomExpire()); } return expire + randomExpire; }}
源码工程
文章中没有贴出全部的代码,只贴出了核心实现,详细源码见如下,源码中包含使用实例
源码地址:
https://github.com/mengq0815/spring-boot-example/tree/master/springBoot-api-cache
如图源码工程介绍图
使用方式
直接在实现的方法上使用注解即可
注解属性详见: 上文 -> 代码实现 > 自定义注解
如果想让方法不走缓存可以使用-强制包裹内的方法不走缓存
//自定义设置-刷新缓存CacheContext.refreshCacheFlag.set(true);//业务方法//自定义设置-刷新缓存CacheContext.refreshCacheFlag.set(false);
实例测试
测试方法接口实现
/** * 用户基本处理Service * * @author 程序员小强 * @version 1.0 */@Servicepublic class UserCacheBaseServiceImpl implements UserCacheBaseService { private static final Logger log = LoggerFactory.getLogger(UserCacheBaseServiceImpl.class); /** * 测试所需的模拟数据 */ private static Map MOCK_USER_MAP = new HashMap<>(); static { MOCK_USER_MAP.put(1, new UserInfo(1, "zhangsan", "张三", 1)); MOCK_USER_MAP.put(2, new UserInfo(2, "lisi", "李四", 1)); MOCK_USER_MAP.put(3, new UserInfo(3, "wangwu", "王五", 2)); MOCK_USER_MAP.put(4, new UserInfo(4, "xiaoliu", "小六", 1)); MOCK_USER_MAP.put(5, new UserInfo(5, "xiaoqi", "小七", 2)); MOCK_USER_MAP.put(6, new UserInfo(6, "xiaoba", "小八", 0)); MOCK_USER_MAP.put(7, new UserInfo(7, "nihao", "你好", 3)); MOCK_USER_MAP.put(8, new UserInfo(8, "wohao", "我好", 1)); MOCK_USER_MAP.put(9, new UserInfo(9, "tahao", "他好", 2)); MOCK_USER_MAP.put(10, new UserInfo(10, "dajiahao", "大家好", 2)); MOCK_USER_MAP.put(11, new UserInfo(11, "womenhao", "我们好", 1)); MOCK_USER_MAP.put(12, new UserInfo(12, "feichanghao", "非常好", 3)); MOCK_USER_MAP.put(13, new UserInfo(13, "xiaoxiao", "小小", 2)); MOCK_USER_MAP.put(14, new UserInfo(14, "dada", "大大", 0)); MOCK_USER_MAP.put(15, new UserInfo(15, "haha", "哈哈", 1)); } /** * 无参数方法测试 * * @return */ @Override @CacheData(keyPrefix = "userListAll") public ListlistAll() { log.info("UserCacheBaseServiceImpl >> listAll"); //睡眠-模拟查询时间 this.sleep(500); return new ArrayList<>(MOCK_USER_MAP.values()); } /** * 根据用户ID查询 * * @param id */ @Override @CacheData(expireTime = 20 * 60) public UserInfo getUserById(Integer id) { log.info("UserCacheBaseServiceImpl >> getUserById id:{}", id); if (null == id) { return null; } //睡眠-模拟查询时间 this.sleep(1000); return MOCK_USER_MAP.get(id); } /** * 根据用户ID和状态查询 * * @param id * @param status * @return */ @Override @CacheData(expireTime = 25 * 60, storageNullFlag = false) public UserInfo getUserByIdAndStatus(Integer id, Integer status) { log.info("UserCacheBaseServiceImpl >> getUserByIdAndStatus id:{},status:{}", id, status); if (null == id) { return null; } //睡眠-模拟查询时间 this.sleep(1000); UserInfo userInfo = MOCK_USER_MAP.get(id); if (userInfo.getStatus().equals(status)) { return userInfo; } return null; } /** * 根据用户ID查询-可存储null值 * * @param id * @return */ @Override @CacheData public UserInfo getUserByIdStorageNull(Integer id) { return this.getUserById(id); } /** * 普通参数测试-分页查询用户 * * @param page * @param pageSize * @return */ @Override @CacheData(expireTime = 20 * 60) public ListlistPageUser(Integer page, Integer pageSize) { List userInfoList = new ArrayList<>(MOCK_USER_MAP.values()); //计算偏移 int start = pageSize * (page - 1); int end = start + pageSize; if (end > userInfoList.size()) { end = userInfoList.size(); } log.info("UserCacheBaseServiceImpl >> listPageUser page:{},pageSize:{},start:{},end:{}", page, +pageSize, start, +end); //睡眠-模拟查询时间 this.sleep(2000); List subUserList = userInfoList.subList(start, end); return new ArrayList<>(subUserList); } /** * 对象参数测试-分页查询用户 * * @param queryBO * @return */ @Override @CacheData(expireTime = 20 * 60) public ListlistPageUserByObjParam(UserListQueryBO queryBO) { int page = queryBO.getPage(); int pageSize = queryBO.getPageSize(); //模拟程序查询 List userInfoList = new ArrayList<>(MOCK_USER_MAP.size()); UserInfo mapValue = null; boolean canReturnFlag = true; boolean userNameNotNullFlag = (null != queryBO.getUserName() && !"".equals(queryBO.getUserName())); boolean userStatusNotNullFlag = (null != queryBO.getStatus()); for (Map.Entry map : MOCK_USER_MAP.entrySet()) { mapValue = map.getValue(); //检索条件都不为空 if (userNameNotNullFlag && userStatusNotNullFlag) { canReturnFlag = mapValue.getUserName().contains(queryBO.getUserName()) && mapValue.getStatus().equals(queryBO.getStatus()); } else if (userNameNotNullFlag) { //用户名称检索 canReturnFlag = mapValue.getUserName().contains(queryBO.getUserName()); } else if (userStatusNotNullFlag) { //用户状态检索 canReturnFlag = mapValue.getStatus().equals(queryBO.getStatus()); } if (!canReturnFlag) { continue; } userInfoList.add(mapValue); } //计算偏移 int start = pageSize * (page - 1); int end = start + pageSize; if (end > userInfoList.size()) { end = userInfoList.size(); } if (start > end) { start = end; } log.info("UserCacheBaseServiceImpl >> listPageUserByCondition page:" + page + " pageSize:" + pageSize + " start:" + start + " end:" + end); //睡眠-模拟查询时间 this.sleep(2000); List subUserList = userInfoList.subList(start, end); return new ArrayList<>(subUserList); } /** * 普通参数+对象参数测试-分页查询用户 * * @param page * @param pageSize * @param queryBO * @return */ @Override @CacheData(expireTime = 20 * 50) public ListlistPageUserByObjAndParam(Integer page, Integer pageSize, UserListQueryBO2 queryBO) { UserListQueryBO listQueryBo = new UserListQueryBO(); listQueryBo.setPage(page); listQueryBo.setPageSize(pageSize); listQueryBo.setUserName(queryBO.getUserName()); listQueryBo.setStatus(queryBO.getStatus()); return this.listPageUserByObjParam(listQueryBo); } private void sleep(long millis) { try { Thread.sleep(millis); } catch (Exception e) { log.info("UserCacheBaseServiceImpl >> sleep exception:", e); } }}
测试Controller
/** * 测试 controller * * @author 程序员小强 */@RestControllerpublic class CacheTestController { @Autowired private UserCacheBaseService userCacheBaseService; /** * 无参数方法测试-查询全部用户 */ @ResponseBody @RequestMapping("/listAll") public Response listAll() { return Response.success(userCacheBaseService.listAll()); } /** * 根据用户ID查询 * * @param id */ @ResponseBody @RequestMapping("/getUserById") public Response getUserById(Integer id) { return Response.success(userCacheBaseService.getUserById(id)); } /** * 根据用户ID查询 * * @param id */ @ResponseBody @RequestMapping("/getUserByIdAndStatus") public Response getUserByIdAndStatus(@RequestParam("id") Integer id, @RequestParam("status") Integer status) { return Response.success(userCacheBaseService.getUserByIdAndStatus(id, status)); } /** * 根据用户ID查询-可存储null值 * * @param id */ @ResponseBody @RequestMapping("/getUserByIdStorageNull") public Response getUserByIdStorageNull(Integer id) { return Response.success(userCacheBaseService.getUserByIdStorageNull(id)); } /** * 普通参数测试-分页查询用户 * * @param page * @param pageSize */ @ResponseBody @RequestMapping("/listPageUser") public Response listPageUser(Integer page, Integer pageSize) { return Response.success(userCacheBaseService.listPageUser(page, pageSize)); } /** * 对象参数测试-分页查询用户 * * @param queryBO */ @ResponseBody @RequestMapping("/listPageUserByObjParam") public Response listPageUserByObjParam(UserListQueryBO queryBO) { return Response.success(userCacheBaseService.listPageUserByObjParam(queryBO)); } /** * 普通参数+对象参数测试-分页查询用户 * * @param page * @param pageSize * @param queryBO */ @ResponseBody @RequestMapping("/listPageUserByObjAndParam") public Response listPageUserByObjAndParam(Integer page, Integer pageSize, UserListQueryBO2 queryBO) { return Response.success(userCacheBaseService.listPageUserByObjAndParam(page, pageSize, queryBO)); } /** * 不使用缓存直接执行方法 * * @param id */ @ResponseBody @RequestMapping("/getUserById-notReadCacheFlag") public Response getUserByIdNotReadCache(Integer id) { //自定义设置不走缓存 CacheContext.notReadCacheFlag.set(true); UserInfo userInfo = userCacheBaseService.getUserById(id); CacheContext.notReadCacheFlag.set(false); return Response.success(userInfo); } /** * 不使用缓存-查询并更新缓存 * * @param id */ @ResponseBody @RequestMapping("/getUserById-refreshCache") public Response getUserByIdRefreshCache(Integer id) { //自定义设置-刷新缓存 CacheContext.refreshCacheFlag.set(true); UserInfo userInfo = userCacheBaseService.getUserById(id); CacheContext.refreshCacheFlag.set(false); return Response.success(userInfo); }}
1.返回人员列表为例
工程启动后访问localhost:8081/cache-user/listAll
第一次访问由于没有缓存,方法内部加了休眠,所以比较慢,后续访问基本上都是30毫秒左右因为缓存已经存在了
2.获取人员详情
localhost:8081/cache-user/getUserById?id=1
3.分页查询人员
localhost:8081/cache-user/listPageUserByObjParam?page=1&pageSize=10
注意:缓存是根据参数生成的Key,如果参数变化,比如要查询第二页,则第一次查询是没有缓存的
总结
此方案比较适合比如xx详情等接口的缓存,分页查询这种会存在按照参数缓存,参数不一致则缓存无法命中;
分页缓存推荐使用redis的List、set有序集合等组合存储。
万物有痕,小弟也不是大佬,欢迎大家指点,共同进步
其它Redis篇
基于Redis分布式限流组件设计与实例
Redis 分布式锁的java版正确实现
Redis 数据类型以及适用场景
Redis 持久化方式
Redis在Linux下的安装
Redis4.0高可用集群模式搭建