05.利用Redis实现秒杀下单

学习目标:

提示:学习如何利用Redisson实现秒杀下单一人一单


学习产出:

秒杀下单流程图
请添加图片描述

解决方案:

  1. 采用Redisson分布式锁

0.数据库表结构
所有的优惠券存放表

CREATE TABLE `tb_voucher`  (
  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
  `shop_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '商铺id',
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代金券标题',
  `sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '副标题',
  `rules` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '使用规则',
  `pay_value` bigint UNSIGNED NOT NULL COMMENT '支付金额,单位是分。例如200代表2元',
  `actual_value` bigint NOT NULL COMMENT '抵扣金额,单位是分。例如200代表2元',
  `type` tinyint UNSIGNED NOT NULL DEFAULT 0 COMMENT '0,普通券;1,秒杀券',
  `status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '1,上架; 2,下架; 3,过期',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 211 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;

请添加图片描述
存放秒杀优惠券表

CREATE TABLE `tb_seckill_voucher`  (
  `voucher_id` bigint UNSIGNED NOT NULL COMMENT '关联的优惠券的id',
  `stock` int NOT NULL COMMENT '库存',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `begin_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '生效时间',
  `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '失效时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀优惠券表,与优惠券是一对一关系' ROW_FORMAT = COMPACT;

请添加图片描述
存放抢到秒杀券用户信息表

CREATE TABLE `tb_voucher_order`  (
  `id` bigint NOT NULL COMMENT '主键',
  `user_id` bigint UNSIGNED NOT NULL COMMENT '下单的用户id',
  `voucher_id` bigint UNSIGNED NOT NULL COMMENT '购买的代金券id',
  `pay_type` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
  `status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
  `pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
  `use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
  `refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;

请添加图片描述

1. 准备pom环境

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.23.1</version>
        </dependency>

2. 配置ThreadLocal和过滤器

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();
    }
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    
    
    @Autowired
    private StringRedisTemplate redis;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**").order(2);
        registry.addInterceptor(new RefreshTokenInterceptor(redis)).addPathPatterns("/**").order(1);
    }
}
---------------------------------------------
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    
    

    //controller执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //1.判断是否需要拦截ThreadLocal
        if (UserHolder.getUser()==null) {
    
    
            response.setStatus(401);
            return false;
        }
        //7.放行
        return true;
    }
    //渲染后返回给前台数据前
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    
        //移除用户,避免内存泄露
        UserHolder.removeUser();
    }
}
---------------------------------------------------
@Slf4j
public class RefreshTokenInterceptor implements HandlerInterceptor {
    
    
    //这个对象不是由spring管理的所以不能用注解自动注入

    private StringRedisTemplate redis;

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

    //controller执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
    
    
            return true;
        }
        //2.基于token获取redis中的用户
        //通过key取到hash中的map集合数据
        Map<Object, Object> userMap = redis.opsForHash().entries("login:token:" + token);
        //3.判断用户是否存在
        if (userMap.isEmpty()) {
    
    
            return true;
        }
        //5.将查询到的hash数据转为userDto对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //6.存在,保存用户信息到ThreadLocal中
        UserHolder.saveUser(userDTO);
        //7.刷新token有效期
        redis.expire(LOGIN_USER_KEY + token, 30, TimeUnit.MINUTES);
        log.info("我是第一个拦截器当前拦截所有请求的用户为,线程为{},{}",UserHolder.getUser(),Thread.currentThread());
        //8.放行
        return true;
    }

3. 配置Redisson

@Configuration
public class RedissonConfig {
    
    
    @Bean
    public RedissonClient redissonClient(){
    
    
        Config config = new Config();
        config.useSingleServer().setAddress("redis://IP地址:6379").
        setPassword("密码");
        return Redisson.create(config);
    }
}

3. Controller层:负责接收请求和向下分配

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    
    
    @Resource
    private IVoucherOrderService voucherOrderService;
    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
    
    
        return voucherOrderService.seckillVoucher(voucherId);
    }
}

4. Service层:负责业务的处理逻辑秒杀下单一人一单

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
    
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisWorker redisWorker;
    @Autowired
    private RedissonClient redisson;

    @Override
    public Result seckillVoucher(Long voucherId) {
    
    
        //1.查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        LocalDateTime currDate = LocalDateTime.now();
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        //3.判断秒杀是否结束
        //当前时间在秒杀时间前,说明未开始
        if (currDate.isBefore(beginTime)) {
    
    
            return Result.fail("秒杀已开始");
        }
        //当前时间在秒杀时间后,说明已结束
        if (currDate.isAfter(endTime)) {
    
    
            return Result.fail("秒杀已结束");
        }
        //4.判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
    
    
            return Result.fail("库存不足");
        }
        Long userId = UserHolder.getUser().getId();
        RLock lock = redisson.getLock("lock:order:" + userId);
        //上锁
        boolean flag = false;
        try {
    
    
            flag = lock.tryLock(1,10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (!flag) {
    
    
            Result.fail("不允许重复下单");
        }
        try {
    
    
            return createVoucherOrder(voucherId);
        } finally {
    
    
            lock.unlock();
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
    
    
        //一人一单 查询订单查看当前用户是否存在
        int count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
        if (count > 0) {
    
    
            System.out.println("用户已经购买过");
            return Result.fail("用户已经购买过");
        }

        //5.扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();
        if (!success) {
    
    
            return Result.fail("库存不足");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //生成唯一ID,这里可以用UUID替换或者雪花算法替代
        long orderId = redisWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //7.返回订单id
        return Result.ok(orderId);
    }
}

猜你喜欢

转载自blog.csdn.net/m0_50677223/article/details/132308863