当前位置: 首页 > news >正文

Redis02-分布式session、缓存查询及缓存问题的解决

一、短信登录及分布式session

在这里插入图片描述
验证码缓存

 @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        boolean invalid = RegexUtils.isPhoneInvalid(phone);

        if(invalid){
            return Result.fail("手机号格式不正确");
        }

        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);

        //3.保存验证码到session或redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,2, TimeUnit.MINUTES);
        //发送验证码
        SMSUtils.sendCode(phome,code);
        return Result.ok(new LoginFormDTO(phone,code));
    }

登录/注册

 @Override
    public Result loginService(LoginFormDTO loginForm,HttpSession session) {
        // TODO 实现登录功能
        //1.验证手机号
        if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){
            return Result.fail("手机号格式不正确");
        }

        //2.校验验证码
//        Object cacheCode = session.getAttribute("code");
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+loginForm.getPhone());
        if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())){
            return Result.fail("验证码错误");
        }

        //3.查询该用户是否存在
        User user = query().eq("phone", loginForm.getPhone()).one();

        //4.若不存在:创建用户并添加到数据库
        if(user == null){
            user= createUserWithPhone(loginForm.getPhone());
        }

        //5.保存数据
        //5.1 将user属性拷贝到userDto中
        UserDTO dto = BeanUtil.copyProperties(user,UserDTO.class);

        //5.2生成token作为登陆令牌
        String token = UUID.randomUUID().toString();
        //5.3将数据保存到redis中
        //由于使用stringRedisTemplate,所以map的key和val都要转成string类型才能被正确序列化和反序列化
        Map<String, Object> map = BeanUtil.beanToMap(dto, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((key, val) -> val.toString()));
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);
        //5.3设置token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok(token);
    }

二、登录拦截器以及刷新

在这里插入图片描述

我们定义两个拦截器,一是FlushTokenInterceptor,二是loginInterceptor,作用如下:

  1. FlushTokenInterceptor:根据token从redis中获取用户信息并将它封装成User对象放入ThreadLocal中,同时刷新过期时间
  2. loginInterceptor:对需要登录的路径进行拦截,从ThreadLocal中获取User对象,若获取不到说明没有登录
  3. FlushToken拦截器的优先级高于login拦截器

MvcConfig

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
                "/user/code",
                "/user/login",
                "/blog/hot",
                "/shop/**",
                "/voucher/**",
                "/shop-type/**").order(1);
        registry.addInterceptor(new FlushTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

FlushTokenInterceptor

public class FlushTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public FlushTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取token
//        HttpSession session = request.getSession();
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
            return true;
        }
        //2.基于token取出user信息
//        UserDTO user = (UserDTO) session.getAttribute("user");
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        //3.判单用户是否存在
        if(entries.isEmpty()){
            return true;
        }
        //4。保存用户信息到threadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(entries,new UserDTO(),false);
        UserHolder.saveUser(userDTO);


        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);



        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

LoginInterceptor

public class LoginInterceptor implements HandlerInterceptor {



    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserDTO user = UserHolder.getUser();
        if(user == null){
            response.setStatus(401);
            return false;
        }


        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

三、商户查询缓存

缓存

引入缓存的目的是提高读数据的性能,降低响应时间。

但不代表任何数据我们都可以通过添加缓存来提高性能,对于那些经常修改的数据,若是添加缓存反而会降低性能。再者,缓存一般占用的是内存,滥用缓存也会对内存造成浪费。
可以考虑添加缓存的数据:读多写少的数据、热点数据

缓存带来的数据不一致问题

该问题主要是由于更新缓存策略不当造成的
缓存更新策略:先修改数据库,再删除缓存
这样能够最大限度的保证数据一致性,但仍然存在数据不一致的问题,如下:
在这里插入图片描述
造成数据不一致是由于读key时key过期了,并且读数据库的操作先于修改数据库的操作、写缓存的操作晚于删除缓存的操作,这是几乎不可能的发生的,理由如下:
在多线程并行下,更新数据库的时间都要远远大于写入缓存的时间,所以写入缓存的执行时机应该先于更新数据库。

缓存穿透

当用户频繁访问数据库中不存在的数据时,请求直接穿透缓存直接到达数据库,增加了数据库的压力

解决方法:1. 对数据库中不存在的数据缓存null 2. 布隆过滤器
注意:缓存空值设置的过期时间尽量短,当数据库新增数据时可能会造成数据不一致,所以尽量缩短数据不一致的时间

缓存雪崩

大量缓存在同一时间内过期,导致大量请求到达数据库

解决方法:1. 给不同key的TTL加上一个随机值 2. 给缓存业务添加降级限流

缓存击穿

热点数据过期导致大量请求到达数据库

解决方法:1. 添加互斥锁 2. 设置逻辑过期时间
在这里插入图片描述
在这里插入图片描述
由于逻辑过期策略不会因为结果为null而去查询数据库,只会在缓存过期时才去查询数据库,因此需要注意以下事项
逻辑过期注意事项:

  1. 逻辑过期的数据需要提前预热,建立逻辑过期缓存
  2. 查询该业务的数据时先使用逻辑过期策略,若返回null再使用其它策略
  3. 修改数据时不用删除逻辑过期的key
  4. 删除数据时需要删除逻辑过期的key和不同缓存的key

缓存查询工具类

该类封装了不同的方法去做缓存的查询以及添加,目的是为了解决不同的缓存问题

@Component
public class CacheClient {
    @Autowired
    private  StringRedisTemplate stringRedisTemplate;

    private ExecutorService CACHE_REBUILD_POOL = Executors.newFixedThreadPool(10);
    /**
     * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
     */
    public <T> void setKeyWithExpire(String key, T data, long time, TimeUnit unit){
        if(data.getClass() != Integer.class && data.getClass() != Long.class && data.getClass() != Double.class && data.getClass() != String.class ) {
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data), time, unit);
        }
        stringRedisTemplate.opsForValue().set(key, data.toString(), time, unit);

    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
     */
    public <T> void setKeyWithLogicExpire(String key,T data, long time){
        //1.构建redisData并设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(data);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));

        //2.将redisData添加到redis中
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }


    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
     */
    public <T,ID> T queryDataThroughPass(String keyPrefix, ID id, Class<T> clazz, Function<ID,T> dbCallback,long time,TimeUnit unit){
        //1.通过key从redis中获取json
        String json = stringRedisTemplate.opsForValue().get(keyPrefix+id);
        //2.若json不为null和空白字符串则代表redis命中,返回结果
        if(StrUtil.isNotBlank(json)){
            return JSONUtil.toBean(json,clazz);
        }

        //3.若redis没命中则判断未命中情况
        if(json != null){ //3.1 redis中存在该key的空白字符串,说明是缓存穿透的key,返回null
            return null;
        }else { //3.2 redis中没有该key,需要去数据库中查找数据
            T data = dbCallback.apply(id);
            // 1.存在该数据,缓存到redis
            if(data != null){
                setKeyWithExpire(keyPrefix + id,JSONUtil.toJsonStr(data), time,unit);
            }else {
                //2.没有该数据,对该key设为""
               setKeyWithExpire(keyPrefix+id,"",time,unit);
            }
            return data;
        }
    }

    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用互斥锁的方式解决缓存击穿问题
     **/
    public <T,ID> T queryDataByMutex(String keyPrefix, ID id, Class<T> clazz, Function<ID,T> dbCallback,long time,TimeUnit unit){
        //1.通过key获取json
        String json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //2.判断redis是否命中,命中直接返回
        if(StrUtil.isNotBlank(json)){

            return JSONUtil.toBean(json,clazz);
        }
        //3.若没命中
         if(json != null) {//3.1 查看redis是否对该key缓存空值,是则返回null
             return null;
         }

         T data = null;
         //3.2 若不是则重建缓存
        try {
            boolean lock = tryLock(id);
            if (!lock) {
                Thread.sleep(50);
                return queryDataByMutex(keyPrefix, id, clazz, dbCallback, time, unit);
            }
            data = dbCallback.apply(id);
            if (data != null) {  //1. 从数据库中查询数据,若存在则添加到redis中
                setKeyWithExpire(keyPrefix + id, data, time, unit);
            } else {
                //2. 若不存在则对该key缓存空值
                setKeyWithExpire(keyPrefix + id, "", time, unit);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();

        }finally {
            unlock(id);
        }
        return data;
    }

    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
     */
    public <T,ID> T queryDataByLogicExpire(String keyPrefix, ID id, Class<T> clazz, Function<ID,T> dbCallback,long time){
        //1.从redis中获取json
        String json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //2.若该json不存在则说明没有对热点信息预热,直接返回null
        if(StrUtil.isBlank(json)){
            return null;
        }
        //3.将json转化成redisData,查看是否过期
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        T data = JSONUtil.toBean((JSONObject) redisData.getData(), clazz);
        // 3.1 若没有过期则直接返回
        if(redisData.getExpireTime().isAfter(LocalDateTime.now())){
            return data;
        }else { // 3.2 若过期则重建缓存
            //1.获取互斥锁
            boolean lock = tryLock(id);
            if(!lock){ //若不成功则直接返回旧数据
                return data;
            }

            //2.若成功则异步重建缓存
            CACHE_REBUILD_POOL.submit(()->{
                T res = dbCallback.apply(id);
                setKeyWithLogicExpire(keyPrefix+id,res,time);
                unlock(id);
            });
            return data;
        }
    }

    private <T> boolean tryLock(T id){
        String key = RedisConstants.LOCK_SHOP_KEY + id;
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private <T> void unlock(T id){
        String key = RedisConstants.LOCK_SHOP_KEY + id;
        stringRedisTemplate.delete(key);
    }
}

业务流程

在这里插入图片描述

public Shop findShopThroughRedis(long id){
        String key = RedisConstants.CACHE_SHOP + id;
        //1.查询redis中是否有该商铺
        String json = stringRedisTemplate.opsForValue().get(key);

        //2.如果有则返回
        if(!StrUtil.isBlank(json)){
            return JSONUtil.toBean(json,Shop.class);
        }

        //3.若不存在则去数据库查询该店铺
        Shop shop = this.getById(id);
        //3.1 若该店铺不存在则对该id保存null,防止缓存穿透
        if(shop == null){
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }

        //4.将查询到的shop保存到redis中
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL*60 + RandomUtil.randomInt(-30,30), TimeUnit.SECONDS);
        return shop;
    }

相关文章:

  • JavaWeb——AjaxJson
  • Spring-Cloud-Feign-03
  • 【深入Javascript闭包】
  • 词典
  • Spring Bean的生命周期
  • 秋招-致谢
  • 「实用工具—LICEcap」写博必备|动图制作|一键生成gif(GIF)
  • 3D目标检测(一)
  • 秋招面试- - -Java体系最新面试题(8)
  • 前端工程师面试题详解(四)
  • app端专项测试
  • 我操作MySQL的惊险一幕
  • 模糊预测股价走势
  • Qt之开源绘图控件QCustomPlot
  • Python 语言程序设计 第五章 字符串应用举例
  • @angular/forms 源码解析之双向绑定
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • 【跃迁之路】【669天】程序员高效学习方法论探索系列(实验阶段426-2018.12.13)...
  • 30天自制操作系统-2
  • CentOS7简单部署NFS
  • CNN 在图像分割中的简史:从 R-CNN 到 Mask R-CNN
  • Electron入门介绍
  • ERLANG 网工修炼笔记 ---- UDP
  • spring security oauth2 password授权模式
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • Terraform入门 - 1. 安装Terraform
  • Vue UI框架库开发介绍
  • vue从创建到完整的饿了么(11)组件的使用(svg图标及watch的简单使用)
  • WebSocket使用
  • 从零到一:用Phaser.js写意地开发小游戏(Chapter 3 - 加载游戏资源)
  • 关于 Cirru Editor 存储格式
  • 将回调地狱按在地上摩擦的Promise
  • 如何选择开源的机器学习框架?
  • 突破自己的技术思维
  • 【云吞铺子】性能抖动剖析(二)
  • ​DB-Engines 12月数据库排名: PostgreSQL有望获得「2020年度数据库」荣誉?
  • #define用法
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (k8s中)docker netty OOM问题记录
  • (ZT) 理解系统底层的概念是多么重要(by趋势科技邹飞)
  • (附源码)springboot家庭财务分析系统 毕业设计641323
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (四)汇编语言——简单程序
  • (学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解
  • (转贴)用VML开发工作流设计器 UCML.NET工作流管理系统
  • (最完美)小米手机6X的Usb调试模式在哪里打开的流程
  • .net core 连接数据库,通过数据库生成Modell
  • .Net Core与存储过程(一)
  • .Net 垃圾回收机制原理(二)
  • .Net面试题4
  • @angular/cli项目构建--Dynamic.Form
  • @Conditional注解详解
  • [ NOI 2001 ] 食物链
  • [ 蓝桥杯Web真题 ]-Markdown 文档解析