Redis practical notes

Redis practical notes



Preface

This note is based on the actual combat chapter in Dark Horse's Redis video. It not only contains ppt pictures from the class, but also my own understanding of the course content, and includes all source codes.

mind Mapping


Login and registration function of SMS verification code

Login based on Session


Implement the function of sending SMS verification code

@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();
}

Implement SMS verification code login function

    @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();
    }

Implement the function of verifying login status (login interceptor)

IDEA View interface method shortcut key Ctrl+I

Friends who are not familiar with ThreadLocal can read this article 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/**"
                );
    }
}

Problems with login based on Session

There is a session sharing problem: multiple Tomcats do not share sessions, causing data loss when the request is switched to different tomcat services
The following is the architecture diagram of the login function just now. If The user logged in on the first tomcat, and then the requested service was sent to the second service. There is no login information on the session on the second
tomcat, which will cause the request to fail

Then if you want to realize session sharing, the following conditions must be met

  1. data sharing
  2. memory storage
  3. key, value structure

Then use redis to solve this problem

Implement shared session login based on Redis


We store objects in Redis here, using a Hash structure instead of json, because the Hash structure can store each field in the object independently, and can perform CRUD on a single field, which takes up less memory.
In fact, it can also be saved as a JSON string, which is more intuitive. Here, a Hash structure is used to store it.

Here, random tokens are used as keys to store object data, also to prevent someone from using the phone to obtain user information.

    @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();
    }

Key-value pairs in 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();
}

Key-value pairs in Redis
image.png

Optimization of login interceptor

Although the expiration time is set when the user information is saved in Redis, this time is when the user information is saved to redis. As long as 30 minutes have passed, the user information will be deleted. But what we want is to delete it when the user has not visited for more than 30 minutes. So we need to optimize the interceptor.

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();
    }
}

Merchant query cache

What is caching?
Cache is the cache area for data exchange. It is a temporary place to store data. Generally, it has high read and write performance.
Caching that can be added in web development
The role of cache

  1. Reduce back-end load (reduce database queries, reduce database pressure)
  2. Improve reading and writing efficiency and reduce response time

The cost of caching

  1. Data consistency cost (when the data in the database changes, the previously cached data in redis is still the old data)
  2. Code maintenance cost (in order to solve problems such as consistency, cache breakdown, cache avalanche, etc., code complexity will be increased)
  3. Operation and maintenance costs

    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);
    }

Small job: cache store types into 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

Summarize

This article briefly explains some practical scenarios of Redis in application development, but there are still many scenario bugs, that is, some undefined states that we did not expect, which will cause the data cached by our redis to be incorrect. These contents Let’s leave it to the next article.

I'm Mayphyr, from a little bit to a billion points, see you next time

Guess you like

Origin blog.csdn.net/aaaaaaaa273216/article/details/133208253