一、背景
当我们想要某个接口访问时,优先访问redis缓存时,可以使用@Cacheable注解实现。但是在处理redis的三大问题的时候,使用自定义的注解可控性更强。根据前面redis的已知的三大问题以及它的解决方案,扩充了注解的功能。
二、实现
1.定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCheck {
String key() default "";//key值
int delTime() default 180;//key值删除时间
boolean isUseNull() default true;
String bloomFilterKey() default "";//使用的布隆过滤器的key值
boolean isUseLock() default true;//是否使用锁
}
2.使用代理,监听注解,同时处理三大问题。
@Aspect
@Component
public class RedisCheckAop {
@Autowired
private RedisCacheUtil redisCacheUtil;
@Autowired
private BloomFilterUtil bloomFilterUtil;
@Autowired
private LockUtil lockUtil;
@Around("@annotation(com.dmsl.annotation.RedisCheck)")
public Object RedisCheck(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解
System.out.println("----------------");
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
RedisCheck annotation = method.getAnnotation(RedisCheck.class);
String key = annotation.key();
int delTime = annotation.delTime();//设置为-1就是用不删除key 解决缓存击穿的方法二
boolean isUseNull = annotation.isUseNull();
String bloomFilterKey = annotation.bloomFilterKey();
boolean isUseLock = annotation.isUseLock();
Object[] args = joinPoint.getArgs();
String redisKey = KeyUtil.GetKey(key,args);
if(redisCacheUtil.CheckHaveCacheData(redisKey))//key存在
{
System.out.println("key存在="+redisKey);
Object data = redisCacheUtil.GetCacheData(redisKey);
if(data!=null)//不为空直接返回
return data;
if(isUseNull)//即使为空也返回 解决缓存穿透的方法一
{
System.out.println("即使为空也返回");
return data;
}
}
else//key不存在
{
if(!bloomFilterKey.isEmpty())//使用布隆过滤器 解决缓存穿透的方法二
{
BloomFilter bloomFilter=bloomFilterUtil.GetBloomFilter(bloomFilterKey);
if(bloomFilter!=null)//布隆过滤器存在
{
System.out.println("布隆过滤器存在");
//不存在说明请求的redisKey mysql不存在
if(!bloomFilter.contains(redisKey)) {
System.out.println("redisKey mysql不存在");
return null;
}
}
}
System.out.println("key不存在="+redisKey);
if(isUseLock)//使用互斥锁 解决缓存击穿和雪崩的方法二
{
if(lockUtil.CheckIsLock(redisKey))//被锁了
{
System.out.println("已经锁了");
Thread.sleep(100);
return RedisCheck(joinPoint);
}
else{
System.out.println("没锁,现在加上锁了");
}
}
}
Object result = null;
try{
result = joinPoint.proceed(args);//调用原本的Service函数,访问mysql获取数据
redisCacheUtil.AddCacheData(redisKey,result,delTime);
System.out.println("请求mysql");
}
finally {
if(isUseLock)
{
System.out.println("释放锁");
lockUtil.UnLock(redisKey);//防止报错
}
}
return result;
}
}
这里的解决方法并非最优,一般需要根据项目做修改。
例如:
解决缓存击穿的问题中,这里我们是存下空值,避免下一次请求不存在的数据又访问数据库。但是恶意用户攻击可能会换不同的不存在的key来攻击(例如:我key值选-1递减的来请求),这样会导致内存不断的增加,因此加入了布隆过滤器来处理。
但是如果已知一个项目中的表key值id是大于0,小于N(这个N是可以获取到的)。并且0-N之前被删的id不多,那么我完全可以不用布隆过滤器,直接判断id是不是在0-N之外,就可以确定id是不是不存在的了。就算是最坏的情况,redis也就只是多了0-N之间被删id的空数据。
3.Redis缓存管理工具
@Component
public class RedisCacheUtil {
@Resource
private RedisUtil redisUtil;
public Object GetCacheData(String key, Object[] args )
{
key = GetKey(key,args);
return GetCacheData(key);
}
public Object GetCacheData(String redisKey)
{
Object data =null;
if (redisUtil.hasKey(redisKey)) {
data = redisUtil.get(redisKey);
}
return data;
}
public boolean CheckHaveCacheData(String key, Object[] args )
{
key = GetKey(key,args);
return CheckHaveCacheData(key);
}
public boolean CheckHaveCacheData(String redisKey)
{
return redisUtil.hasKey(redisKey);
}
public void AddCacheData(String key, Object[] args,Object data,int delTime)
{
key = GetKey(key,args);
AddCacheData(key,data,delTime);
}
public void AddCacheData(String redisKey, Object data,int delTime)
{
redisUtil.set(redisKey, data);
if(delTime>0)
redisUtil.expire(redisKey, delTime);
}
public void RemoveCacheData(String key, Object[] args){
key = GetKey(key,args);
if (redisUtil.hasKey(key)) {
redisUtil.del(key);
}
}
public void RemoveCacheData(String key, Object arg){
key = GetKey(key,arg);
if (redisUtil.hasKey(key)) {
redisUtil.del(key);
}
}
}
4.布隆过滤器实现
public class BloomFilter {
private static final int BIT_SIZE = 12;
private static final int DEFAULT_SIZE = 2 << BIT_SIZE;
private static final int[] SEEDS = new int[]{3, 13, 46};
private BitSet bits = new BitSet(DEFAULT_SIZE);
private HashCode[] hashFunc = new HashCode[SEEDS.length];
public BloomFilter() {
for (int i = 0; i < SEEDS.length; i++) {
hashFunc[i] = new HashCode(DEFAULT_SIZE, SEEDS[i]);
}
}
public void add(Object value) {
for (HashCode f : hashFunc) {
bits.set(f.hash(value), true);
}
}
public boolean contains(Object value) {
boolean ret = true;
for (HashCode f : hashFunc) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
public static class HashCode {
private int bitSize;
private int seed;
public HashCode(int bitSize, int seed) {
this.bitSize = bitSize;
this.seed = seed;
}
public int hash(Object value) {
int h;
return (value == null) ? 0 : Math.abs(seed * (bitSize - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
}
}
}
5.布隆过滤器管理
@Component
public class BloomFilterUtil {
private Map<String,BloomFilter> bloomFilterMap=new HashMap<>();
public BloomFilter GetBloomFilter(String key)
{
if(!bloomFilterMap.containsKey(key))
return null;
return bloomFilterMap.get(key);
}
public void AddBloomFilter(String key,BloomFilter bloomFilter)
{
bloomFilterMap.put(key,bloomFilter);
}
}
6.互斥锁
@Component
public class LockUtil {
private static final String lockKey="lockKey";
private Set<String> lockSet=new CopyOnWriteArraySet<>();
public boolean CheckIsLock(String key){
key = GetKey(lockKey,key);
return !lockSet.add(key);
}
public void UnLock(String key){
key = GetKey(lockKey,key);
lockSet.remove(key);
}
}
7.使用方法
@RedisCheck(key = RedisKey.topViewVideoKey)
public List<VideoEntity> FindTopViewCountVideo(int start, int len) {
return videoMapper.FindTopViewCountVideo(start, len);
}
8.服务器启动时,初始化布隆过滤器
public void InitBloomFilter(){
List<VideoEntity> videoEntityList = videoService.GetAllVideo();
BloomFilter bloomFilter=new BloomFilter();
for(VideoEntity videoEntity:videoEntityList)
{
bloomFilter.add(KeyUtil.GetKey(RedisKey.videoKey,videoEntity.getId()));
}
bloomFilterUtil.AddBloomFilter(BloomFilterKey.videoKey,bloomFilter);
}
三、测试用例
一、测试布隆过滤器:
设置
@RedisCheck(key= RedisKey.videoKey,isUseNull=false,bloomFilterKey= BloomFilterKey.videoKey)
单元测试
@Test
//10个线程 执行10次
//@PerfTest(invocations = 10,threads = 10)
public void Test(){
appInitService.InitBloomFilter();
VideoEntity videoEntity= videoService.FindVideoByID(-1);
System.out.println("+++++++++null="+(videoEntity==null));
}
1. 输入不存在的key=-1请求一次。预期输出:
----------------
布隆过滤器存在
redisKey mysql不存在
+++++++++null=true
2.输入不存在的key=-1再请求一次。预期输出和上面一致。
二、测试是否为空也返回
设置
@RedisCheck(key= RedisKey.videoKey,isUseNull=true)
单元测试
@Test
//10个线程 执行10次
//@PerfTest(invocations = 10,threads = 10)
public void Test(){
appInitService.InitBloomFilter();
VideoEntity videoEntity= videoService.FindVideoByID(-1);
System.out.println("+++++++++null="+(videoEntity==null));
}
1. 输入不存在的key=-2请求一次。预期输出:
----------------
key不存在=video--2
没锁,现在加上锁了
请求mysql
释放锁
+++++++++null=true
2. 输入不存在的key=-2再请求一次。预期输出:
----------------
key存在=video--2
即使为空也返回
+++++++++null=true
三、测试互斥锁
设置
@RedisCheck(key= RedisKey.videoKey,isUseLock=true)
单元测试
开10个线程,总共执行10次
@Rule
public ContiPerfRule contiPerfRule = new ContiPerfRule();
@Test
//10个线程 执行10次
@PerfTest(invocations = 10,threads = 10)
public void Test(){
appInitService.InitBloomFilter();
VideoEntity videoEntity= videoService.FindVideoByID(152);
System.out.println("+++++++++");
}
pom.xml需要引入
<dependency>
<groupId>org.databene</groupId>
<artifactId>contiperf</artifactId>
<version>2.3.4</version>
<scope>test</scope>
</dependency>
1. 输入存在的key=156执行。
当输出中,打印了两次以上“key不存在”(打印一次不能说错,但是达不到测试目的),“请求mysql”打印了一次,那就是正常的。
----------------
key不存在=video-156
没锁,现在加上锁了
----------------
key不存在=video-156
已经锁了
----------------
请求mysql
释放锁
+++++++++null=false
key存在=video-156
----------------
----------------
key存在=video-156
----------------
----------------
----------------
key存在=video-156
key存在=video-156
key存在=video-156
key存在=video-156
----------------
+++++++++null=false
----------------
key存在=video-156
+++++++++null=false
key存在=video-156
+++++++++null=false
+++++++++null=false
+++++++++null=false
+++++++++null=false
+++++++++null=false
+++++++++null=false
----------------
key存在=video-156
+++++++++null=false