参考文章
设计思路
没有缓存的时候,获取数据是直接查询数据库的。如果某时间段内并发查询的次数太多,会对数据库造成很大的压力,甚至崩盘。这时我们就会想到使用缓存。查询数据时先查询缓存,如果缓存有则直接还回,如果没有则查询数据库,把结果放入缓存中并返回。下次查询时可以直接从缓存中获取。
现有一商品表 goods ,对goods的查询做redis缓存优化。goods的key,value如下。
key = 固定值+version(可变)+参数 = "cache:goods:" + +version(可变)+参数
value = 从数据库查询的结果。
其中version也是 redis缓存中的值
key = 国定值="cache:goods:version"
value = 1 //初始值1,goods每次增删改,value都加1
思路:
-
创建2个注解:CacheVersion ,CyCache
@interface CacheVersion {versionKey,expireTime=3600}:缓存版本 @interface CyCache {cacheKey,versionKey,expireTime=3600} :缓存
-
创建2个aspect:CacheVersionAspect,CacheAspect
class CacheVersionAspect:拦截标记了注解CacheVersion 的增删改方法。 在方法执行,CacheVersion.versionKey对应的缓存加1 class CacheAspect :拦截标记了注解CyCache的查询方法。 在执行数据库查询之前先查一下redis缓存(CyCache.cacheKey+version+参数)是否有数据, 如果有则直接返回,没有则执行查询方法,得到结果后把结果放入redis中并返回。
-
给service的增删改方法添加注解@CacheVersion ,给查询方法添加注册@CyCache
通过@CyCache,查询不用每次查询数据库,可以从redis缓存中获取数据。 通过@CacheVersion,每次增删改后version的值会递增。下次查询时, (CyCache.key+version+参数)的缓存就没有(因为version的值变了),需要重新查询数据库并缓存。
实现
创建2个注解:CacheVersion ,CyCache
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 缓存版本注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheVersion {
/**
* 版本key
*/
String[] versionKeys() default {};
/**
* 过期时间,单位秒
*/
int expireTime() default 3600;
}
package com.example.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 缓存注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
/**
* 版本key 需要和 CacheVersion。versionKey 一样
*/
String versionKey() default "";
/**
* 数据key的固定值部分
*/
String cacheKey() default "";
/**
* 方法的参数集合
*/
String[] paramName() default {};
/**
* 过期时间,单位秒
*/
int expireTime() default 3600;
}
创建2个aspect:CacheVersionAspect,CacheAspect
package com.example.demo.aspect;
import com.example.demo.annotation.CacheVersion;
import com.example.demo.cache.CacheClient;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 缓存版本处理切面
*/
@Slf4j
@Component
@Aspect
public class CacheVersionAspect {
@Resource
private CacheClient cacheClient;
@After(value = "@annotation(cacheVersion))")
public void handlerServiceCacheVersion(JoinPoint point, CacheVersion cacheVersion) {
try {
String[] versionKeys = cacheVersion.versionKeys();
for (String versionKey : versionKeys) {
cacheClient.increaseCount(versionKey);
cacheClient.expire(versionKey, cacheVersion.expireTime());
log.info("update version of : {}", versionKey);
}
} catch (Exception e) {
log.info("缓存异常:", e);
}
}
}
package com.example.demo.aspect;
import com.example.demo.annotation.Cache;
import com.example.demo.cache.CacheClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
@Aspect
public class CacheAspect {
@Resource
private CacheClient cacheClient;
public CacheAspect() {
}
@Around(value = "@annotation(cache))")
public Object handlerServiceCache(ProceedingJoinPoint point, Cache cache) {
try {
//获取参数拼凑的key
String paramKey = getParamKey(point, cache.paramName());
//获取version的key
String versionKey = getCacheVersion(cache);
//dataKey= cache.cacheKey + 参数拼凑的key +version的key
String dataKey = cache.cacheKey() + paramKey + versionKey;
// 处理缓存
Object data = cacheClient.get(dataKey);
if (data != null) {
System.out.println("从缓存中获取数据,key = " + dataKey);
return data;
}
data = point.proceed();
if (data != null) {
cacheClient.set(dataKey, data);
cacheClient.expire(dataKey, cache.expireTime());
}
return data;
} catch (Throwable e) {
log.error("缓存异常:", e);
}
return null;
}
/**
* 获取version的key
*/
private String getCacheVersion(Cache cache) {
String versionKey = cache.versionKey();
if (StringUtils.isBlank(versionKey)) {
return "";
}
Integer cacheVersion;
if (cacheClient.get(versionKey) == null) {
cacheVersion = cacheClient.increaseCount(versionKey);
} else {
cacheVersion = (Integer) cacheClient.get(versionKey);
}
cacheClient.expire(versionKey, cache.expireTime());
return ":" + cacheVersion.toString();
}
/**
* 获取参数拼凑的key
*/
private String getParamKey(ProceedingJoinPoint point, String[] params) {
if (params == null || params.length == 0) {
return "";
}
StringBuilder keyExtra = new StringBuilder();
try {
Map<String, Object> nameAndArgs = this.getFieldsName(point, point.getArgs());
for (String param : params) {
if (nameAndArgs.get(param) != null) {
keyExtra.append(":").append(nameAndArgs.get(param));
}
}
} catch (Exception e) {
log.info("缓存异常:", e);
}
return keyExtra.toString();
}
/**
* 基于Aspect,java1.8以上用java自带的参数反射,以下用spring的实现,似乎跟javassist一样
*/
private Map<String, Object> getFieldsName(ProceedingJoinPoint point, Object[] args) {
Map<String, Object> map = new HashMap<>();
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] names = methodSignature.getParameterNames();
for (int i = 0; i < args.length; i++) {
map.put(names[i], args[i]);
}
return map;
}
}
给service的增删改方法添加注解@CacheVersion ,给查询方法添加注册@CyCache
package com.example.demo.service;
import com.example.demo.annotation.Cache;
import com.example.demo.annotation.CacheVersion;
import com.example.demo.demain.Good;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class GoodService {
private Integer goodNum = 2;
private String description = "好商品";
@Cache(cacheKey = "cache:good", versionKey = "cache:good:version", paramName = {"name"})
public List<Good> getGoodList(String name) {
System.out.println("从数据库中获取数据");
List<Good> goods = new ArrayList<>();
for (int i = 1; i <= goodNum; i++) {
Good good = Good.builder().id(i).name(i + "号商品").price(2000L).description(i + "号商品 " + description).build();
goods.add(good);
}
return goods;
}
@CacheVersion(versionKeys = {"cache:good:version"})
public void addGood() {
System.out.println("增长商品");
goodNum++;
}
@CacheVersion(versionKeys = {"cache:good:version"})
public void deleteGood() {
System.out.println("减少商品");
goodNum--;
}
@CacheVersion(versionKeys = {"cache:good:version"})
public void updateGood(Integer id, String description) {
System.out.println("修改商品描述 description = " + description);
this.description = description;
}
}
测试
package com.example.demo.service;
import com.alibaba.fastjson.JSON;
import com.example.demo.demain.Good;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
@SpringBootTest
class GoodServiceTest {
@Resource
private GoodService goodService;
@Test
void getGoodList() {
System.out.println("第1次查询:");
List<Good> goods = goodService.getGoodList("你好");
System.out.println(JSON.toJSONString(goods));
System.out.println("第2次查询:");
goods = goodService.getGoodList("你好");
System.out.println(JSON.toJSONString(goods));
System.out.println("########## 修改商品 ##########");
goodService.updateGood(2,"非常好的商品");
System.out.println("########## 修改商品 ##########");
System.out.println("修改后第1次查询:");
goods = goodService.getGoodList("你好");
System.out.println(JSON.toJSONString(goods));
System.out.println("修改后第2次查询:");
goods = goodService.getGoodList("你好");
System.out.println(JSON.toJSONString(goods));
}
}
运行结果
问题
1、缓存雪崩
缓存失效,请求全部到数据库中,很可能就把我们的数据库搞垮,导致整个服务瘫痪!缓存失效可能如下原因:
1,Redis缓存数据时,需设置过期时间,如果大量过期时间相同,极端的情况下,可能会出现这些缓存同时失效,全部请求到数据库中。
2,Redis挂掉了,全部请求到数据库中。
,
如何解决缓存雪崩
第一种情况
在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。
第二种情况可分为 三步处理
事发前:
- redis持久化。
- 实现Redis的高可用(主从架构+Sentinel 或者Redis Cluster),避免Redis全部挂掉。
- 设置备份缓存,如本地缓存(ehcache),当redis缓存雪崩后,启用备份缓存。
事发中:
- 用 hystrix 对源服务访问进行 限流、降级。降级在高并发系统中是非常正常的:比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据。可参考文章: Hystrix学习总结
- 启用备份缓存
事发后:
- 立即重启。redis持久化后,redis重启后自动从磁盘上加载数据,快速恢复缓存数据。
2、缓存穿透
缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存
这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。这就是缓存穿透:请求的数据在缓存大量不命中,导致请求走数据库。缓存穿透如果发生了,也可能把我们的数据库搞垮,导致整个服务瘫痪!
如何解决缓存雪崩
- 由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层!
- 当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置一个较短的过期时间。
3、缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
如何解决缓存雪崩
- 设置热点数据永远不过期。
- 加互斥锁,互斥锁参考代码如下:
说明:
1)缓存中有数据,直接走上述代码13行后就返回结果了
2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。
3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点。