Redis实战

短信登录功能

发送短信验证码实现流程

  1. 提交手机号
  2. 校验手机号
  3. 生成验证码,并保存
  4. 保存验证码到redis
  5. 发送验证码
    @Override
    public Result sendCode(String phone) {
        //校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail(UserErrorConstant.PHONE_FORMAT_ERROR);
        }
        //生成验证码
        String code = RandomUtil.randomNumbers(6);

        //保存验证码(有效期:5min)
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //发送验证码
        log.debug("发送短信验证码成功,验证码:{}",code);

        return Result.ok();
    }

短信验证码登录注册实现流程

  1. 提交手机号和验证码
  2. 验证验证码
  3. 根据手机号查询用户
  4. 用户是否存在(如果不存在创建新用户并保存用户到数据库)
  5. 保存用户到redis
    @Override
    public Result loginByCode(LoginFormDTO loginFormDTO) {
        //校验手机号
        String phone = loginFormDTO.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail(UserErrorConstant.PHONE_FORMAT_ERROR);
        }

        //从redis获取验证码并验证
        String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY+phone);
        String code = loginFormDTO.getCode();
        if(cacheCode==null||!cacheCode.equals(code)){
            return Result.fail(UserErrorConstant.VERIFICATION_CODE_ERROR);
        }

        //根据手机号查询用户
        User user = query().eq("mobile",phone).one();

        //判断用户是否存在
        if(user==null){
            user = createUserWithPhone(phone);
        }

        //使用jwt令牌
        String token = UUID.randomUUID().toString();

        //将user对象转成HashMap存储(由于使用的是stringRedisTemplate,所以要把id转为string)
        UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
        userDTO.setToken(token);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue == null ? "" : fieldValue.toString()));

        stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY+token,userMap);
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL,TimeUnit.HOURS);

        //返回token
        return Result.ok(userDTO);
    }

校验登录状态实现流程

  1. 请求并携带cookie
  2. 从redis获取用户
  3. 判断用户是否存在(如果不存在则拦截)
  4. 如果存在则保存用户到threadlocal
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       
        //从请求头中获取token
        String token = request.getHeader("authorization");
        if (StringUtils.isEmpty(token)) {
            //不存在token
            return true;
        }
        //从redis中获取用户
        Map<Object, Object> userMap =
                stringRedisTemplate.opsForHash()
                        .entries(RedisConstants.LOGIN_USER_KEY + token);
        //用户不存在
        if (userMap.isEmpty()) {
            return true;
        }
        //hash转UserDTO存入ThreadLocal
        UserHolder.saveUser(BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false));
        //token续命
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

缓存功能

缓存就是数据交换的缓冲区(Cache),是存储数据的临时地方,一般读写性能较高。

作用:

  • 降低后端负载。
  • 提高读写效率,降低响应时间。(解决高并发问题)

成本:

  • 数据一致性成本
  • 代码维护成本(缓存击穿)
  • 运维成本

缓存更新策略

内存淘汰:利用redis的内存淘汰机制,内存不足时自动淘汰部分数据。

超时剔除:添加ttl时间,到期自动删除缓存(兜底)。

主动更新:在修改数据库的同时,更新缓存。

  1. 方案1:由缓存的调用者,在更新数据库的同时更新缓存。
  2. 方案2:缓存与数据库整合为一个服务,由服务来维护一致性。
  3. 方案3:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致。
如何保证缓存与数据库的操作的同时成功或失败(原子操作问题)?
  • 单体系统,将缓存与数据库操作放在一个事务。
  • 分布式系统,利用tcc等分布式事务方案。
先操作缓存还是先操作数据库(线程安全)?

先操作数据库再删除缓存的操作更保险,因为数据库的操作时间一般要比缓存慢。

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会达到数据库。

缓存空对象(被动方案)

优点:实现简单,维护方便

缺点:额外的内存消耗(缓存垃圾,设置ttl删除)、可能造成短期的不一致(插入时更新缓存)

布隆过滤(被动方案)

在客户端和redis之间加入布隆过滤器,如果不存在则拒绝,存在则放行。(用byte数组存储,hashcode二进制hash对应位置,若为1存在,若为0不存在)

优点:内存占用少,没有多余key

缺点:实现复杂,存在误判可能

主动方案
  • 增加id的复杂度,避免被猜测id规律(雪花算法)
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流
    /**
     * 缓存穿透
     * @param id
     * @return
     */
    public Result queryWithPassThrough(int id){
        String key = RedisConstants.CACHE_SHOP_KEY+id;
        //从redis查询企业用户信息缓存
        String userJson = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if(StrUtil.isNotBlank(userJson)){
            User user = JSONUtil.toBean(userJson,User.class);
            return Result.ok(user);
        }
        //判断命中的是否是空值
        if(userJson!=null){
            //返回错误信息
            return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
        }

        //不存在,根据id查询数据库
        User user = userMapper.queryUserById(id);
        //不存在,返回错误
        if(user==null){
            //缓存穿透-》将空值写入redis
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
        }
        //存储到redis中,设置了超时时间
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);

        return Result.ok(user);
    }

缓存雪崩

缓存雪崩是指同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案
  • 给不同的key的ttl添加随机值(同一时间段大量key失效,有时候在做缓存的预热时会在同一时间批量的数据导入)
  • 利用redis集群提高服务的可用性(主宕机,从结点上还有数据)
  • 给缓存业务添加降级限流策略(提前做好服务降级,比如快速失败拒绝服务)
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。(无数请求缓存重建)

解决方案

互斥锁

未命中需要获取锁才能查询数据库重建缓存数据,写入缓存后再释放锁。

优点:没有额外内存消耗,保证数据的一致性,实现简单。

缺点:线程需要等待,性能受影响,可能有死锁风险。

    /**
     * 缓存击穿->互斥锁
     * @param id
     * @return
     */
    public Result queryWithPassMutex(int id)  {
        String key = RedisConstants.CACHE_SHOP_KEY+id;
        //从redis查询企业用户信息缓存
        String userJson = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if(StrUtil.isNotBlank(userJson)){
            User user = JSONUtil.toBean(userJson,User.class);
            return Result.ok(user);
        }
        //判断命中的是否是空值
        if(userJson!=null){
            //返回错误信息
            return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
        }

        //缓存重建
        String lockKey = RedisConstants.LOCK_SHOP_KEY+id;
        User user =null;
        try {
            boolean isLock = tryLock(lockKey);
            if(!isLock){
                Thread.sleep(50);
                //风险!改成while(true)轮询就好
                return queryWithPassMutex(id);
            }
            //不存在,根据id查询数据库
            user = userMapper.queryUserById(id);
            //不存在,返回错误
            if(user==null){
                //缓存穿透-》将空值写入redis
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
            }
            //存储到redis中,设置了超时时间
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);
        } catch (InterruptedException e){
            throw new RuntimeException(e);
        } finally {
            //释放互斥锁
            unlock(lockKey);
        }

        return Result.ok(user);
    }
逻辑过期

发现逻辑时间已过期,未命中获取互斥锁,返回过期数据,开启新线程去做缓存重建并释放锁,如果有新线程来的时候还是会返回新数据。

优点:线程无需等待,性能较好。

缺点:不保证一致性,有额外内存消耗,实现复杂。

    /**
     * 缓存穿透
     * @param id
     * @return
     */
    public Result queryWithPassThrough(int id){
        String key = RedisConstants.CACHE_SHOP_KEY+id;
        //从redis查询企业用户信息缓存
        String userJson = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if(StrUtil.isNotBlank(userJson)){
            User user = JSONUtil.toBean(userJson,User.class);
            return Result.ok(user);
        }
        //判断命中的是否是空值
        if(userJson!=null){
            //返回错误信息
            return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
        }

        //不存在,根据id查询数据库
        User user = userMapper.queryUserById(id);
        //不存在,返回错误
        if(user==null){
            //缓存穿透-》将空值写入redis
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return Result.fail(UserErrorConstant.USER_NOEXIST_ERROR);
        }
        //存储到redis中,设置了超时时间
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),RedisConstants.CACHE_SHOP_TTL,TimeUnit.HOURS);

        return Result.ok(user);
    }

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值