提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
很多人都应该碰到过这种需求,需要优先从缓存中读取数据如果缓存中没有则从数据库中获取,大部分人的想法肯定是用aop对方法切面使用方法名称+参数作为key,但是这种方法有很大的弊端
,本文会分析弊端并且提供解决方案。
一、旧的aop实现方式
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cached {
String value() default ""; // 默认为空,表示使用方法名作为缓存的key
// 过期时间,单位为秒
long expireSeconds() default -1; // 默认为-1,表示不过期
String keyPrefix() default "";// key的前缀
}
@Aspect
@Component
public class CachingAspect {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.test.user.annotation.Cached) || @within(com.test.user.annotation.Cached)")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object cacheMethod(ProceedingJoinPoint joinPoint) throws Throwable {
Cached cached = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(Cached.class);
String cacheKey = getCacheKey(joinPoint,cached);
Object cachedValue = redisTemplate.opsForValue().get(cacheKey);
if (cachedValue != null) {
if(cachedValue.equals("null")){
return null;
}
return cachedValue;
}
Object result = joinPoint.proceed();
if (result != null) {
redisTemplate.opsForValue().set(cacheKey, result);
if (cached.expireSeconds() > 0) {
redisTemplate.expire(cacheKey, cached.expireSeconds(), java.util.concurrent.TimeUnit.SECONDS);
}
}else{
// 防止空值缓存穿透
redisTemplate.opsForValue().set(cacheKey, "null");
if (cached.expireSeconds() > 0) {
redisTemplate.expire(cacheKey, cached.expireSeconds(), java.util.concurrent.TimeUnit.SECONDS);
}
}
return result;
}
private String getCacheKey(ProceedingJoinPoint joinPoint, Cached cached) {
String name = joinPoint.getTarget()
.getClass()
.getName();
Object[] args = joinPoint.getArgs();
StringBuilder keyBuilder = new StringBuilder(name);
for (Object arg : args) {
keyBuilder.append(":").append(arg == null ? "" : arg.toString());
}
return cached.keyPrefix() + keyBuilder.toString();
}
}
代码中使用
@Cached(keyPrefix = RedisKeyConstant.LOCATION_KEY_PREFIX, expireSeconds = 20)
@Override
public MemberCompanyCoordinateVO getCompanyCoordinateCached(Long userId) {
return this.getBaseMapper().getCompanyCoordinate(userId);
}
弊端
我们分析代码可以发现在使用时有以下弊端
- 缓存过期时间不能动态指定,使用aop注解做缓存时,缓存过期时间只能手动写死在注解上,无法从配置中读取,
如果修改失效时间需要重启服务
,当然我们也可以在使用注解时不指定失效时间,统一在CachingAspect
里面做处理,但是这样CachingAspect 就会变得比较复杂不建议这样处理。
@Cached(keyPrefix = RedisKeyConstant.LOCATION_KEY_PREFIX, expireSeconds = 20)
- 失效缓存不方便,因为缓存的key是通过rediskey的前缀+方法名称+参数(多个)生成的,所以在失效时极为不方法,存入的时候是在aop中存入,失效的时候却在程序中的另一个地方,不方便集中关联。
所以我们需要换另外一种实现方式
二、新的实现方式
@Slf4j
@Component
public class RedisCacheUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取缓存,如果缓存不存在则调用supplier获取数据并缓存
* @param cacheKey
* @param expireSeconds
* @param timeUnit
* @param supplier
* @return
* @param <T>
*/
public <T> T getOrSaveWithExpiration (String cacheKey, Long expireSeconds, TimeUnit timeUnit, Supplier<T> supplier){
Object cachedValue = redisTemplate.opsForValue().get(cacheKey);
// 优先取缓存
if (cachedValue != null) {
if(cachedValue.equals("null")){
return null;
}
try {
return (T) cachedValue;
}catch (Exception e){
log.error("缓存数据类型错误",e);
throw new BusinessException("缓存数据类型错误");
}
}
T t = supplier.get();
if (t != null) {
redisTemplate.opsForValue().set(cacheKey, t, expireSeconds, timeUnit);
}else{
// 防止空值缓存穿透
redisTemplate.opsForValue().set(cacheKey, "null",expireSeconds, timeUnit);
}
return t;
}
public void delete(String cacheKey){
redisTemplate.delete(cacheKey);
}
}
redisCacheUtil.getOrSaveWithExpiration 方法会根据传入的key优先从缓存中读取,如果缓存中没有则会调用传入的获取数据库中数据的方法从数据库,并且写入到缓存中设置好失效时间。
在代码中使用
// 优先从数据库中读取门店数据
@Override
public MemberCompanyCoordinateVO getRedisCompanyData(Long companyId) {
Long expireSeconds = 86400L; // 可以改成从配置文件中读取
return redisCacheUtil.getOrSaveWithExpiration(buildKey(companyId),
expireSeconds,
java.util.concurrent.TimeUnit.SECONDS,
() -> this.getBaseMapper().getCompany(userId));
// this.getBaseMapper().getCompany(userId)这里是查库 返回MemberCompanyCoordinateVO 对象
}
public String buildKey(Long companyId){
return RedisKeyConstant.LOGIN_COMPANY_DATA + companyId;
}
// 失效缓存
@Override
public void removeRedisCompanyData(Long companyId) {
redisCacheUtil.delete(buildKey(companyId));
}
如果有别的库中的数据也需要维护缓存只需要替换以下参数就能自动实现缓存维护,下面是具体实现。
@Override
public User getRedisUserData(Long userId) {
Long expireSeconds = 86400L; // 可以改成从配置文件中读取
String key = "user:" + userId;
return redisCacheUtil.getOrSaveWithExpiration(key ,
expireSeconds,
java.util.concurrent.TimeUnit.SECONDS,
() -> this.getBaseMapper().getUser(userId));
// this.getBaseMapper().getUser(userId)这里是查库 返回User 对象
}
新的实现方式存入时就指定好了失效时间,失效时间可以从配置中读取,后续修改时间失效无需重启服务,而且存入的方法可以和失效的方法写在同一个类中更好维护。