LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

MarkDown语法

2022/3/25

Markdown快速入门(typora)

1、代码块:

//代码块语法:
```java
```c++
①. java代码
package com.pcy.po;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Entity
@Table(name = "t_user")
public class User{
    
}
②. c++代码
#include <cstdio>
#include <cstring>
char s[1005];
int main(){
    while(scanf("%s",s) != EOF);
    cout << (int)strlen(s);
    return 0;
}

2、标题:

//标题语法
 # 一级标签
 ## 二级标签
 ### 三级标签
 #### 四级标签
 ##### 五级标签
 ###### 六级标签

一级标签

二级标签

三级标签

四级标签

五级标签
六级标签

3、字体:

//斜体
*第一次写博客*
//加粗
**第一次写博客**
//代码高亮显示
==第一次写博客==
//删除线
~~第一次写博客~~
//下划线
<u>第一次写博客</u>
//脚注
第一次写博客[^Asuna]

第一次写博客
第一次写博客
==第一次写博客==
第一次写博客
第一次写博客

第一次写博客[^Asuna]

4、引用

//引用语法
>作者:Kirito
>>作者:Kirito
>>>作者:Kirito
+
-

作者:Kirito

作者:Kirito

作者:Kirito

5、分割线:

//分割线
---
//分割线2
***


6、图片插入

//在线图片 || 本地图片
![我的照片](/image/XXX.png 或 用图床上传至GitHub 然后右键复制图片链接 再从浏览器中打开 再右键复制图像链接) --图片路径
[敲重点]Markdown可以通过普通的<img>标签来指定图片的 高度 和 宽度
<img src = "https://raw.githubusercontent.com/P-luminary/image/master/data/avatar.png" width = "50%">

我的照片

7、超链接

//超链接语法
[P-luminary GitHub](https://github.com/Luminarness)

8、列表

//无序列表 ("-"是实心圈  "+"是空心圈)
- 目录1
- 目录2
- 目录3
//有序列表
数字键 + "." + 名称
//列表嵌套(需要在子列表中的选项前面添加四个空格)
1. 第一项
    - 第一项嵌套的第一个元素
2. 第二项
    - 第二项嵌套的第二个元素
  • 目录1

  • 目录2

  • 目录3

    1. 首页

    2. 分类

    3. 标签

    4. 第一项

      • 第一项嵌套的第一个元素
    5. 第二项

      • 第二项嵌套的第二个元素

9、表格

在Typora中右键 -> 插入 -> 表格
日期 心情 饱和度
2022-03-25 1:27 非常好
阅读全文

PTA技巧

2022/3/25

时间优化:(写好的代码有测试点运行超时, 可以暂时放下, 不要浪费时间)

  1. 如果出现运行超时考虑是否因为非法输入导致死循环

  2. 尽量创建全局变量(创建一次,后面赋值),不要重复创建临时变量

  3. 尽量使用scanf,printf,而不使用cin,cout (C++的IO存在缓存(可取消))

#include <bits/stdc++.h> //万能头
using namespace std;
→→→    ios::sync_with_stdio(false); ←←←
→→→    cin.tie(0);cout.tie(0);    ←←←
  1. 使用引用访问容器中的元素(遍历,排序==>增强型for循环)
vector<int> vc{0,1,2,3};
for(int& i : vc)
    cout << i;
  1. 使用unordered_map,unordered_set替换map,set可以提升效率(注意改头文件)

  2. 为了实现排序功能,尽量使用sort对数组排序,而不要依赖与map和set的自动排序(map,set结构庞大)

  3. 一般递归算法都比较慢,深搜(DFS)可以通过剪枝优化,并查集可以进行路径压缩提升效率。


时间优化:

  1. 为了方便函数调用,可以都用全局变量(但这是个坏习惯)

  2. 不确定数据多少的情况,不一定非要用动态数组,可以直接根据题目给的数据范围定义一个较大的数组(大小应大于题目所给的范围),一般情况下不会出现内存超限 [数组大小别超过10^8,二维数组不能超过array(10000)(10000)]

  3. 答案错误很有可能是漏了题目的关键信息

  4. 格式错误是因为排版跟题目要求不同,可能多空格或空行

  5. 段错误是因为非法访问才会导致,一般情况下是因为数组访问越界

  6. 涉及到除法的要考虑除数不能为0,一般会有一个测试点

  7. 图论问题用深搜可以得到大部分的分

  8. 选取适当的结构(容器)可以让思路更清晰

  9. 熟悉编译器的调试功能(Debug)可以更快找出bug


必备知识:

​ 数据结构与算法:

  1. 树:二叉树的遍历方式,平衡二叉树的建树过程,根据两种遍历来建树

  2. 图:深搜,广(层)搜,并查集(推荐博客)

  3. 堆:堆结构,建堆过程(堆排序)。

  4. 链表:根据结点连接链表(一般通过结构体进行模拟)。

  5. 排序:熟悉快排和归并排序的排序过程

常用库、函数(黑科技)

容器、字符串处理、lambda表达式、自定义排序、类型转换、数值边界、数学函数、auto关键字、堆、常用功能函数(查找 计数 倒序)
知识点 说明 推荐博客
string容器 封装了一些对字符串的常用操作 C++ String详解
regex正则表达式 使用正则表达式来替换、查找字符串 C++ 正则表达式
stirngstream字符串IO类 使用字符串来进行IO操作 C++ stringstream
常用字符处理函数 isdigit(char ch)
  1. sort函数排序,自定义结构体比较方式

  2. STL标准库list,vector,queue,stack,map,set

  3. 数组(容器)倒序

    1. 倒序函数: void reverse(typename begin, typename end);
    2. 利用反向迭代器倒序(构造new string)
  4. auto关键字

  5. find函数

string str; // string类函数:未找到返回string::npos
if(str.find("substr") != string::npos);
    //......
set<int> st;
if(st.find(0) != st.end()); //set、map成员函数: 未找到返回尾迭代器
    //......
vector<int> vc;
if(find(vc.begin(),vc.end(),0) != vc.end()); //序列式容器可以通过algorithm库中的find
    //......
阅读全文

Redis实战

2022/1/15

黑马点评Redis

实现短信的登录和注册

com/hmdp/controller/UserController.java
package com.hmdp.controller;


import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;

import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.query;


/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @Resource
    private IUserInfoService userInfoService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        String phone = loginForm.getPhone();
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误!");
        }
        // 校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();

        // 不一致 报错
        if (cacheCode == null || !code.equals(cacheCode)) {
            return Result.fail("验证码错误");
        }

        // 一致 根据手机号查询用户 用mybatisplus
        User user = userService.getOne(lambdaQuery(User.class).eq(User::getPhone, phone));
        // 判断用户是否存在
        if (ObjectUtil.isEmpty(user)) {
            // 不存在 创建新用户保存
            user = new User();
            user.setPhone(phone);
            user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
            userService.save(user);
        }
        // 存在 保存用户信息到session当中
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setNickName(user.getNickName());
        userDTO.setPhone(user.getPhone());
        session.setAttribute("user", userDTO); // 确保保存的是 UserDTO 对象
        return Result.ok();
    }

    /**
     * 登出功能
     * @return 无
     */
    @PostMapping("/logout")
    public Result logout(){
        // TODO 实现登出功能
        return Result.fail("功能未完成");
    }

    @GetMapping("/me")
    public Result me(){
        // TODO 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

    @GetMapping("/info/{id}")
    public Result info(@PathVariable("id") Long userId){
        // 查询详情
        UserInfo info = userInfoService.getById(userId);
        if (info == null) {
            // 没有详情,应该是第一次查看详情
            return Result.ok();
        }
        info.setCreateTime(null);
        info.setUpdateTime(null);
        // 返回
        return Result.ok(info);
    }
}
com/hmdp/service/impl/UserServiceImpl.java
package com.hmdp.service.impl;

import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

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

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        return null;
    }
}

实现登录校验拦截器

package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login"
                );
    }
}
package com.hmdp.utils;

import com.hmdp.dto.UserDTO;

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();
    }
}
package com.hmdp.utils;

import cn.hutool.core.util.ObjectUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取session
        HttpSession session = request.getSession();
        // 获取session中的用户
        Object user = session.getAttribute("user");
        // 判断用户是否存在
        if (ObjectUtil.isEmpty(user)) {
            // 不存在,拦截
            response.setStatus(401);
            return false;
        }
        // 存在,保存用户信息到ThreadLocal
        UserDTO userDTO = (UserDTO) user; // 确保 user 已经是 UserDTO 类型
        UserHolder.saveUser(userDTO);
        // 放行
        return true;
    }

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

集群的Session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

session的替代方案应该满足:数据共享、内存存储、key、value结构

发送短信验证码后生成的验证码 保存验证码到Redis → 以手机号作为Key存储验证码 → 发送验证码

com/hmdp/controller/UserController.java
package com.hmdp.controller;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.query;


/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @Resource
    private IUserInfoService userInfoService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        String phone = loginForm.getPhone();
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误!");
        }
        // 校验验证码
//        Object cacheCode = session.getAttribute("code");
        // 从Redis中获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(SystemConstants.LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();

        // 不一致 报错
        if (cacheCode == null || !code.equals(cacheCode)) {
            return Result.fail("验证码错误");
        }

        // 存储

        // 一致 根据手机号查询用户 用mybatisplus
        User user = userService.getOne(lambdaQuery(User.class).eq(User::getPhone, phone));
        // 判断用户是否存在
        if (ObjectUtil.isEmpty(user)) {
            // 不存在 创建新用户保存
            user = new User();
            user.setPhone(phone);
            user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
            userService.save(user);
        }
        // 保存用户信息到redis中
        // 随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 将User对象转换为Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // 存储  用了putall 要把userDto转map
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 存储   因为存的时候不能设置有效期 要存完以后再去设置有效期
        String tokenKey = SystemConstants.LOGIN_CODE_TOKEN + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 设置token有效期 30min
        stringRedisTemplate.expire(tokenKey, SystemConstants.REDIS_TIMEOUT, TimeUnit.MINUTES);
        // 但是我要如果状态在 就不断更新token的有效期 【更新token有效期】



        // 存在 保存用户信息到session当中
//        UserDTO userDTO = new UserDTO();
//        userDTO.setId(user.getId());
//        userDTO.setNickName(user.getNickName());
//        userDTO.setPhone(user.getPhone());
        // 确保保存的是 UserDTO 对象
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
    }

    /**
     * 登出功能
     * @return 无
     */
    @PostMapping("/logout")
    public Result logout(){
        // TODO 实现登出功能
        return Result.fail("功能未完成");
    }

    @GetMapping("/me")
    public Result me(){
        // TODO 获取当前登录的用户并返回
        // userService.login(loginForm, session)
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

    @GetMapping("/info/{id}")
    public Result info(@PathVariable("id") Long userId){
        // 查询详情
        UserInfo info = userInfoService.getById(userId);
        if (info == null) {
            // 没有详情,应该是第一次查看详情
            return Result.ok();
        }
        info.setCreateTime(null);
        info.setUpdateTime(null);
        // 返回
        return Result.ok(info);
    }
}

解决状态登录刷新的问题【双重拦截器】

请求 → 拦截器①(拦截一切路径)[流程:获取token、查询Redis用户、保存到ThreadLocal、刷新Token有效期、放行] → 拦截器②(拦截需要登录的路径)[查询ThreadLocal的用户, 不存在则拦截、存在则继续]

com/hmdp/utils/LoginInterceptor.java
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class LoginInterceptor implements HandlerInterceptor {
    // 这个类的对象是手动new出来的 没有通过Spring容器 所以需要通过构造方法去引用
    // 拦截器不能给spring容器因为每次都会经过它去访问

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户 放行
        return true;
    }
}
com/hmdp/utils/RefreshTokenInterceptor.java
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class RefreshTokenInterceptor implements HandlerInterceptor {
    // 这个类的对象是手动new出来的 没有通过Spring容器 所以需要通过构造方法去引用
    // 拦截器不能给spring容器因为每次都会经过它去访问
    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 不存在:拦截,返回401状态码
//            response.setStatus(401);
            return true;
        }
        String key = SystemConstants.LOGIN_CODE_TOKEN + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 判断用户是否存在
//        if (ObjectUtil.isEmpty(user)) {
        if (userMap.isEmpty()){
            // 不存在,拦截
//            response.setStatus(401);
            return true;
        }
        // 将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 存在,保存用户信息到ThreadLocal
//        UserDTO userDTO = (UserDTO) user; // 确保 user 已经是 UserDTO 类型
        UserHolder.saveUser(userDTO);
        // 刷新token有效期
        stringRedisTemplate.expire(key, SystemConstants.REDIS_TIMEOUT, TimeUnit.MINUTES);
        // 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
com/hmdp/config/MvcConfig.java
package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

商户查询缓存—练习题

com/hmdp/controller/ShopTypeController.java
/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
    @Resource
    private IShopTypeService typeService;

    @GetMapping("list")
    public Result queryTypeList() {
//        List<ShopType> typeList = typeService
//                .query().orderByAsc("sort").list();
//        return Result.ok(typeList);
        return typeService.listShop();
    }
com/hmdp/service/impl/ShopTypeServiceImpl.java
/**
 * <p>
 *  服务实现类
 * </p>
 * @since 2021-12-22
 */
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result listShop() {
        String shopTypeCache = stringRedisTemplate.opsForValue().get(SystemConstants.CACHE_SHOP_TYPE_KEY);

        if (ObjectUtil.isNotEmpty(shopTypeCache)) {
            Long ttl = stringRedisTemplate.getExpire(SystemConstants.CACHE_SHOP_TYPE_KEY, TimeUnit.MINUTES);
            System.out.println("TTL for CACHE_SHOP_TYPE_KEY: " + ttl + " minutes");
            List<ShopType> shopTypeList = JSONUtil.toList(shopTypeCache, ShopType.class);
            return Result.ok(shopTypeList);
        }



        List<ShopType> queryShopTypeList = query().orderByAsc("sort").list();
        if (ObjectUtil.isEmpty(queryShopTypeList)) {
            return Result.fail("查询失败");
        }
        stringRedisTemplate.opsForValue().set(SystemConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(queryShopTypeList), SystemConstants.SHOP_TYPE_TTL, TimeUnit.MINUTES);
        return Result.ok(queryShopTypeList);
    }
}

添加商铺缓存

com/hmdp/service/impl/ShopServiceImpl.java
package com.hmdp.service.impl;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.SystemConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(SystemConstants.CACHE_SHOP_KEY + id);
        // 判断是否存在
        if (ObjectUtil.isNotEmpty(shopJson)) {
            // 存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 不存在,根据id查询数据库
        Shop shop = getById(id);
        // 不存在,返回错误
        if (ObjectUtil.isEmpty(shop)) {
            return Result.fail("店铺不存在");
        }
        // 存在,写入redis
        stringRedisTemplate.opsForValue().set(SystemConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop));
        // 返回
        return Result.ok(shop);
    }
}

实现商铺缓存与数据库的双写一致性

先更新后删除
com/hmdp/controller/ShopController.java
 /**
     * 更新商铺信息
     * @param shop 商铺数据
     * @return 无
     */
    @PutMapping
    public Result updateShop(@RequestBody Shop shop) {
//        shopService.updateById(shop);
        return shopService.update(shop);

    }
com/hmdp/service/impl/ShopServiceImpl.java
@Override
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (ObjectUtil.isEmpty(id)) {
            return Result.fail("店铺id不能为空");
        }
        // 更新数据库
        updateById(shop);
        // 删除缓存
        stringRedisTemplate.delete(SystemConstants.CACHE_SHOP_KEY + id);
        return Result.ok();
    }

缓存穿透的解决思路

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点
      • 额外的内存消耗
      • 可能造成短期的不一致

    缓存null的时候加一个TTL

  • 布隆过滤器算法[bitMap数组用Hash值去判断]
    • 优点:内存占用较少,没有多余key
    • 缺点
      • 实现复杂
      • 存在误判可能

缓存击穿的解决思路

  • 互斥锁
  • 逻辑过期

缓存雪崩的解决思路

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提高服务的可用性

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存

利用互斥锁解决缓存击穿问题setnx设置一把锁

setnx当key不存在的时候才会写入赋值
del lock释放锁
setnx lock 获取锁赋值

在linux中如果用的docker那么就用它
docker连接redis
首先先找到redis的镜像编码
docker ps
找到以后
docker exec -it 5832be55766e redis-cli

[root@localhost ~]# docker exec -it 5832be55766e redis-cli
127.0.0.1:6379> AUTH pass
OK
127.0.0.1:6379> setnx lock 1
(integer) 1
127.0.0.1:6379> get lock
"1"
127.0.0.1:6379> setnx lock 2
(integer) 0
127.0.0.1:6379> setnx lock 3
(integer) 0
127.0.0.1:6379> setnx lock 2
(integer) 0
127.0.0.1:6379> get lock
"1"
127.0.0.1:6379> del lock
(integer) 1
127.0.0.1:6379> setnx lock 3
(integer) 1
    
--------------------------------------------------------------------------
// 加个有效期TTL 避免锁得不到释放 产生死锁
 private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 防止自动拆箱产生空值
        return BooleanUtil.isTrue(flag);
    }

// 释放锁
private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

SETNX 是 Redis 的一个命令,它是 “SET if Not eXists” 的缩写,用于在键不存在时设置键的值。如果键已经存在,SETNX 命令不会执行任何操作,并返回 0;如果键不存在,它会设置键的值,并返回 1

在分布式锁的实现中,SETNX 命令经常被用来尝试获取锁。如果某个键(通常用作锁的标识)不存在,则设置该键并获取锁;如果键已经存在,表示锁已经被其他客户端持有,因此当前客户端无法获取锁。

以下是 SETNX 命令的基本用法:

SETNX key value
  • key:你想要设置的键名。
  • value:你想要设置的值。

从 Redis 2.6.12 版本开始,SETNX 命令被 SET 命令的一个选项所取代,该选项允许你执行类似于 SETNX 的操作,同时还能设置键的超时时间(过期时间),这在实现分布式锁时非常有用。以下是使用 SET 命令的示例:

SET key value [EX seconds] [PX milliseconds] [NX|XX]
  • EX seconds:设置键的过期时间为 seconds 秒。
  • PX milliseconds:设置键的过期时间为 milliseconds 毫秒。
  • NX:只在键不存在时执行设置操作。
  • XX:只在键已经存在时执行设置操作。

因此,现代的分布式锁实现通常会使用如下命令:

SET lock_key unique_value EX 30 NX

这里,lock_key 是锁的键名,unique_value 是一个确保释放锁时安全的唯一值(通常是当前时间戳加上一个随机数或者客户端ID),EX 30 设置了键的超时时间为30秒,NX 确保只有在键不存在时才设置键。

互斥锁的实现

互斥锁通常用于单机应用,确保同一时刻只有一个线程可以执行某个操作。以下是一个简单的示例:

java

复制

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

public class MutexLock {
    private final StringRedisTemplate redisTemplate;
    private final String lockKey = "mutex_lock_key";

    public MutexLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryLock() {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        // 尝试设置锁,如果设置成功返回true,否则返回false
        return ops.setIfAbsent(lockKey, "locked");
    }

    public void unlock() {
        // 删除锁
        redisTemplate.delete(lockKey);
    }
}

分布式锁的实现

分布式锁用于分布式系统,确保在多个不同的机器或服务中只有一个可以执行某个操作。以下是一个简单的示例:

java

复制

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

public class DistributedLock {
    private final StringRedisTemplate redisTemplate;
    private final String lockKey = "distributed_lock_key";
    private final long lockTimeout = 30000; // 锁超时时间,例如30秒

    public DistributedLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryLock() {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        // 尝试设置锁,并设置超时时间
        Boolean success = ops.setIfAbsent(lockKey, "locked", lockTimeout, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(success);
    }

    public void unlock() {
        // 删除锁
        redisTemplate.delete(lockKey);
    }
}

基于逻辑过期方式解决缓存击穿的问题

不要直接在类中添加逻辑过期的字段,这样对代码不好
① 搞一个RedisData.java 设置一个逻辑过期 然后再去实现继承
② RedisData中搞一个private Object data;
热点数据需要提前导入

封装Redis工具类

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击
穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

Redis总结

优惠券秒杀—全局唯一ID

全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

Redis自增ID策略:

  • 每天一个Key,方便统计订单量
  • ID构造是 时间戳 + 计数器

添加优惠券秒杀针对特价券

每个店铺都可以发布优惠券,分为平价券特价券

平价券可以任意购买,而特价券需要秒杀抢购:

  • tb_voucher:优惠券的基本信息,优惠金额、使用规则等
  • tb_seckill_voucher优惠券的库存开始抢购时间结束抢购时间。特价优惠券才需要填写这些信息
[root@localhost ~]# docker ps
CONTAINER ID   IMAGE                         COMMAND                  CREATED         STATUS             PORTS                                                                                                                                NAMES
03154afad287   seataio/seata-server:1.4.2    "java -Djava.securit…"   11 months ago   Up About an hour   8091/tcp, 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp                                                                                  seata-server
c34a590a5649   xuxueli/xxl-job-admin:2.1.2   "sh -c 'java -jar /a…"   11 months ago   Up About an hour   0.0.0.0:8280->8080/tcp, :::8280->8080/tcp                                                                                            xxl-job-admin
3574b316b5d2   nacos/nacos-server:1.4.0      "bin/docker-startup.…"   11 months ago   Up About an hour   0.0.0.0:8848->8848/tcp, :::8848->8848/tcp                                                                                            nacos
951e83eb8120   rabbitmq:3.8.3-management     "docker-entrypoint.s…"   11 months ago   Up About an hour   4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 15671/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp   rabbitmq
6561e8458fa2   influxdb:1.8.0                "/entrypoint.sh infl…"   11 months ago   Up About an hour   0.0.0.0:8086->8086/tcp, :::8086->8086/tcp, 0.0.0.0:8088->8088/tcp, :::8088->8088/tcp, 0.0.0.0:9083->8083/tcp, :::9083->8083/tcp      influxdb
5832be55766e   redis:5.0.0                   "docker-entrypoint.s…"   11 months ago   Up About an hour   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp                                                                                            redis
07ecd8b04853   mysql:5.7                     "docker-entrypoint.s…"   11 months ago   Up About an hour   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp                                                                                 mysql
[root@localhost ~]# docker exec -it 5832be55766e redis-cli
127.0.0.1:6379> AUTH pass

库存超卖问题分析每秒上百上千的并发

超卖问题是经典的多线程安全问题,针对这一问题的常见解决方案就是加锁:

秒杀 → 一人一单拒绝黄牛

要求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

集群下线程的并发安全问题

分布式锁 — 基本原理

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

MySQL Redis Zookeeper
互斥 利用mysql本身的互斥锁机制 利用setnx这样的互斥命令 利用节点的唯一性和有序性实现互斥
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开连接自动释放

基于Redis的分布式锁

实现分布式锁时需要实现的两个基本方法:获取锁、释放锁

改进Redis的分布式锁

Redis的事务:可以使用看门狗

Redis的Lua脚本利用Lua去调用Redis(确保原子的一致性)

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html

redis.call('命令名称', 'key', '其他参数', ...)

例如我们要执行set name jack,则脚本是这样:
redis.call('set', 'name', 'jack')

例如我们要先执行set name Rose,再执行get name,则脚本如下:

# 先执行 set name jack
redis.call('set', 'name', 'jack')

# 再执行 get name
local name = redis.call('get', 'name')

# 返回
return name
执行Lua脚本

写好脚本后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

127.0.0.1:6379> help @scripting
EVAL script numkeys key [key ...] arg [arg ...]

要执行redis.call('set', 'name', 'jack')这个脚本,语法如下:

# 调用脚本 [0是脚本需要的key类型的参数个数 => 传参的变量]
EVAL "return redis.call('set', 'name', 'jack')"  0
# 不带变量
127.0.0.1:6379> EVAL "return redis.call('set', 'name', 'Jack')" 0
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"Jack"

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:【lua语言数组脚标是从1开始的

# 调用脚本【初始版】
127.0.0.1:6379> EVAL "return redis.call('set', 'name', 'Jack')" 0
# 调用脚本【进阶版】
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose
OK
127.0.0.1:6379> get name
"Rose"
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name heihei
OK
127.0.0.1:6379> get name
"heihei"

基于Redis的分布式锁

释放锁的业务流程是这样的:

1.获取锁中的线程标示
2.判断是否与指定的标示 (当前线程标示) 一致
3.如果一致则释放锁 (删除)
4.如果不一致则什么都不做

-- 锁的key
-- local key = "lock:order:5"
-- 不能写死就传参
local threadId = KEYS[1]

-- 当前线程标示
-- local threadId = "fagsidajkldw-33"
-- 不能写死就传参
local threadId = ARGV[1]

-- 获取锁中的线程标识 get key
local id = redis.call('get', key)
-- 比较线程标示与锁中的标示是否一致
if(id == threadId) then
   -- 释放锁 del key
   return redis.call('del', key)
end
return 0

----------------------------- 进阶版 -------------------------------
-- 获取锁中的线程标识 get key
local id = redis.call('get', KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
   -- 释放锁 del key
   return redis.call('del', KEYS[1])
end
return 0
----------------------------- 进阶简化版 -------------------------------
-- 这里KEYS[1]就是锁的Key, 这里的ARGV[1],就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if(redis.call('GET', KEYS[1]) == ARGV[1]) then
    -- 一致,则删除锁
    return redis.call('DEL', KEYS[1])
end
-- 不一致 直接返回
return 0

再次改进Redis的分布式锁经典白雪

基于Redis的分布式锁实现思路
利用setnx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性

需求:基于**Lua脚本**实现分布式锁的释放锁逻辑
提示:RedisTemplate调用Lua脚本的API如下:

com/hmdp/utils/SimpleRedisLock.java
package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    // 不同业务不同锁
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    // 是静态的要在静态代码块里面做初始化 这个类一加载 这个代码块就初始化完成了 不用每次释放锁再加载
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public SimpleRedisLock(String name,
                           StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name,
                        threadId,
                        timeoutSec,
                        TimeUnit.SECONDS);
        // 自动拆箱避免空指针风险
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                // 制造单集合
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

    //    public void unLock() {
//        // 获取线程标识
//        String threadId = ID_PREFIX + Thread.currentThread().getId();
//        // 获取锁
//        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//        // 判断锁是否ours
//        if (threadId.equals(id)) {
//            // 解锁
//            stringRedisTemplate.delete(KEY_PREFIX + name);
//        }
//    }
}

基于Redis的分布式锁优化

基于setnx实现的分布式锁存在下面的问题:
  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果Redis提供了主从集群 [写操作访问主节点,读操作访问从节点],主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

Redisson

Redisson是一个在Redis的基础上实现的]ava驻内存数据网格(In-MemoryData Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redisson—官方网站
Redisson—GitHub地址

Redisson可重入锁原理

package com.hmdp;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
        lock = redissonClient.getLock("order");
    }

    @Test
    void method1() throws InterruptedException {
        // 尝试获取锁
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
            log.error("获取锁失败 .... 1");
            return;
        }
        try {
            log.info("获取锁成功 .... 1");
            method2();
            log.info("开始执行业务 ... 1");
        } finally {
            log.warn("准备释放锁 .... 1");
            lock.unlock();
        }
    }
    void method2() {
        // 尝试获取锁
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 .... 2");
            return;
        }
        try {
            log.info("获取锁成功 .... 2");
            log.info("开始执行业务 ... 2");
        } finally {
            log.warn("准备释放锁 .... 2");
            lock.unlock();
        }
    }
}

Redisson的锁重试和WatchDog机制

基于Redis的分布式锁优化
基于setnx实现的分布式锁存在下面的问题:

不可重入:同一个线程无法多次获得同一把锁
不可重试:获取锁只尝试一次就返回false,没有重试机制
超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
主从一致性:如果Redis提供了主从集群主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.