自定义注解解决Redis三大问题代码实现

一、背景

当我们想要某个接口访问时,优先访问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

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序资源库

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值