Redis缓存配置

redis缓存

使用redis缓存的原因是因为在可能的高并发环境下,mysql数据库无法承受大量的请求,可能会导致数据库崩溃。而这些请求很大一部分都是查询请求,因此采用redis这样的以内存作为存储数据空间的数据库来存储查询请求的数据,这样既提高了查询的效率,又分担了一大部分请求并发的压力。

具体实现使用了AOP面向切面编程的思想,即使用@PointCut注解指定切面,在本项目中就是指定各类控制器中的各种方法,并且以**@Around注解代表的环绕方式**进行切入。利用简单类名加方法名加参数名作为缓存的键cacheKey,方法得到的值作为键的值。对于一个查询请求类型的方法执行前先在redis中查询cacheKey,如果没有则在redis中设置这个cacheKey和值以及过期时间,设置过期时间的原因是因为redis是基于内存存储数据,因此需要定期清理数据。如果有cacheKey则直接从redis中获取值。对于除了查询类型以外的方法,则先删除当前控制器下的所有缓存键,避免缓存无法更新的问题,然后再去数据库进行查询,并返回结果。

在这个过程中,可能会遇到缓存击穿,缓存穿透,缓存雪崩的问题。

因为所谓的缓存击穿,就是一个承受着高并发请求的键,因为过期时间到了,cacheKey就失效了,这时候高并发的请求会涌入mysql数据库,就像大量的请求击穿了缓存一样,因此叫做缓存击穿嘛。在本项目中解决的方案为当cacheKey不存在时,进入数据库中查询的行为使用syncronize锁住,并且使用双检锁确保只有一个线程能进入数据库查询,这样即使高并发请求的key失效也不会造成大量的请求涌入mysql数据库了,就解决了缓存击穿的问题。但是因为此项目没有高并发的用户访问,属于内部操作管理系统,所以使用syncronize已足够。

而面向高并发用户的系统使用syncronize来锁就不能解决分布式服务器的问题,因为syncronize和lock只能锁住本地JVM的线程,无法锁住其他服务器中的线程,因此在那种情况下应该采用分布式锁,而分布式锁的实现比较常见的有两种方案,一种为zookeeper实现,一种是redis实现,实现原理为请求访问时使用SETNX命令向redis中存储一个具有过期时间的key,如果成功设置则上锁成功,如果设置失败则进入阻塞。由于分布式服务器使用的是同一个redis数据库,所以就能达到互斥的效果。

而对于缓存雪崩的问题,由于缓存雪崩就是多个key同时过期导致大量查询请求进入数据库,所以在设置键的过期时间的时候精确到微妙或纳秒级别,这样就可以避免大量键同时过期的问题。

对于缓存穿透的问题,也就是解决用户利用查询数据库不存在的数据进行恶意攻击的行为,本项目中是即使数据库中查出数据可能为空,但是也添加到缓存中,这样再次查询就会查出缓存中的空而不是进入mysql数据库中查。

java 复制代码
@Aspect
@Component
public class RedisCacheAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private final Random random = new Random();

    //    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    @Pointcut("execution(* com.stt.cms.*.controller..*(..))")
    private void pointcut(){}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Signature signature = pjp.getSignature(); //获取连接点的签名
        MethodSignature ms = (MethodSignature) signature;//因为在spring aop中,连接点的类型都是方法,因此,这个签名就是一个方法的签名
        Method method = ms.getMethod();//从方法签名中获取方法
        String className = method.getDeclaringClass().getSimpleName();
        if (method.isAnnotationPresent(GetMapping.class)) {//如果方法上存在GetMapping注解,说明这个方法就是一个查询方法
            // 检查是否是上传或下载方法
            if (method.getName().startsWith("upload") || method.getName().startsWith("download")) {
                return pjp.proceed(); // 直接执行方法,不进行缓存
            }

            //需要将返回结果放入redis缓存中,方便下一次查询的时候使用
            Object[] args = pjp.getArgs();
            String methodName = method.getName();
            String cacheKey = className + "::" + methodName + JSON.toJSONString(args);
            //首先去校验缓存中是否存在我们需要获取的数据
            if (Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey))) {
                //如果缓存中存在,那么就说明获取的数据就是返回值
                return redisTemplate.opsForValue().get(cacheKey);
            } else {
                //考虑到并发问题,这里需要上锁进行处理
                synchronized (this){ //这里的双检锁就是为了解决缓存击穿问题的
                    //如果redis中不存在缓存
                    if(Boolean.FALSE.equals(redisTemplate.hasKey(cacheKey))){
                        //缓存中没有数据
                        Object result = pjp.proceed(); //执行方法得到的返回结果
                        //这里的过期时间要设置为随机过期时间,防止缓存雪崩,但是需要注意的是,随机时间是小单位的
                        //随机,不能是大单位的随机
                        long expire = Duration.ofMinutes(5).toNanos() + random.nextInt(1000);
                        redisTemplate.opsForValue().set(cacheKey, result, expire, TimeUnit.NANOSECONDS);
                        return result;
                    }  else {
                        return redisTemplate.opsForValue().get(cacheKey);
                    }
                }
            }
            //这里还需要考虑缓击穿和缓存存穿透问题
        } else {
            //其余情况我们只需要考虑增删改带来的缓存失效问题的处理
            if(method.isAnnotationPresent(PostMapping.class)
               || method.isAnnotationPresent(PutMapping.class)
               ||method.isAnnotationPresent(DeleteMapping.class)){
                //这里需要考虑去删除缓存,从而更新缓存
                List<String> cacheKeys = redisTemplate.execute((RedisCallback<List<String>>) connection -> {
                    ScanOptions scanOptions = ScanOptions.scanOptions().match(className + "*").count(50).build();
                    List<String> keys = new ArrayList<>();
                    Cursor<byte[]> cursor = connection.scan(scanOptions);
                    while (cursor.hasNext()) {
                        byte[] next = cursor.next();
                        String scanKey = new String(next);
                        System.err.println("扫描到失效的键:" + scanKey);
                        keys.add(scanKey);
                    }
                    return keys;
                });
                if(cacheKeys != null)
                    redisTemplate.delete(cacheKeys);
            }
            return pjp.proceed();
        }
    }
}
相关推荐
hanbarger几秒前
nosql,Redis,minio,elasticsearch
数据库·redis·nosql
微服务 spring cloud22 分钟前
配置PostgreSQL用于集成测试的步骤
数据库·postgresql·集成测试
先睡25 分钟前
MySQL的架构设计和设计模式
数据库·mysql·设计模式
弗罗里达老大爷26 分钟前
Redis
数据库·redis·缓存
别这么骄傲1 小时前
lookup join 使用缓存参数和不使用缓存参数的执行前后对比
缓存
仰望大佬0071 小时前
Avalonia实例实战五:Carousel自动轮播图
数据库·microsoft·c#
学不透java不改名1 小时前
sqlalchemy连接dm8 get_columns BIGINT VARCHAR字段不显示
数据库
一只路过的猫咪1 小时前
thinkphp6使用MongoDB多个数据,聚合查询的坑
数据库·mongodb
呼啦啦啦啦啦啦啦啦2 小时前
【MySQL篇】事务的认识以及四大特性
数据库·mysql