缓存雪崩:一旦缓存没有数据了,所有请求打到数据库,数据库承受不住,然后这个应用就无法使用。
解决方案:
1、redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
2、服务限流,降级,避免MySQL被打死
3、redis持久化,快速恢复缓存数据
缓存穿透:再查询一个不存在的数据的时候,先查询缓存,缓存为null,再查询数据库,数据也为null,之后,每一次都如此,每一个都把压力落到了数据库层。
解决办法:
1、ip或id限流加黑名单。防止恶意攻击。
2、设置默认值,在查询到数据库为null的时候,在缓存中保存一个默认值。
缺点:1、对于insert操作,需要去过期这些缓存,增加了缓存的复杂性。2、浪费内存。3、如果别人操控很多台机器在限流范围内循环请求不存在数据,比如:id从-1到-9999。没有办法,但是代价太大了,也很难做到。
改善方法:设置缓存过期时间,设置短一点,redis的缓存策略改成lru(volatile或allkey lru),内存满了之后,去掉最近最少使用的缓存。或者,缓存穿透的key单独存放,不影响正常数据的缓存。
贴代码:我们系统使用的是ace-cache缓存,但是这个对缓存相关的问题都没有进行处理,所以可以做以下修改。
public static final String nullObject = "null-data-uuid";
@Pointcut("@annotation(com.ace.cache.annotation.Cache)")
public void aspect() {
}
@Around("aspect()&&@annotation(anno)")
public Object interceptor(ProceedingJoinPoint invocation, Cache anno)
throws Throwable {
MethodSignature signature = (MethodSignature) invocation.getSignature();
Method method = signature.getMethod();
Object result = null;
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] arguments = invocation.getArgs();
String key = "";
String value = "";
try {
key = getKey(anno, parameterTypes, arguments);
value = cacheAPI.get(key);
if(nullObject.equals(value)){
// result不为null,则finally块就不会去查数据库
result = value;
return null;
}
Type returnType = method.getGenericReturnType();
result = getResult(anno, result, value, returnType);
} catch (Exception e) {
log.error("获取缓存失败:" + key, e);
} finally {
if (result == null) {
result = invocation.proceed();
// 防止缓存穿透
if(result == null){
if (StringUtils.isNotBlank(key)) {
cacheAPI.set(key, nullObject, 3600);
}
}
if (StringUtils.isNotBlank(key)) {
cacheAPI.set(key, result, anno.expire());
}
}
}
return result;
}
对于写for循环的缓存穿透攻击,限制ip,每秒超过200次,则限制ip访问,对于多个ip的同事缓存穿透攻击,模仿正常请求太像了,无法限制,只能加机器,加大数据库的读性能。
代码:redis限流
缓存更新顺序的问题:先更新数据库,再移除缓存,一旦缓存移除失败,则回滚数据库。先后顺序不能反过来,一旦先移除缓存,再更新数据库,那么在移除缓存后、还没来得及更新数据库之前,又来了一个线程,重新 从数据库中读取数据,写入缓存中,则会导致缓存的是脏数据。
这里,有一个问题:如果存在数据库读写分离,再加上缓存机制,那么单纯这样的操作还是会导致缓存不一致的问题:DB主从同步延迟导致的缓存不一致
@Around("aspect()&&@annotation(anno)")
public Object interceptor(ProceedingJoinPoint invocation, CacheClear anno)
throws Throwable {
// 先更新数据库,再更新缓存。更新缓存失败,则回滚数据库。
Object proceed = invocation.proceed();
try {
MethodSignature signature = (MethodSignature) invocation.getSignature();
Method method = signature.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] arguments = invocation.getArgs();
String key = "";
if (StringUtils.isNotBlank(anno.key())) {
key = getKey(anno, anno.key(), CacheScope.application,
parameterTypes, arguments);
cacheAPI.remove(key);
} else if (StringUtils.isNotBlank(anno.pre())) {
key = getKey(anno, anno.pre(), CacheScope.application,
parameterTypes, arguments);
cacheAPI.removeByPre(key);
} else if (anno.keys().length > 1) {
for (String tmp : anno.keys()) {
tmp = getKey(anno, tmp, CacheScope.application, parameterTypes,
arguments);
cacheAPI.removeByPre(tmp);
}
}
} catch (Exception e){
log.error(e);
throw new RuntimeException("清楚缓存失败");
}
return proceed;
}
缓存击穿:在更新数据并且缓存过期了之后,在一瞬间同事发生多个读数据的请求,这一瞬间缓存中没有数据,压力都落到了数据库。
解决办法:使用分布式锁锁住。分布式锁
String value = redis.get(key);
if (value == null) {
// redission是redis工具类,比较完善的处理好了redis分布式锁的各种问题
redisson.getLock(lockKey).lock(30, TimeUnit.SECONDS)
if (value == null) {
value = db.get(key);
redis.set(key, value, expire_secs);
redisson.getLock(lockKey).unlock()
}
}
或者粗糙一点
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
网友评论