项目利用使用到了redis,比如会出现穿透、击穿、雪崩等问题。
穿透
缓存种不存在,数据库种也不存在,导致每一次的请求都会到数据库层面。
解决方案:缓存空对象,或者使用布隆过滤器
击穿
某个key在有大量的请求,但是大量请求到的时候,过期了,然后导致大量请求都到数据库层面
解决方案:数据不过期或者使用分布式锁,防止所有的请求都到数据库
雪崩
缓存种的key大面积失效,导致所有请求都到了数据库。
解决方案:在原有的缓存时间上,追加随机时间。避免同时失效
实现分布式锁,并解决上述问题
第一种方法:模板模式
因为项目种使用redis缓存的地方比较多,如果每个地方都写一份代码,比较冗余。
所以,采用了模板模式,将加锁、解锁操作都封装到抽象类种,使调用者只需要关系数据的处理即可;
RedisLock类,构建加锁解锁骨架
public abstract class RedisLock<T> {
private static final String TAG = "RedisLock";
private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
private RedisTemplate redisTemplate;
private T data;
public RedisLock(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 枷锁
*
* @param key
* @param timeUnit
* @param timeout
*/
public void lock(String key, TimeUnit timeUnit, long timeout) {
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, timeUnit);
while (isLock == null || !isLock) {
logger.warn("没有获取到锁");
isLock =redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, timeUnit);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
//出现异常释放锁
// redisTemplate.delete(key);
}
}
try {
if (isLock) {
logger.warn("获取到锁");
//再次请求获取缓存,如果还是为空,则查询数据库。
//交给调用者处理
data = handler();
}
} catch (Exception e) {
throw e;
} finally {
//解锁
redisTemplate.delete(key);
logger.warn("释放了锁");
}
}
public T getData() {
return data;
}
public abstract T handler();
}
在需要使用的地方初始化RedisLock
@Override
public ExchangePostsVO getOnePosts(Integer postId) {
String postKey = RedisKeyUtil.getPostOneKey(postId);
ExchangePosts record = (ExchangePosts) redisTemplate.opsForValue().get(postKey);
String lockKey = "lock:post:" + postId;
if (record == null) {
RedisLock<ExchangePosts> redisLock = new RedisLock(redisTemplate) {
@Override
public ExchangePosts handler() {
//获取数据逻辑
ExchangePosts postObj = (ExchangePosts) redisTemplate.opsForValue().get(postKey);
if (postObj != null) {
return postObj;
}
postObj = getById(postId);
if (postObj == null) {
postObj = new ExchangePosts();
//创建空对象,防止缓存穿透
}
redisTemplate.opsForValue().set(postKey, postObj, 3+postObj.hashCode()%7, TimeUnit.HOURS);
return postObj;
}
};
redisLock.lock(lockKey, TimeUnit.MILLISECONDS, 100);
record = redisLock.getData();
}
log.warn("存在数据");
if (record.getId() == null) {
throw new CustomException("记录不存在");
}
ExchangePostsVO vo = getExchangePostsVO(record);
return vo;
}
第二种方法:AOP
利用切面编程 AOP来实现代码的复用,自定义一个注解,在切面拦截此注解,从切面处获取自定义key的前缀并追加入参,构造成key。
从缓存种获取到信息,判断信息是否为空,如果空,则创建分布式锁。如果拿到了锁,则从缓存种获取信息,如果有,则返回,没有则去数据库种获取信息,如果获取到的信息为null,则把空对象放入缓存,如果不为空,则把对象放入缓存。
如果没有拿到锁,则休眠100毫米后再次尝试是否拿到了锁。如果拿到了,则从缓存种获取,没有则从数据库种获取。
自定义注解
/**
* 自定义注解-实现Redis的数据缓存,并通过切面解决穿透、击穿、雪崩问题
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cache {
/**
* 设置缓存的key的前缀
* @return
*/
String prefix() default "cache:";
}
切面
package com.tute.edu.planetcommunity.lock;
import com.tute.edu.planetcommunity.annotation.Cache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* 处理redis缓存。解决穿透-切面
*/
@Aspect
@Component
public class CacheAop {
@Autowired
private RedisTemplate redisTemplate;
private String preLockKey = "lock:";
/**
* 环绕通知
*
* @param point
*/
@Around("@annotation(com.tute.edu.planetcommunity.annotation.Cache)")
public Object redisCache(ProceedingJoinPoint point) {
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Class returnType = methodSignature.getReturnType();
Cache annotation = methodSignature.getMethod().getAnnotation(Cache.class);
//获取自定义的key前缀
String prefix = annotation.prefix();
//拼接我们的key
String key = prefix + Arrays.asList(point.getArgs()).toString();
Object obj = redisTemplate.opsForValue().get(key);
if (obj == null) {
//获取分布式锁
String lockKey = preLockKey + key;
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
while (isLock == null || !isLock) {
try {
TimeUnit.MILLISECONDS.sleep(100);
isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
obj = redisTemplate.opsForValue().get(key);
if (obj != null) {
return obj;
}
//请求数据库,查询信息
obj = point.proceed(point.getArgs());
if (obj == null) {
obj = returnType.newInstance();
}
redisTemplate.opsForValue().set(key, obj, 3 + obj.hashCode() % 7, TimeUnit.HOURS);
return obj;
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
redisTemplate.delete(lockKey);
}
} else {
return obj;
}
try {
return point.proceed(point.getArgs());
} catch (Throwable throwable) {
throwable.printStackTrace();
return new Object();
}
}
}
使用
@Override
@Cache(prefix = "post:")
public ExchangePosts getById(Serializable id) {
return super.getById(id);
}