简介
随着系统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 mengqiang
* @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 mengq
*/
@Aspect
@Component
public 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<String, Object> 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;
}
}
源码工程
文章中没有贴出全部的代码,只贴出了核心实现,详细源码见如下,源码中还包含使用实例
源码传送门:传送门
使用方式
直接在实现的方法上使用注解即可
注解属性详见: 上文 -> 代码实现 > 自定义注解
如果调用方法的时候想让不走缓存可以使用-强制包裹内的方法不走缓存
//自定义设置-刷新缓存
CacheContext.refreshCacheFlag.set(true);
//业务方法
//自定义设置-刷新缓存
CacheContext.refreshCacheFlag.set(false);
实例测试
测试方法接口实现
/**
* 用户基本处理Service
*
* @author mengq
* @version 1.0
*/
@Service
public class UserCacheBaseServiceImpl implements UserCacheBaseService {
private static final Logger log = LoggerFactory.getLogger(UserCacheBaseServiceImpl.class);
/**
* 测试所需的模拟数据
*/
private static Map<Integer, UserInfo> 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 List<UserInfo> listAll() {
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 List<UserInfo> listPageUser(Integer page, Integer pageSize) {
List<UserInfo> 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<UserInfo> subUserList = userInfoList.subList(start, end);
return new ArrayList<>(subUserList);
}
/**
* 对象参数测试-分页查询用户
*
* @param queryBO
* @return
*/
@Override
@CacheData(expireTime = 20 * 60)
public List<UserInfo> listPageUserByObjParam(UserListQueryBO queryBO) {
int page = queryBO.getPage();
int pageSize = queryBO.getPageSize();
//模拟程序查询
List<UserInfo> 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<Integer, UserInfo> 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<UserInfo> subUserList = userInfoList.subList(start, end);
return new ArrayList<>(subUserList);
}
/**
* 普通参数+对象参数测试-分页查询用户
*
* @param page
* @param pageSize
* @param queryBO
* @return
*/
@Override
@CacheData(expireTime = 20 * 50)
public List<UserInfo> listPageUserByObjAndParam(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 mengq
*/
@RestController
public 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有序集合等组合存储。
万物有痕,可能存在需要优化的点,小强也不是大佬,小强还在学习中,欢迎大家指点,共同进步
关注程序员小强公众号更多编程趣事,知识心得与您分享