【Redis实战篇】Redis有可能出现的问题以及如何解决问题

 🔥作者主页小林同学的学习笔录

🔥mysql专栏小林同学的专栏

目录

1.Redis

1.1  短信登录

1.1.1  基于Session实现登录

1.1.2  集群的session共享问题

1.1.3  基于Redis实现共享session登录

1.1.4  登录拦截器的优化

1.2  商户查询缓存

1.2.1  什么是缓存

1.2.2  添加Redis缓存

1.2.3  缓存更新策略

1.2.3.1  主动更新策略

1.2.4  缓存穿透

1.2.5  缓存雪崩

1.2.6  缓存击穿

1.2.7  缓存工具封装


主打逻辑清晰有条理,代码实现可以找我要

小林同学学JAVA,不是干货不制作https://blog.csdn.net/2301_77358195

1.Redis


Redis数据库很多人知道,但是对于它的作用还是有点不清晰,一张图让你了解

1.1  短信登录

资源获取:可以找我要


启动项目后,在浏览器访问:http://localhost:8081/shop-type/list ,如果可以看到数据则证明运行

没有问题

启动nginx,然后访问:http:// http://localhost:8080 ,即可看到页面

1.1.1  基于Session实现登录

threadlocal: Java Web 应用程序中使用 ThreadLocal 来保存用户登录信息。这样做的好处是,每

个线程(即每个请求)都可以访问自己独立的用户登录信息,而不会与其他线程的登录信息混淆。

用户退出时清除 ThreadLocal 数据。


有一个问题就是,当我修改页面的信息并且退出用户,然后继续登录该用户是否会数据同步问题?

这其实要是不关threadlocal的事情,threadlocal只不过是保证你这次登录请求里面的其他请求是否

会被拦截器放行,拦截器校验该threadlocal,如果是同一个用户就会选择放行。

数据同步的话,需要你把用户信息保存到数据库,然后第二次登录查看该用户信息,从而实现数据

同步。

①.接下来完成一下发送验证码的请求以及后端响应验证码

主要逻辑:

  • 1.验证手机号
  • 2.如果手机号不符合则返回错误信息
  • 3.手机号符合,生成验证码
  • 4.把验证码保存到session
  • 5.发送验证码

②.然后按登录又发送一个请求

主要逻辑:

  • 1.检验手机号(因为是另外一个请求,有可能电话号码被改了)
  • 2.校验验证码
  • 3.验证码不一致或者为null,返回错误信息
  • 4.验证码一致根据手机号查询用户
  • 5.判断用户是否存在
  • 6.不存在,就创建用户并保存到数据库
  • 7.将用户信息保存到session,但是只保存DTO(用户的id和名称),隐藏用户的敏感信息

③.点完登录页面

前端再一次发送请求后会携带sessionId访问后端

为了不让请求直接请求到对应的controller,要配置一个拦截器进行请求的转发到不同的controller


拦截器的主要逻辑:

  • 1.获取session
  • 2.获取session的用户
  • 3.判断用户是否存在
  • 4.用户不存在,拦截并返回401的代码
  • 5.存在,把用户信息保存到threadlocal
  • 6.放行


1.1.2  集群的session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数

据丢失的问题。

session的替代方案应该满足:

  • 数据共享
  • 内存存储
  • key、value结构

1.1.3  基于Redis实现共享session登录


改进后的效果

①.接下来完成一下发送验证码的请求以及后端响应验证码

主要逻辑:

  • 1.验证手机号
  • 2.如果手机号不符合则返回错误信息
  • 3.手机号符合,生成验证码
  • 4.把验证码保存到redis,并且设置验证码的有效期
  • 5.发送验证码

②.然后按登录又发送一个请求

主要逻辑:

  • 1.检验手机号(因为是另外一个请求,有可能电话号码被改了)
  • 2.从redis获取验证码并校验
  • 3.验证码不一致或者为null,返回错误信息
  • 4.验证码一致根据手机号查询用户
  • 5.判断用户是否存在
  • 6.不存在,就创建用户并保存到数据库
  • 7.保存用户到redis中
  •    7.1 随机生成token,作为登录的令牌
  •    7.2 将User对象转为HashMap
  •    7.3 存储对象到HashMap
  •    7.4 设置toke有效期为30分钟
  • 8.返回token给客户端,这里一定要返回给客户端

拦截器的主要逻辑:

1.获取session

2.基于TOKEN获取redis中的用户

3.判断用户是否存在

4.用户不存在,拦截并返回401的代码

5.将查询到的Hash数据转为UserDTO对象

6.存在,把用户信息保存到threadlocal

7.刷新token的有效期

8.放行


拦截器刷新token有效期的原因是为了仿造session,如果在30分钟内没有任何活动,tomcat会删除该sessionId,需要用户重新登录,

如果超出没走拦截器redis对于前端的token信息将被删除,从而需要重新登录

如果走拦截器就会一直刷新token,防止登录的时候设置token时间为30分钟,超过30分钟被强制退出


1.1.4  登录拦截器的优化


为什么需要优化呢?

因为我们只对应一个拦截器,并且该拦截器只拦截登录的路径,这就存在问题,如果用户活动其他

路径,但是token有效期并没有被刷新,因此需要再定义一个拦截器来拦截所有路径,给token更新

有效期

可以看到第一个拦截器负责更新token以及保存用户到ThreadLocal,第二个拦截器则只需要判断

ThreadLocal是否有用户,从而确定是不是要放行


1.2  商户查询缓存

1.2.1  什么是缓存

缓存是一种临时存储数据的技术,旨在提高数据访问速度和性能。当程序需要访问某些数据时,它

们通常会从缓存中获取数据,而不是直接从原始数据源(如数据库或网络)获取。

这样可以减少访问原始数据源的频率,从而降低系统的负载并提高响应速度。

缓存涉及的地方很广,比如:

1.2.2  添加Redis缓存

代码用到了许多hutool工具类方法

①.toBean(String jsonString, Class<T> beanClass)

JSONUtil.toBean(type, ShopType.class)

含义是将type字符串转成ShopType对象

②.toJsonStr(Object obj)

JSONUtil.toJsonStr(shopType)

含义是将shopType实例转成String类型

③.toList(String jsonArray, Class<T> elementType)

JSONUtil.toList(shopType, ShopType.class)

含义是将字符串转换为ShopType对象


主要逻辑:

  • 1.从redis查询商铺
  • 2.判断一下商铺存在不存在
  • 3.redis商铺存在,返回商铺信息
  • 4.redis商铺不存在,就去数据库查询
  • 5.数据库中商铺不存在,返回错误信息
  • 6.数据库商铺存在,将数据缓存到redis
  • 7.返回数据


1.2.3  缓存更新策略

业务场景:
低一致性需求:使用内存淘汰机制。例如:修改比较少

高一致性需求:主动更新,并以超时剔除作为兜底方案。例如:修改操作比较多

1.2.3.1  主动更新策略

操作缓存和数据库时有三个问题需要考虑:

1.删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存

答案:可以看出删除缓存比较高效

2.如何保证缓存与数据库的操作的同时成功或失败?
单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等分布式事务方案

3.先操作缓存还是先操作数据库?
先删除缓存,再操作数据库
先操作数据库,再删除缓存

答案:先操作数据库,再删除缓存,这种方案线程安全性高

为什么先操作数据库,再删除缓存,这种方案线程安全性高?

案例:给查询商铺的缓存添加超时剔除和主动更新的策略

修改ShopController中的业务逻辑,满足下面的需求:
根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
根据id修改店铺时,先修改数据库,再删除缓存

1.2.4  缓存穿透

缓存穿透是指恶意访问者通过构造恶意的查询条件,使得每次查询都无法命中缓存从而大量请求

直接穿透缓存到达数据库,导致数据库负载过大。

常见的解决方案有两种:


①.缓存空对象:

  • 优点:实现简单,维护方便
  • 缺点:
  • 额外的内存消耗
  • 可能造成短期的不一致

②.布隆过滤:

  • 优点:内存占用较少,没有多余key
  • 缺点:
  • 实现复杂
  • 存在误判可能

注意:这里的空对象是"",不是null,null不是对象

解决缓存穿透

用缓存空对象解决缓存穿透

主要逻辑:

  • 1.从redis查询商铺
  • 2.判断一下商铺存在不存在
  • 3.redis商铺存在,返回商铺信息
  • 4.如果命中的是空值就直接返回错误信息,不用再去查数据库解决缓存穿透
  • 5.redis商铺不存在,就去数据库查询
  • 6.数据库中商铺不存在,返回错误信息前加上values=""的缓存
  • 7.数据库商铺存在,将数据缓存到redis
  • 8.返回数据


改变的地方有两点:

//4.然后在查数据库之前,判断是否为空值,如果为空值("")就返回错误信息,如果不为空值则去查找数据库
if(shopJson.equals("")){
            //返回错误信息
            return Result.fail("店铺不存在");
        }

//6.如果店铺不存在,缓存空值,缓存时间尽量设计少点,因为有可能出现数据不一致问题
if (shop == null) {
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            //返回错误信息
            return Result.fail("店铺不存在"); 
        }


总结:


缓存穿透产生的原因是什么?

用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度(如雪花算法),避免被猜测id规律,因为可以通过过滤id,如果id格式不符合可以被过滤掉
  • 加强用户权限校验
  • 做好热点参数的限流


1.2.5  缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库

带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值,避免同一时间多个key失效问题
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存


    

1.2.6  缓存击穿

缓存击穿是指一个缓存中不存在但数据库中存在的数据,在高并发情况下,当有大量请求同时访问

这个不存在于缓存但存在于数据库的数据时,这些请求会直接穿透缓存,去请求数据库,导致数据

库负载激增,甚至可能引起数据库压力过大而崩溃。

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效

了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

注意:热点数据,缓存预热需要提前加载。

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

   

 //尝试获取锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    //释放锁
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

基于互斥锁方式解决缓存击穿问题

这里的互斥锁用字符串里面的setnx来实现,因为它只能有一个key,其他key是加不进去的

主要逻辑:

  • 1.从redis查询商铺
  • 2.判断一下商铺存在不存在
  • 3.redis商铺存在,返回商铺信息
  • 4.如果命中的是空值就直接返回错误信息,不用再去查数据库解决缓存穿透
  • 5.实现缓存重建
  •    5.1 获取互斥锁
  •    5.2 判断是否获取成功
  •    5.3 失败,则休眠并重试
  •    5.4 成功,根据id查询数据库
  • 6.数据库中商铺不存在,返回错误信息前加上values=""的缓存
  • 7.数据库商铺存在,将数据缓存到redis
  • 8.释放互斥锁
  • 9.返回数据


基于逻辑过期方式解决缓存击穿问题

//设置逻辑时间
RedisData redisData = new RedisData();
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

主要逻辑:

  • 1.从redis查询商铺
  • 2.判断一下商铺存在不存在
  • 3.商铺存在,返回商铺信息
  • 4.命中,需要先把json反序列化为对象
  • 5.判断是否过期
  • 5.1 未过期,直接返回数据
  • 6.已过期,缓存重建
  •    6.1 获取互斥锁
  •    6.2 判断是否获取锁
  •    6.3 成功,开启独立线程,实现缓存重建
  •    6.4 释放锁
  •    6.5 返回过期的商铺信息


 

//5.判断是否过期
 if (expireTime.isAfter(LocalDateTime.now())){
        //5.1 未过期,直接返回数据
        return shop;
    }

总结:

  

1.2.7  缓存工具封装

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间 
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

@Slf4j
@Component
public class CacheClient {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //创建一个线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
    public void set(String key, Object values, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(values),time,unit);
    }

    //将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
    public void setWithLogicalExpire(String key, Object values, Long time, TimeUnit unit){
        //设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(values);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));

        //写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }


    //根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
        String key = keyPrefix + id;

        //1.从redis查询商铺
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断一下商铺存在不存在
        //3.商铺存在,返回商铺信息
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }

        //如果命中的是空值就直接返回,不用再去查数据库解决缓存穿透
        if(json != null){
            //返回错误信息
            return null;
        }

        //4.查数据库
        R r = dbFallback.apply(id);

        //5.商铺不存在,返回错误信息
        if (r == null) {
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //6.商铺存在,将数据缓存到redis
        //stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        this.set(key,r,time,unit);
        //7.返回数据
        return r;
    }

    //缓存击穿,用逻辑时间解决方案
    public <R,ID> R queryWithLoginExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
        String key = keyPrefix + id;

        //1.从redis查询商铺
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断一下商铺存在不存在
        //3.商铺存在,返回商铺信息
        if (StrUtil.isBlank(json)) {
            return null;
        }
        //4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //5.1 未过期,直接返回数据
            return r;
        }

        //6.已过期,缓存重建
        //6.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean tryLock = tryLock(lockKey);
        //6.2 判断是否获取锁
        if (tryLock) {
            //6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                //重构缓存
                try {
                    //查询数据库
                    R r1 = dbFallback.apply(id);
                    //写入redis
                    this.setWithLogicalExpire(key,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //6.4 返回过期的商铺信息
        return r;
    }
    
    //互斥锁解决缓存穿透
    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

    //尝试获取锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //释放锁
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

}

最近更新

  1. TCP协议是安全的吗?

    2024-04-23 01:10:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-23 01:10:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-23 01:10:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-23 01:10:01       20 阅读

热门阅读

  1. UDF小白入门

    2024-04-23 01:10:01       15 阅读
  2. git工具的安装及使用

    2024-04-23 01:10:01       12 阅读
  3. backtracking Leetcode 回溯算法题

    2024-04-23 01:10:01       12 阅读
  4. Linux文本处理三剑客:awk、grep和sed

    2024-04-23 01:10:01       15 阅读
  5. js高级 笔记03

    2024-04-23 01:10:01       12 阅读
  6. FastJson的使用

    2024-04-23 01:10:01       13 阅读
  7. 【程序设计与算法——C/C++入门】C语言入门

    2024-04-23 01:10:01       17 阅读