当前位置: 首页 > news >正文

Redis--day7--黑马点评--优惠券秒杀

请添加图片描述
(以下内容全部来自上述课程)
在这里插入图片描述

优惠券秒杀

1. 全局唯一ID

每个店铺都可以发布优惠券:
在这里插入图片描述

当用户抢购时,就会生成订单并保存到tb voucher order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显
  • 受单表数据量的限制

全局ID生成器

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
请添加图片描述
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其他信息:
请添加图片描述
ID的组成部分:

  • 符号位:1bit,永远为0
  • 时间戳:31bit,以秒为单位,可以使用69年
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

2. Redis实现全局唯一ID

RedisIdWorker.java:

package com.hmdp.utils;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 {/*** 开始时间戳*/private static final long BEGIN_TIMESTAMP = 1640995200L;/*** 序列号的位数*/private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix) {// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.生成序列号// 2.1.获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2.自增长long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}
}

3. 添加优惠券

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:
在这里插入图片描述
表关系如下:

  • tb_voucher:优惠券的基本信息,优惠金额、使用规则等
  • tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息

在VoucherController中提供了一个接口,可以添加秒杀优惠券:

package com.hmdp.controller;import com.hmdp.dto.Result;
import com.hmdp.entity.Voucher;
import com.hmdp.service.IVoucherService;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;/*** <p>*  前端控制器* </p>** @author 虎哥* @since 2021-12-22*/
@RestController
@RequestMapping("/voucher")
public class VoucherController {@Resourceprivate IVoucherService voucherService;/*** 新增普通券* @param voucher 优惠券信息* @return 优惠券id*/@PostMappingpublic Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());}/*** 新增秒杀券* @param voucher 优惠券信息,包含秒杀信息* @return 优惠券id*/@PostMapping("seckill")public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());}/*** 查询店铺的优惠券列表* @param shopId 店铺id* @return 优惠券列表*/@GetMapping("/list/{shopId}")public Result queryVoucherOfShop(@PathVariable("shopId") Long shopId) {return voucherService.queryVoucherOfShop(shopId);}
}

请添加图片描述

4. 实现秒杀

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

请添加图片描述
VoucherOrderController.java:

package com.hmdp.controller;import com.hmdp.dto.Result;
import com.hmdp.service.IVoucherOrderService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** <p>*  前端控制器* </p>** @author 虎哥* @since 2021-12-22*/
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Resourceprivate IVoucherOrderService voucherOrderService;@PostMapping("seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}
}

IVoucherOrderService.java:

package com.hmdp.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);
}

VoucherOrderServiceImpl.java:

package com.hmdp.service.impl;import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.time.LocalDateTime;@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1. 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//2. 判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())){//尚未开始return Result.fail("秒杀尚未开始");}//3. 判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())){//已经结束return Result.fail("秒杀已经结束");}//4. 判断库存是否充足if (voucher.getStock() < 1){//库存不足return Result.fail("库存不足");}//5. 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id",voucherId).update();if (!success){//扣减失败return Result.fail("库存不足");}//6. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//6.1 订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//6.2 用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//6.3 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//7. 返回订单idreturn Result.ok(orderId);}}

5. 库存超卖问题分析

请添加图片描述
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
请添加图片描述
悲观锁:添加同步锁,让线程串行执行

  • 优点:简单粗暴
  • 缺点:性能一般

乐观锁:不加锁,在更新时判断是否有其他线程在修改

  • 优点:性能好
  • 缺点:存在成功率低的问题

乐观锁

乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:

  • 版本号法
    请添加图片描述
  • CAS法(用库存代替版本)
    请添加图片描述

6. 乐观锁解决超卖问题

//5. 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1")  //set stock = stock - 1 .eq("voucher_id",voucherId).gt("stock",0) //where id = ? and stock > 0 .update();if (!success){//扣减失败return Result.fail("库存不足");}

7. 实现一人一单

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
请添加图片描述
目前完整代码:VoucherOrderServiceImpl.java:

package com.hmdp.service.impl;import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.val;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.time.LocalDateTime;@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {//1. 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//2. 判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())){//尚未开始return Result.fail("秒杀尚未开始");}//3. 判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())){//已经结束return Result.fail("秒杀已经结束");}//4. 判断库存是否充足if (voucher.getStock() < 1){//库存不足return Result.fail("库存不足");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic synchronized Result createVoucherOrder(Long voucherId) {//5. 一人一单Long userId = UserHolder.getUser().getId();//5.1 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2 判断是否存在if (count > 0) {//用户已经购买过了return Result.fail("用户已经购买过一次!");}//6. 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1")  //set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) //where id = ? and stock > 0.update();if (!success) {//扣减失败return Result.fail("库存不足");}//7. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1 订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2 用户idvoucherOrder.setUserId(userId);//7.3 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//8. 返回订单idreturn Result.ok(orderId);}
}

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

一人一单的并发安全问题

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。

  1. 我们将服务启动两份,端口分别为8081和8082:
    在这里插入图片描述
  2. 然后修改nqinx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
    在这里插入图片描述
    现在,用户请求会在这两个节点上负载均衡,再次测试下是否存在线程安全问题。

请添加图片描述

http://www.dtcms.com/a/336364.html

相关文章:

  • steam_api64.dll丢失?steam_api64.dll修复工具
  • 837. 新 21 点
  • C语言基础:(十六)深入理解指针(6)
  • 在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
  • 基于粒子群优化算法优化支持向量机的数据回归预测 PSO-SVM
  • Java实战:数字转中文大写金额的完整实现与优化技巧
  • 偏最小二乘结构方程(PLS-SEM)_TomatoSCI分析日记
  • bash shell 入门
  • rt-thread audio框架移植stm32 adc+dac,对接cherryusb uac,进行录音和播放
  • RTC之神奇小闹钟
  • 弱类型语言(Strong Typing)与强类型语言(Weak Typing)(描述语言对变量类型处理的严格程度)
  • 【Virtual Globe 渲染技术笔记】7 GPU 光线投射
  • 法拉第笼原理
  • Windows快捷方式添加命令行参数
  • 【备忘】superdesign如何使用?(UI设计)
  • 电脑上搭建HTTP服务器在局域网内其它客户端无法访问的解决方案
  • 钉钉退出后重新登录显示网络异常,解决方法(随手记)
  • 嵌入式LINUX——————TCP并发服务器
  • Python 设计模式详解 —— 掌握软件设计的通用解决方案
  • PWM输入捕获(测量按键按下时间、测量PWM波)
  • 25. 能否创建一个包含可变对象的不可变对象
  • YOLOV5训练自己的数据集并用自己的数据集检测
  • 2025-08-17 李沐深度学习16——目标检测
  • PAT 1068 Find More Coins
  • ACPI TABLE 方式加载device driver--以spi controller为例
  • 认识信号量机制、以及用信号量来实现进程互斥于进程同步
  • 计算机网络 TCP time_wait 状态 详解
  • VirtualBox-4.3.10-93012-Win.exe 安装教程附详细步骤(附安装包下载)
  • 为何她总在关键时“失联”?—— 解密 TCP 连接异常中断
  • TensorRT-LLM.V1.1.0rc1:Dockerfile.multi文件解读