Bootstrap

黑马程序员redis详细笔记(基础篇、实战篇)

1 基础篇

1.1 初识Redis

1.2 Redis常见命令

在这里插入图片描述

1.3 Redis的Java客户端

  • Jedis和Lettuce:这两个主要是提供了Redis命令对应的API,方便我们操作Redis
  • SpringDataRedis又对这两种做了抽象和封装

1.3.1 Jedis客户端(一般不使用这个)

1.3.2 SpringDataRedis客户端

  • SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作
  • 将不同数据类型的操作API封装到了不同的类型中:
    在这里插入图片描述
1.3.2.1 快速入门
  • SpringBoot已经提供了对SpringDataRedis的支持,使用非常简单
  • 首先,新建一个maven项目,然后按照下面步骤执行:
  • 1)引入依赖
        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--连接池依赖common-pool  这个是必须的  默认使用到-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
  • 2)配置Redis:application.yml文件
spring:
  redis:
    host: 192.168.150.101
    port: 6379
    password: 123321
    # springboot默认使用lecctuce  如果要使用jedis 需要自己引入jedis的依赖
    lettuce:
      pool:
        # 最大连接
        max-active: 8
        # 最大空闲连接
        max-idle: 8
        # 最小空闲连接
        min-idle: 0
        # 最大等待时长,没有等到会报错
        max-wait: 100ms
  • 3)注入RedisTemplate
  • SpringBoot的自动装配,我们可以拿来就用:
@SpringBootTest
class RedisStringTests {

    @Autowired
    private RedisTemplate edisTemplate;

    @Test
    void testString() {
        // 写入一条String数据
        redisTemplate.opsForValue().set("name", "虎哥");
        // 获取string数据
        Object name = stringRedisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
    }
}
1.3.2.2 自定义序列化
  • RedisTemplate可以接收任意Object作为值写入Redis:
    在这里插入图片描述
  • 只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的:
    在这里插入图片描述
  • 缺点:可读性差、内存占用较大
  • 可以自定义RedisTemplate的序列化方式,代码如下:
  • com/heima/redis/config/RedisConfig.java
// redis配置类
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
        // 1 创建RedisTemplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 2 设置连接工厂
        template.setConnectionFactory(connectionFactory);
        // 3 创建JSON序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = 
            							new GenericJackson2JsonRedisSerializer();
        // 4.1 设置Key的序列化  key和hashkey采用String序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 4.2 设置Value的序列化  value和hashvalue采用JSON序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 5 返回
        return template;
    }
}
  • com/heima/RedisDemoApplicationTests.java
@SpringBootTest
class RedisDemoApplicationTests {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    @Test
    void testString() {
        // 写入一条String数据
        redisTemplate.opsForValue().set("name", "虎哥");
        // 获取string数据
        Object name = redisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
    }

    @Test
    void testSaveUser() {
        // 写入数据
        redisTemplate.opsForValue().set("user:100", new User("虎哥", 21));
        // 获取数据
        User o = (User) redisTemplate.opsForValue().get("user:100");
        System.out.println("o = " + o);
    }
}
  • 采用了JSON序列化来代替默认的JDK序列化方式,存储自定义对象的时候会这样:
    在这里插入图片描述
  • 总结

1、整体可读性有了很大提升
2、能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象
3、但是记录了序列化时对应的class名称,是为了查询时实现自动反序列化,这会带来额外的内存开销

1.3.2.3 StringRedisTemplate
  • 为了节省内存空间,不使用JSON序列化器来处理value,而是统一使用String序列化器
  • 要求只能存储String类型的key和value
  • 当需要存储Java对象时,手动完成对象的序列化和反序列化
    在这里插入图片描述
  • 这种用法比较普遍,SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate
  • 它的key和value的序列化方式默认就是String方式
  • 省去了我们自定义RedisTemplate的序列化方式的步骤,而是直接使用:
package com.heima;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.redis.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Map;

@SpringBootTest
class RedisStringTests {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 存字符串
     */
    @Test
    void testString() {
        // 写入一条String数据
        stringRedisTemplate.opsForValue().set("verify:phone:13600527634", "124143");
        // 获取string数据
        Object name = stringRedisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
    }

    // springmvc中默认使用的JSON序列化工具
    private static final ObjectMapper mapper = new ObjectMapper();

    /**
     * 存对象
     */
    @Test
    void testSaveUser() throws JsonProcessingException {
        // 创建对象
        User user = new User("虎哥", 21);
        // 手动序列化
        String json = mapper.writeValueAsString(user);
        // 写入数据
        stringRedisTemplate.opsForValue().set("user:200", json);

        // 获取数据
        String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
        // 手动反序列化
        User user1 = mapper.readValue(jsonUser, User.class);
        System.out.println("user1 = " + user1);
    }

    @Test
    void testHash() {
        // hash存用的是put 取一个用的是get
        stringRedisTemplate.opsForHash().put("user:400", "name", "虎哥");
        stringRedisTemplate.opsForHash().put("user:400", "age", "21");
        // 一次性把所有的key value都取出来 用entries
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
        System.out.println("entries = " + entries);
    }

    @Test
    void name() {
    }
}

2 实战篇

在这里插入图片描述

2.1 短信登录

2.1.1 导入黑马点评项目(看黑马文件)

  • 记得修改mysql、redis的配置信息

2.1.2 基于Session实现登录流程

  • 发送验证码:

  • 短信验证码登录、注册:

  • 校验登录状态:
    在这里插入图片描述

2.1.3 实现发送短信验证码功能

  • 页面流程
    在这里插入图片描述

  • 具体代码如下

    /**
     * 发送手机验证码
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            // 2 如果返回true 则说明不符合
            // 如果不符合 返回错误信息
            return Result.fail("手机号格式错误");
        }

        // 2 生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 3 保存验证码到session
        session.setAttribute("code",code);

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

        return Result.ok();
    }

2.1.4 实现登录功能

在这里插入图片描述

    /**
     * 登录功能
     *
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {

        // 1 校验手机号
        // 获取验证码时手机号是正确的 登录时也许会改变
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2 如果不符合 返回错误信息
            return Result.fail("手机号格式错误");
        }

        // 2 校验验证码
        // 2.1 用户输入的code
        String code = loginForm.getCode();
        // 2.1 保存在session中的code
        String sessionCode = (String) session.getAttribute("code");
        // 这样写太繁琐 换一种方式写
//        if (sessionCode == null) {
//            return Result.fail("验证码错误");
//        } else {
//            if (!sessionCode.equals(code)) {
//                return Result.fail("验证码错误");
//            }
//        }
        if (sessionCode == null || !sessionCode.equals(code)) {
            // 3 验证码不一致 报错
            return Result.fail("验证码错误");
        }

        // 4 验证码不一致 根据手机号查询用户
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        lqw.eq(User::getPhone, phone);
        User user = baseMapper.selectOne(lqw);

        // 5 判断用户是否存在
        if (null == user) {
            // 6 不存在 创建用户并且保存
            user = createUserWithPhone(phone);

        }

        // 7 保存用户到session
        session.setAttribute("user",user);
        return Result.ok();
    }

    /**
     * 根据手机号创建用户
     * @param phone 手机号
     * @return 用户对象
     */
    private User createUserWithPhone(String phone) {
        // 创建用户
        User user = new User();
        user.setPhone(phone);
        // 创建一个有规律的随机用户名
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        baseMapper.insert(user);
        return user;
    }

2.1.5 登录校验功能

  • 关于threadlocal

1、每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收
2、每个请求都是独立的,在每个用户去访问工程时,可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据
3、在threadLocal中,无论是put方法和get方法, 都是获得当前用户的线程,然后从线程中取出线程的成员变量map
4、只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离

在这里插入图片描述

  • 请求头的Cookie中含有SessionId

在这里插入图片描述

  • 拦截器代码
package com.hmdp.interceptor;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
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 {

        // 1 获取session
        HttpSession session = request.getSession();
        // 2 获取session中的而用户
        UserDTO user = (UserDTO)session.getAttribute("user");

        // 3 判断用户是否存在
        if (user == null){
            // 4 不存在 拦截 返回401状态码
            response.setStatus(401);
            return false;
        }

        // 5 存在 保存用户信息到Threadlocal中
        UserHolder.saveUser(user);

        // 6 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        // 移除用户
        UserHolder.removeUser();
    }
}

  • 让拦截器生效
package com.hmdp.config;
import com.hmdp.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new LoginInterceptor())
                // 不需要拦截的路径
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/upload/**",
                        "/shop-type/**",
                        "/voucher/**"
                );
    }
}

  • com/hmdp/controller/UserController.java
    /**
     * 登录校验功能
     * @return
     */
    @GetMapping("/me")
    public Result me(){
        // 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

2.1.6 隐藏用户敏感信息

  • 通过浏览器观察到此时用户的全部信息都在,这样极为不靠谱
  • 应当在返回用户信息之前,将用户的敏感信息进行隐藏
  • 采用的核心思路就是书写一个UserDto对象,这个UserDto对象没有敏感信息
  • 改变登录功能的代码
        // 7 保存用户到session
        // user中含有全部信息 包括许多敏感信息 需要把user转化成userDTo
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
  • 在拦截器处:
        // 2 获取session中的而用户
        UserDTO user = (UserDTO)session.getAttribute("user");
  • 在UserHolder处:将user对象换成UserDTO

2.1.7 session共享问题

在这里插入图片描述

2.1.8 Redis代替session的业务流程

2.1.8.1 设计key的结构

在这里插入图片描述

2.1.8.2 设计key的具体细节
  • 关于key的处理,session他是每个用户都有自己的session
  • 但是redis的key是共享的,就不能使用code了
  • 在后台生成一个随机串token,然后让前端带来这个token就能完成整体逻辑
2.1.8.3 整体访问流程

在这里插入图片描述

在这里插入图片描述

2.1.9 基于Redis实现短信登录

  • com/hmdp/service/impl/UserServiceImpl.java
/**
     * 发送手机验证码
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2 如果返回true 则说明不符合
            // 如果不符合 返回错误信息
            return Result.fail("手机号格式错误");
        }

        // 3 生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4 保存验证码到redis 设置有效期
        // 把key设置为常量 有前缀的 login:code
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,2, TimeUnit.MINUTES);

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

        return Result.ok();
    }
 /**
     * 登录功能
     *
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1 校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            // 2 如果不符合 返回错误信息
            return Result.fail("手机号格式错误");
        }

        // 2 校验验证码
        // 用户输入的code
        String code = loginForm.getCode();
        // 从redis获取验证码校验
        String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (redisCode == null || !redisCode.equals(code)){
            // 3 不一致 报错
            return Result.fail("验证码错误");
        }

        // 4 根据手机号查询用户
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
        lqw.eq(User::getPhone,phone);
        User user = baseMapper.selectOne(lqw);

        // 5 判断用户是否存在
        if (user == null){
            // 6 不存在 创建用户并且保存
            user=createUserWithPhone(phone);

        }

        // 7 保存用户信息到redis中
        // 7.1 随机生成token 作为登录令牌
        // 使用UUID生成token
        String token = UUID.randomUUID().toString();

        // 7.2 将User对象转为Hash存储
        // 将user转变为userDto
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // userDTO转变为Map结构
        // Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
        // id是long类型的数据 stringRedisTemplate要求所有数据都必须是string 需要转换成string
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        // 忽略空的值
                        .setIgnoreNullValue(true)
                        // 修改字段值 字段名 字段值 -> 修改后的字段名 修改后的字段值
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

        // 7.3 存储  使用Hash结构存储
        // 使用putAll:里面有多个key value 需要把userDto转变为Map结构
        // 记得设置有效期 session有效期30分钟
        // java.lang.Long cannot be cast to java.lang.String
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
        // 7.4 设置token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);

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

    /**
     * 根据手机号创建用户
     * @param phone 手机号
     * @return 用户对象
     */
    private User createUserWithPhone(String phone) {
        // 创建用户
        User user = new User();
        user.setPhone(phone);
        // 创建一个有规律的随机用户名
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        baseMapper.insert(user);
        return user;
    }
  • 对于token有效期的设置:每当有一个请求进来,有效期就应该刷新,需要改变拦截器的代码
  • com/hmdp/interceptor/LoginInterceptor.java
    // 这里是拦截器 不在spring容器中 不能同@Autowired注入stringRedisTemplate
    // 换个方法 在配置类中注入
    // 或者加个注解:@Configuration 就可以自动注入啦
    private StringRedisTemplate stringRedisTemplate;

    // 构造函数
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

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

        // 1 获取请求头中的token
        // 这一步需要前端把token保存到请求头中
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
            // 不存在 拦截 返回401状态码
            response.setStatus(401);
            return false;
        }
        // 2 基于token获取redis中的用户
        // 只用 get 只能取出一个
        // 用entrise  取出的是一个map
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+token);

        // 3 判断用户是否存在
        // 如果userMap是null的话 自动返回一个空值
        if (userMap.isEmpty()){
            return true;
        }

        // 5 将查询到的Map对象转为UserDto
        // 忽略转换过程的错误:肯定不忽略
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);

        // 6 存在 保存用户信息到Threadlocal中
        UserHolder.saveUser(userDTO);

        // 7 刷新token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8 放行
        return true;
    }
  • com/hmdp/config/MvcConfig.java
    // @Configuration:这里可以注入
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                // 不需要拦截的路径
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/upload/**",
                        "/shop-type/**",
                        "/voucher/**"
                );
    }

2.1.10 登录拦截器的优化

2.1.10.1 初始方案思路总结:
  • 现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效
  • 此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的
    在这里插入图片描述
2.1.10.2 优化方案:

在这里插入图片描述

  • com/hmdp/interceptor/RefreshTokenInterceptor.java
package com.hmdp.interceptor;
import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import io.netty.util.internal.StringUtil;
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;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class RefreshTokenInterceptor implements HandlerInterceptor {

    // 这里是拦截器 不在spring容器中 不能同@Autowired注入stringRedisTemplate
    // 换个方法 在配置类中注入
    // 或者加个注解:@Configuration 就可以自动注入啦
    private StringRedisTemplate stringRedisTemplate;

    // 添加一个构造器
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

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

        // 1 获取token(请求头中的)
        // 这一步需要前端把token保存到请求头中
        String token = request.getHeader("authorization");
        if(StringUtil.isNullOrEmpty(token)){
            // 不需要拦截 返回true 直接放行 后面还有一个拦截器
            return true;
        }

        // 2 基于token获取redis中的用户
        // 只用 get 只能取出一个
        // 用entrise  取出的是一个map
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+token);
        // 3 判断用户是否存在
        if (userMap.isEmpty()){
            // 不需要拦截 返回true 直接放行 后面还有一个拦截器
            return true;
        }

        // 5 将查询到的Map对象转为UserDto
        // 忽略转换过程的错误:肯定不忽略
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);

        // 6 存在 保存用户信息到Threadlocal中
        UserHolder.saveUser(userDTO);

        // 7 刷新token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        // 移除用户
        UserHolder.removeUser();
    }
}

  • com/hmdp/interceptor/LoginInterceptor.java
package com.hmdp.interceptor;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import io.netty.util.internal.StringUtil;
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;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class LoginInterceptor implements HandlerInterceptor {

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

        // 1 判断是否需要拦截(threadlocal中是否有我们的用户)
        if(UserHolder.getUser() == null){
            // 没有 需要拦截 设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }

        // 有用户 放行
        return true;
    }
}

  • com/hmdp/config/MvcConfig.java
package com.hmdp.config;
import com.hmdp.interceptor.LoginInterceptor;
import com.hmdp.interceptor.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    // @Configuration:这里可以注入
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 后执行 默认情况下按照添加顺序执行 拦截需要登录的请求
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/upload/**",
                        "/shop-type/**",
                        "/voucher/**"
                )
                // 确定执行顺序 越小越先执行
                .order(1);

        // 先执行 拦截所有请求
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                .addPathPatterns("/**")
                // 确定执行顺序 越小越先执行
                .order(0);
    }
}

2.2 商户查询缓存

2.2.1 什么是缓存

  • 缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取
    在这里插入图片描述

2.2.2 添加商户缓存

  • 缓存模型和思路
    在这里插入图片描述
  • com/hmdp/service/impl/ShopServiceImpl.java
    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @Override
    public Result queryById(Long id) {

        // key要唯一 就用id
        String key = CACHE_SHOP_KEY + id;

        // 1 从redis查询商铺缓存  以店铺ID为key
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        // 2 判断是否存在
        // null "" "\t\n" 都会被认为是false
        if (StrUtil.isNotBlank(shopJson)) {
            // 3 存在直接返回  JSON格式变回类对象
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        // 4 shop不存在 根据id查询数据库
        Shop shopById = iShopService.getById(id);

        // 5 不存在 返回错误
        if (shopById == null){
            return Result.fail("该商铺不存在");
        }

        // 6 存在 写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById));

        // 7 返回
        return Result.ok(shopById);
    }

2.2.3 缓存更新策略

  • 缓存更新是redis为了节约内存而设计出来,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,把他叫为淘汰更合适
  • 三种更新策略对比
    在这里插入图片描述
  • 数据库缓存不一致解决方案
    在这里插入图片描述
  • 数据库和缓存不一致具体解决方法:
  • 删除缓存还是更新缓存?

更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新(选择这个

  • 先操作缓存还是先操作数据库?

先删除缓存,再操作数据库
先操作数据库,再删除缓存(选择这个

在这里插入图片描述

2.2.4 实现商铺和缓存与数据库双写一致

  • 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,设置超时时间
  • 根据id修改店铺时,先修改数据库,再删除缓存
  • 修改重点代码1:修改ShopServiceImpl的queryById方法:设置redis缓存时添加过期时间
        // 6 存在 写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById)
                ,CACHE_SHOP_TTL, TimeUnit.MINUTES);
  • com/hmdp/service/impl/ShopServiceImpl.java
  • 重写shopController的更新方法
    /**
     * 更新商铺信息
     * @param shop 商铺数据
     * @return 无
     */
    @Override
    // 保证原子性
    @Transactional
    public Result updateShop(Shop shop) {
        // 获取店铺id
        Long id = shop.getId();
        if (id == null){
            return Result.fail("店铺ID不能为空");
        }
        // 1 更新数据库
        iShopService.updateById(shop);
        // 2 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
        return Result.ok();
    }

2.2.5 缓存穿透问题的解决思路

  • 缓存空对象(本项目选择的
  • 优点:实现简单,维护方便
  • 缺点:
    • 额外的内存消耗
    • 可能造成短期的不一致
  • 布隆过滤
  • 优点:内存占用较少,没有多余key
  • 缺点:
    • 实现复杂
    • 存在误判可能
      在这里插入图片描述

2.2.6 编码解决商品查询的缓存穿透问题

在这里插入图片描述

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

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
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.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

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

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IShopService iShopService;


    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @Override
    public Result queryById(Long id) {

        // key要唯一 就用id
        String key = CACHE_SHOP_KEY + id;

        // 1 从redis查询商铺缓存  以店铺ID为key
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        // 2 判断是否存在
        // null "" "\t\n" 都会被认为是false
        if (StrUtil.isNotBlank(shopJson)) {
            // 3 存在直接返回  JSON格式变回类对象
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        // 判断是否是空值  是空值的话  就说明店铺不存在
        if (shopJson == ""){
            // 返回一个错误信息
            return Result.fail("该店铺不存在");
        }

        // 4 shop不存在 根据id查询数据库  shopJson == null
        Shop shopById = iShopService.getById(id);

        // 5 不存在 返回错误
        if (shopById == null){
            // 5.1 将空值写入redis
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            // 5.2 返回错误信息
            return Result.fail("该商铺不存在");
        }

        // 6 存在 写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopById)
                ,CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 7 返回
        return Result.ok(shopById);
    }

    /**
     * 更新商铺信息
     * @param shop 商铺数据
     * @return 无
     */
    @Override
    // 保证原子性
    @Transactional
    public Result updateShop(Shop shop) {
        // 获取店铺id
        Long id = shop.getId();
        if (id == null){
            return Result.fail("店铺ID不能为空");
        }
        // 1 更新数据库
        iShopService.updateById(shop);
        // 2 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
        return Result.ok();
    }
}

2.2.7 缓存雪崩问题及解决思路

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

给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性(微服务)
给缓存业务添加降级限流策略(微服务)
给业务添加多级缓存(微服务)

2.2.8 缓存击穿问题及解决思路

  • 缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
  • 产生危害:同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大
    在这里插入图片描述
  • 使用锁来解决
    在这里插入图片描述
  • 逻辑过期方案:不设置过期时间,其实就不会有缓存击穿的问题,采用逻辑过期方案
    在这里插入图片描述
  • 进行对比
    在这里插入图片描述

2.2.9 利用互斥锁解决缓存击穿问题

  • 操作锁的代码: 核心思路就是利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false
    在这里插入图片描述

2.2.10 利用逻辑过期解决缓存击穿问题

在这里插入图片描述

  • 代码汇总
package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
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.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

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

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IShopService iShopService;


    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @Override
    public Result queryById(Long id) {

        // 缓存穿透
        // Shop shop = queyWithPassThrough(id);

        // 互斥锁解决缓存击穿
        Shop shop = queyWithMutex(id);

        // 使用逻辑过期解决缓存击穿

        // 7 返回
        if (shop == null){
            return Result.fail("该店铺不存在");
        }
        return Result.ok(shop);
    }

    // 线程池
    private static final ExecutorService CACHE_RREBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public Shop queyWithLogicalExpie(Long id) {
        String key = CACHE_SHOP_KEY + id;

        // 1 从redis查询商铺缓存  以店铺ID为key
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        // 2 判断是否存在
        // null "" "\t\n" 都会被认为是false
        if (StrUtil.isBlank(shopJson)) {
            // 3 不存在直接返回  JSON格式变回类对象
            return null;
        }

        // 4 shopJson不为空 则需要把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        // 先强行转化为JSONObject
        JSONObject data = (JSONObject)redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        // 5 判断逻辑时间是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            // 6 未过期 则直接返回商铺信息
            return shop;
        }

        // 7 过期 需要重建缓存
        // 7.1 尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY+id;
        boolean flag = tryLock(lockKey);


        if (flag){
            // 7.3 获取互斥锁成功 开启独立线程 实现缓存重建
            CACHE_RREBUILD_EXECUTOR.submit(()->{
                try {
                    // 重建缓存
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }
        // 7.2 获取互斥锁失败 返回店铺信息
        // 不管成功还是失败 最后都是要返回shop
        return shop;
    }

    /**
     * 存储逻辑过期时间
     *
     * @param id
     */
    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {

        // 1 查询商铺信息
        Shop shop = iShopService.getById(id);

        Thread.sleep(200);

        // 2 封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

        // 3 写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));

    }

    /**
     * 互斥锁解决缓存击穿
     *
     * @param id
     * @return
     */
    public Shop queyWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;

        // 1 从redis查询商铺缓存  以店铺ID为key
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        // 2 判断是否存在
        // null "" "\t\n" 都会被认为是false
        if (StrUtil.isNotBlank(shopJson)) {
            // 3 存在直接返回  JSON格式变回类对象
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }

        // 是一个空值
        if (shopJson.equals("")) {
            return null;
        }

        // 既没有数据 也没有空值 是null
        // 4 实现缓存重建

        // 4.1 获取互斥锁

        // 锁的key和缓存的key不一样
        String lockKey = "lock:shop:" + id;

        Shop shop = null;
        try {
            boolean flag = tryLock(lockKey);
            // 4.2 判断是否获取成功
            if (!flag) {
                // 4.3 失败 则休眠并且重试
                Thread.sleep(50);
                // 进行递归 要返回
                return queyWithMutex(id);
            }

            // 4.4 成功 根据id查询数据库
            shop = iShopService.getById(id);
            // 模拟重建缓存 在本地查询太快了  休眠一下
            Thread.sleep(200);

            // 查询数据库结果 不存在 返回错误
            if (shop == null) {

                // 将空值写入redis   这里写入的是空值 而不是Null
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);

                return null;
            }

            // 4.5 将商铺数据写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop)
                    , CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {

            // 5 最终必须释放互斥锁
            unLock(lockKey);
        }
        // 6 返回数据
        return shop;
    }

    /**
     * 获取锁
     */
    public boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        // 不能直接返回 有可能会出现空指针
        // 上面是引用类型 转换为基本类型
        return BooleanUtil.isTrue(flag);

    }

    /**
     * 释放锁
     * @param key
     */
    public void unLock(String key) {
        stringRedisTemplate.delete(key);
    }


    /**
     * 缓存穿透代码
     * 返回空或者数据本身
     * @param id
     * @return
     */
    public Shop queyWithPassThrough(Long id) {
        String key = CACHE_SHOP_KEY + id;

        // 1 从redis查询商铺缓存  以店铺ID为key
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        // 2 判断是否存在
        // null "" "\t\n" 都会被认为是false
        if (StrUtil.isNotBlank(shopJson)) {
            // 3 存在直接返回  JSON格式变回类对象
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }

        // 是一个空值
        if (shopJson.equals("")) {
            return null;
        }

        // 4 不存在 根据id查询数据库
        Shop shopById = iShopService.getById(id);

        // 5 查询数据库结果 不存在 返回错误
        if (shopById == null) {

            // 将空值写入redis  这里写入的是空值 而不是Null
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        // 6 存在 写入redis  设置过期时间
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById)
                , CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 7 返回
        return shopById;

    }

    /**
     * 更新商铺信息
     * @param shop 商铺数据
     * @return 无
     */
    @Override
    // 保证原子性
    @Transactional
    public Result updateShop(Shop shop) {
        // 获取店铺id
        Long id = shop.getId();
        if (id == null){
            return Result.fail("店铺ID不能为空");
        }
        // 1 更新数据库
        iShopService.updateById(shop);
        // 2 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
        return Result.ok();
    }
}

2.3 优惠卷秒杀

2.3.1 全局唯一ID

  • 全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性
    在这里插入图片描述
  • 为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
    在这里插入图片描述
  • ID的组成部分:符号位:1bit,永远为0
  • 时间戳:31bit,以秒为单位,可以使用69年
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

2.3.2 Redis实现全局唯一Id

  • com/hmdp/utils/RedisIdWorker.java:新建一个类
package com.hmdp.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {

    // 定义一个初始时间戳
    public static final long BEGIN_TIME = 1640995200L;

    // 序列号位数
    public static final long COUNT_BITS = 32;

    // 用到redis的自增长
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public long nextId(String keyPrefix){

        // 1 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = nowSecond - BEGIN_TIME;

        // 2 生成序列号
        // 确定当天序列号的key 获取当天日期 精确到天
        String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:DD"));
        // 实现自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + data);

        // 3 拼接并返回  借助位运算
        // 移位之后后面32位全都是0 或运算可以保证原来的样子
        return timeStamp << COUNT_BITS | count;

    }

    public static void main(String[] args) {
//        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
//        long timeToSecond = time.toEpochSecond(ZoneOffset.UTC);
//        System.out.println(timeToSecond);

    }
}

  • stringRedisTemplate.opsForValue().increment函数:

2.3.3 添加优惠卷

  • 每个店铺都发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:
    在这里插入图片描述
  • tb_voucher:优惠券的基本信息,优惠金额、使用规则等
  • tb_seckill_voucher:秒杀券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息
  • 新增普通卷代码: VoucherController
    /**
     * 新增普通券
     * @param voucher 优惠券信息
     * @return 优惠券id
     */
    @PostMapping
    public Result addVoucher(@RequestBody Voucher voucher) {
        voucherService.save(voucher);
        return Result.ok(voucher.getId());
    }
  • 新增秒杀卷代码:
  • VoucherController
    /**
     * 新增秒杀券
     * @param voucher 优惠券信息,包含秒杀信息
     * @return 优惠券id
     */
    @PostMapping("seckill")
    public Result addSeckillVoucher(@RequestBody Voucher voucher) {
        voucherService.addSeckillVoucher(voucher);
        return Result.ok(voucher.getId());
    }
  • VoucherServiceImpl
    @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        baseMapper.insert(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
    }
  • 最后还有查询优惠券

2.3.4 实现秒杀下单

在这里插入图片描述

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单
    在这里插入图片描述
  • com/hmdp/service/impl/VoucherOrderServiceImpl.java
package com.hmdp.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.Voucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisIdWorker redisIdWorker;

    /**
     * 优惠券秒杀功能
     * @param voucherId
     * @return
     */
    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        // 1 根据id查询优惠券信息
        LambdaQueryWrapper<SeckillVoucher> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SeckillVoucher::getVoucherId,voucherId);
        SeckillVoucher seckillVoucher = seckillVoucherService.getOne(lqw);

        // 2 判断秒杀是否开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
            return Result.fail("秒杀尚未开始");
        }

        // 3 判断秒杀是否结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
            return Result.fail("秒杀已经结束");
        }

        // 4 判断库存是否充足
        if (seckillVoucher.getStock()<1){
            return Result.fail("秒杀券已经不足");
        }

        // 5 扣减库存
        seckillVoucher.setStock(seckillVoucher.getStock()-1);
        boolean success = seckillVoucherService.update(seckillVoucher, null);
        if (!success){
            return Result.fail("秒杀券已经不足");
        }

        // 6 创建订单 写入数据库
        VoucherOrder voucherOrder = new VoucherOrder();
        // 生成全局唯一Id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        baseMapper.insert(voucherOrder);


        // 7 返回订单id
        return Result.ok(orderId);
    }
}

2.3.5 库存超卖问题分析

  • 有关超卖问题分析:在我们原有代码中是这么写的
        // 4 判断库存是否充足
        if (seckillVoucher.getStock()<1){
            return Result.fail("秒杀券已经不足");
        }

        // 5 扣减库存
        seckillVoucher.setStock(seckillVoucher.getStock()-1);
        boolean success = seckillVoucherService.update(seckillVoucher, null);
        if (!success){
            return Result.fail("秒杀券已经不足");
        }
  • 假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存
    在这里插入图片描述
  • 超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁
  • 对于加锁,我们通常有两种解决方案
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2.3.6 乐观锁解决超卖问题(学习了多线程再来补充)

2.3.7 优惠券秒杀(一人一单)

  • 需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
    在这里插入图片描述
  • @Transactional和synchronized什么时候提交事务、什么时候释放锁
  • P53和P54后面再看看

2.3.8 集群环境下的并发问题

  • 通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了
  • 将服务启动两份,端口分别为8081和8082:(记得更改端口)
    在这里插入图片描述
    在这里插入图片描述
  • 然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
    在这里插入图片描述
  • 有关锁失效原因分析:部署了多个tomcat,每个tomcat都有一个属于自己的jvm,每个jvm都有一个自己的锁对象
    在这里插入图片描述

后序请看下一篇文章

;