欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

黑马--Redis 初学者到真实世界 [真实世界

最编程 2024-05-03 15:30:08
...

文章目录

  • 一、短信登录
    • 基于session实现短信登录的流程
    • 实现发送短信验证码功能
    • 实现登录校验拦截器
    • session共享的问题分析
    • Redis代替session的业务流程
    • 基于Redis实现短信登录
    • 解决登录状态刷新的问题
  • 二、商户缓存查询
    • 什么是缓存
    • 添加Redis缓存
    • 练习:给店铺类型查询业务添加缓存
    • 缓存更新策略
    • 实现缓存与数据库的双写一致
    • 缓存穿透
    • 缓存雪崩
    • 缓存击穿
    • 利用互斥锁解决缓存击穿问题
    • 基于逻辑过期方式解决缓存击穿问题
    • 封装Redis工具类
    • 缓存总结
      • 认识缓存
      • 缓存更新策略
      • 主动更新方案
      • 最佳实践
      • 缓存穿透
      • 缓存雪崩
      • 缓存击穿(热点key)
  • 三、优惠券秒杀
    • 全局ID生成器
    • Redis实现全局唯一ID
    • 添加优惠券
    • 实现秒杀下单
    • 超卖问题
    • 乐观锁解决超卖问题
    • 实现一人一单功能
    • 集群模式下线程锁失效
  • 四、分布式锁
    • 分布式锁基本原理
    • Redis分布式锁的基本实现
    • 实现Redis分布式锁初级版本
    • Redis分布式锁误删
    • 改进Redis的分布式锁
    • 分布式锁的原子性问题
    • Lua脚本解决多条命令原子性问题
    • 再次改进Redis的分布式锁
    • 基于Redis的分布式锁实现思路:
    • Redisson功能介绍
    • Redisson快速入门
    • Redisson可重入锁原理
    • Redisson的锁重试和WatchDog机制
    • Redisson的multiLock问题
  • 五、秒杀优化
    • 异步秒杀思路
    • 基于Redis完成秒杀资格判断
    • 基于阻塞队列实现秒杀业务
  • 六、Redis消息队列
    • 认识消息队列
    • 基于List结构模拟消息队列
    • 基于PubSub的消息队列
    • 基于Stream的消息队列
    • 基于Redis的Stream结构作为消息队列,实现异步秒杀下单
  • 七、达人探店
    • 发布探店笔记
    • 实现查看发布探店笔记的接口
    • 点赞
    • 点赞排行榜
  • 八、好友关注
    • 关注和取关
    • 共同关注
    • 关注推送
      • Feed流实践方案分析
      • 基于推模式实现关注推送功能
      • 实现关注推送页面的分页查询
      • 实现滚动查询
  • 九、附近商户
    • GEO数据结构
    • 附近商户搜索
      • 导入店铺数据到GEO
    • 实现附近商户功能
  • 十、用户签到
    • BitMap用法
    • 签到功能
    • 签到统计
  • 十一、UV统计
    • HyperLogLog用法
    • 实现UV统计

在这里插入图片描述

一、短信登录

基于session实现短信登录的流程

在这里插入图片描述

实现发送短信验证码功能

在这里插入图片描述

发送验证码功能:

@Override
public Result sendCode(String phone, HttpSession session) {
    //1.校验手机号
    if(RegexUtils.isPhoneInvalid(phone)){
        //2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    //3.符合,生成验证码
    String code = RandomUtil.randomNumbers(6);
    //4.保存验证码到session
    session.setAttribute("code",code);
    //5.发送验证码
    log.debug("发送短信验证码成功,验证码:{}"+code);
    //返回ok
    return Result.ok();
}

在这里插入图片描述

登录功能:

登录表单的实体类:

@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}

登录逻辑代码实现:

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2.校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if(cacheCode==null ||! cacheCode.toString().equals(code)){
            //3.不一致,报错
            return Result.fail("验证码错误!");
        }
        //4.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        //5.判断用户是否存在
        if(user==null) {
            //6.不存在,创建新用户并保存
            user=createUsrWithPhone(phone);
        }
        //7.保存用户信息到session中
        session.setAttribute("user",user);
        return null;   
    }
 
    private User createUsrWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        //2.保存用户
        save(user);
        return user;
    }

实现登录校验拦截器

ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

注意,为了隐藏用户敏感信息,也为了节省ThreadLocal的空间,需要将User转为UserDTO返回给前端。

可以通过hutool工具类,在UserService里修改:

session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
    public static void saveUser(UserDTO user){
        tl.set(user);
    } 
    public static UserDTO getUser(){
        return tl.get();
    }
    public static void removeUser(){
        tl.remove();
    }
}

拦截器:

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session
        HttpSession session = request.getSession();
        //2.获取session中的用户
        Object user = session.getAttribute("user");
        //3.判断用户是否存在
        if(user==null) {
            //4.不存在,拦截,返回401状态码,代表未授权
            response.setStatus(401);
            return false;
        }
        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        //6.放行
        return true;
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

添加拦截器:

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**","/upload/**");
    }
}

session共享的问题分析

session共享问题:多态Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题
在这里插入图片描述

session的替代方案应该满足:

数据共享
内存存储
key、value结构
===>redis

Redis代替session的业务流程

在这里插入图片描述

在这里插入图片描述

基于Redis实现短信登录

拦截器修改:

public class LoginInterceptor implements HandlerInterceptor {
 
    private StringRedisTemplate stringRedisTemplate;
 
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取请求头中的token
        String token = request.getHeader("authorization");
        //判断token是否为空
        if(StrUtil.isBlank(token)){
            response.setStatus(401);
            return false;
        }
        String key=LOGIN_USER_KEY+token;
        //2.基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if(userMap.isEmpty()){
            //不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        //5.存在,将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //6.保存用户到ThreadLocal
        UserHolder.saveUser(userDTO);
        //7.刷新token有效期
        stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
        //8.放行
        return true;
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

MvcConfig修改:

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

UserServiceImpl修改:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
 
    @Resource
    private StringRedisTemplate stringRedisTemplate;
 
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到redis中
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //5.发送验证码
        log.debug("发送短信验证码成功,验证码:{}"+code);
        //返回ok
        return Result.ok();
    }
 
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2.校验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if(cacheCode==null ||!cacheCode.equals(code)){
            //3.不一致,报错
            return Result.fail("验证码错误!");
        }
        //4.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        //5.判断用户是否存在
        if(user==null) {
            //6.不存在,创建新用户并保存
            user=createUsrWithPhone(phone);
        }
        //保存用户信息到redis中
        //1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);  //true代表isSimple,即不带中划线
        //2.将User对象转为Hash存储
        UserDTO userDTO=BeanUtil.copyProperties(user,UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
        String tokenKey=LOGIN_USER_KEY+token;
        //7.存储
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        //设置token有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok();
    }
 
    private User createUsrWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        //2.保存用户
        save(user);
        return user;
    }
}

Redis代替session需要考虑的问题:

选择合适的数据结构

选择合适的key

选择合适的存储粒度

解决登录状态刷新的问题

在这里插入图片描述
RefreshTokenInterceptor

public class RefreshTokenInterceptor implements HandlerInterceptor {
 
    private  StringRedisTemplate stringRedisTemplate;
 
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate=stringRedisTemplate;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 
        //1.获取请求头中的token
        String token = request.getHeader("authorization");
        //判断token是否为空
        if(StrUtil.isBlank(token)){
            return true;
        }
        String key=LOGIN_USER_KEY+token;
        //2.基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if(userMap.isEmpty()){
            return true;
        }
        //5.存在,将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //6.保存用户到ThreadLocal
        UserHolder.saveUser(userDTO);
        //7.刷新token有效期
        stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
        //8.放行
        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<					

推荐阅读