Redis实战篇笔记

Redis实战笔记



前言

该笔记是根据黑马的Redis视频中的实战篇而记录的笔记,不仅有课上的ppt图片,也有我自己对课程内容的理解,并且包括所有的源代码

思维导图


短信验证码的登录注册功能

基于Session实现登录


实现发送短信验证码功能

@Override
    public Result sendCode(String phone, HttpSession session) {
    
    
    //1. 校验手机号(正则表达式)
    // RegexUtils.isPhoneInvalid(phone)是项目中已提前写好的,校验手机号格式的方法,内部也是用正则表达式,返回的布尔值是判断是否无效
    if (RegexUtils.isPhoneInvalid(phone)) {
    
    
        // 1.1 返回给前端 报错信息
        return Result.fail("手机号格式错误");
    }
    //2. 生成验证码
    String code = RandomUtil.randomNumbers(6);
    //3. 保存验证码到session
    session.setAttribute("code",code);
    //4. 发送验证码 (可能会用到第三方短信服务,这里的项目就不演示了)
    log.debug("短信验证码是: {}",code);
    return Result.ok();
}

实现短信验证码登录功能

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
    
    
        // 这个验证是有问题的,因为他是将手机号和验证码分开验证的,并没有关联验证,
        // 如果手机号发生改变的话,也会登录成功,在下一个版本会用redis代替
        // 1. 从 loginForm里面拿到手机号,验证码进行校验
        String phone = loginForm.getPhone();
        Object code = session.getAttribute("code");
        if(code==null||!code.toString().equals(loginForm.getCode())){
    
    
            // 1.1 返回给前端 报错信息
            return Result.fail("验证码错误");
        }
        if (RegexUtils.isPhoneInvalid(phone)) {
    
    
            // 1.1 返回给前端 报错信息
            return Result.fail("手机号格式错误");
        }
        // 2.根据手机号查用户
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("phone",phone);
        User one = this.getOne(wrapper);
        // 3.如果用户不存在,创建新用户
        if(one==null){
    
    
           one= createUserWithPhone(phone);
        }
        // 4.如果用户存在,保存用户脱敏信息到session
        session.setAttribute("user",one);
        return Result.ok();
    }

实现校验登录状态功能(登录拦截器)

idea 查看接口的方法的快捷键 Ctrl+I

不熟悉 ThreadLocal的小伙伴可以看这篇文章 https://zhuanlan.zhihu.com/p/102744180

public class LoginInterceptor implements HandlerInterceptor {
    
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        // 1. 从session中获取用户信息
        Object user = request.getSession().getAttribute("user");
        // 2.判断用户是否存在
        if (user == null) {
    
    
            // user为空,则登录信息不存在,进行拦截
            // 这里不需要再对数据库进行查询了,因为如果这个登录信息不在数据库中,他就不会登录成功。
            response.setStatus(404);
            return false;
        }
        //3.保存用户的脱敏信息到ThreadLocal中
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties((User)user,userDTO);
        UserHolder.saveUser(userDTO);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    
        // 操作完成后,将登录的用户信息移除调,避免用户泄露
        UserHolder.removeUser();
    }
}
public class MVCConfig  implements WebMvcConfigurer {
    
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(new LoginInterceptor())
                //对于不需要登录也能访问的路径进行放行
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/blog/hot",
                        "upload/**"
                );
    }
}

基于Session实现登录的问题

存在session共享问题:多台 Tomcat并不共享session,当请求切换到不同 tomcat服务时导致数据 丢失的问题
下面是刚才登录功能的架构图,如果用户在第一个tomcat上登录了,然后请求的服务发到了第二个服务,第二个
tomcat上的session上没有登录信息,会导致请求失败

那如果要实现session共享,要满足的条件如下

  1. 数据共享
  2. 内存存储
  3. key,value结构

那用redis就可以解决这个问题

基于Redis实现共享session登录


我们这里在Redis里存对象,用Hash结构,而不是用json来存储,因为Hash结构可以将对象中每个字段独立存储,可以对单个字段做CRUD,内存占用更少。
其实用JSON字符串保存也可以,比较直观,这里就用Hash结构来存储。

这里用随机token来作为key存储对象数据,也是为了防止有人利用phone来获取用户信息

    @Override
    public Result sendCode(String phone, HttpSession session) {
    
    
        //1. 校验手机号(正则表达式)
        // RegexUtils.isPhoneInvalid(phone)是项目中已提前写好的,校验手机号格式的方法,内部也是用正则表达式,返回的布尔值是判断是否无效
        if (RegexUtils.isPhoneInvalid(phone)) {
    
    
            // 1.1 返回给前端 报错信息
            return Result.fail("手机号格式错误");
        }
        //2. 生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3. 保存验证码到redis,  相当于 set key value ex 120
        // 为了防止其他业务也是用手机号作为key,我们这里要给他加一个业务名前缀
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //4. 发送验证码 (可能会用到第三方短信服务,这里的项目就不演示了)
        log.debug("短信验证码是: {}",code);
        return Result.ok();
    }

Redis中的键值对
image.png

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    
    
    // 这个验证是有问题的,因为他是将手机号和验证码分开验证的,并没有关联验证,
    // 如果手机号发生改变的话,也会登录成功,在下一个版本会用redis代替
    // 1. 从 loginForm里面拿到手机号,从redis中拿到校验码
    String phone = loginForm.getPhone();
    String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    if(code==null||!code.equals(loginForm.getCode())){
    
    
        // 1.1 返回给前端 报错信息
        return Result.fail("验证码错误");
    }
    if (RegexUtils.isPhoneInvalid(phone)) {
    
    
        // 1.1 返回给前端 报错信息
        return Result.fail("手机号格式错误");
    }
    // 2.根据手机号查用户
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.eq("phone",phone);
    User one = this.getOne(wrapper);
    // 3.如果用户不存在,创建新用户
    if(one==null){
    
    
        one= createUserWithPhone(phone);
    }
    // 4.将用户脱敏信息保存到redis中
    // 4.1 生成随机token
    String token = UUID.randomUUID().toString(true);
    // 4.2 将User对象转为Hash存储。
    UserDTO userDTO = BeanUtil.copyProperties(one, UserDTO.class);
    // 4.3 这里如果只是 Map<String, Object> userMap = BeanUtil.beanToMap(userDTO)
    // ,由于userDTO的id是Long类型,用stringRedisTemplate操作时,无法将Long类型转为String类型会报错
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
    // 4.3 存到redis中
    stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
    // 4.4 一定要设置过期时间,但是这里不是说30分钟后无效,是用户只要访问,他的有效时间就是30分钟,而当用户30分钟不访问后,才失效
    stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.SECONDS);
    return Result.ok();
}

Redis中的键值对
image.png

登录拦截器的优化

虽然用户信息在Redis中保存的时候设置过期时间了,但是这个时间是当用户信息保存到 redis 后,只要过了30分钟后,用户信息就删除了。但是我们希望的情况是当用户超过30分钟没有访问才删除。所以我们要对拦截器进行优化。

public class RefreshTokenInterceptor implements HandlerInterceptor {
    
    

    private StringRedisTemplate stringRedisTemplate;

    // 这里为什么要有一个构造器呢,是因为这个类没有交给 Spring 管理,Spring不会帮我们去创建他,我们要自己手动创建
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
    
    
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        // TODO 1. 获取请求头的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
    
    
            response.setStatus(404);
            return false;
        }
        // TODO 2.基于Token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 2.判断用户是否存在
        if (userMap.isEmpty() ) {
    
    
            response.setStatus(404);
            return false;
        }
        // 5. TODO  将查到的用户从Hash转为userDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6. 保存
        UserHolder.saveUser(userDTO);

        // 7. 刷新token有效期,只要用户访问任何一个业务就刷新他的过期时间
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.SECONDS);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    
        // 操作完成后,将登录的用户信息移除调,避免用户泄露
        UserHolder.removeUser();
    }
public class LoginInterceptor implements HandlerInterceptor {
    
    
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        if (UserHolder.getUser() == null) {
    
    
            // 无用户,需要拦截
            response.setStatus(401);
            return false;
        }
        // 有用户,放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    
        // 操作完成后,将登录的用户信息移除调,避免用户泄露
        UserHolder.removeUser();
    }
}

商户查询缓存

什么是缓存?
缓存就是数据交换的缓存区,是存储数据的临时地方,一般读写性能较高
Web开发中可以添加的缓存
缓存的作用

  1. 降低后端负载(减少查询数据库,减少数据库的压力)
  2. 提高读写效率,降低响应时间

缓存的成本

  1. 数据一致性成本(当数据库的数据发生变化时,redis中以前缓存的还是旧数据)
  2. 代码维护成本(为了解决一致性,缓存击穿,缓存雪崩等问题,会提高代码复杂度)
  3. 运维成本

    public Result queryById(Long id) {
    
    
        //1. 从Redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.判断缓存是否命中
        if (StrUtil.isNotBlank(shopJson)) {
    
    
            //3.缓存命中直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4.缓存未命中,根据商铺id查询数据库
        Shop shop = this.getById(id);
        if(shop==null){
    
    
            //5. 数据库中查询失败,直接返回
            return Result.fail("店铺不存在");
        }
        //6. 数据库中查询成功,将数据缓存到Redis中
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));
        return Result.ok(shop);
    }

小作业:将商铺类型缓存到Redis中

    public Result queryTypeList() {
    
    
        // 1.这个是首页就要开始查询,应该是从数据库查到数据传到redis
        List<ShopType> sort = this.query().orderByAsc("sort").list();
        List<String> collect = sort.stream().map((shopType ->
                JSONUtil.toJsonStr(shopType))).collect(Collectors.toList());
        stringRedisTemplate.opsForList().leftPushAll(CACHE_SHOP_LIST_KEY,collect);
        return Result.ok(sort);
    }

image.png

总结

这篇文章算是简单的说明了一下 Redis 在应用开发中的一些实用场景,但是这其中还有许多的场景bug,也就是一些我们没有想到的 undefined state,会导致我们 redis 缓存的数据不对,这些内容我们就放到下一篇来讲吧

我是Mayphyr,从一点点到亿点点,我们下次再见

猜你喜欢

转载自blog.csdn.net/aaaaaaaa273216/article/details/133208253