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

黑马复盘 -- 优惠券秒杀

全局ID生成器

在分布式系统下用来生成全局唯一ID的工具:
唯一性,高可用,递增性,安全性,高性能

MySQL自增 ID 缺陷

1,ID 可被预测
2,单库自增 ID 有性能上限,高并发场景扛不住
3,分库分表自增 ID 会重复

全局 ID

符号位(1 bit),时间戳(31 bit),序列号(32 bit)

@ComponentpublicclassRedisIdWorker{//开始时间戳privatestaticfinallongBEGIN_TIMESTAMP=1640995200L;//序列号位数privatestaticfinalintCOUNT_BITS=32;privateStringRedisTemplatestringRedisTemplate;publicRedisIdWorker(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplate=stringRedisTemplate;}publiclongnextId(StringkeyPrefix){//这个参数是业务前缀// 1.生成时间戳LocalDateTimenow=LocalDateTime.now();longnowSecond=now.toEpochSecond(ZoneOffset.UTC);longtimestamp=nowSecond-BEGIN_TIMESTAMP;// 2.生成序列号// 2.1.获取当前日期,精确到天Stringdate=now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2.自增长longcount=stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);// 3.拼接并返回returntimestamp<<COUNT_BITS|count;}}

1,每个业务用自己的自增序列
2,INCR icr:order:2026:06:01如果这个key不存在,创建它,值设置为1;每天自增都会清0,防止redis内存过大

UUID

雪花算法

优惠券下单

表现层

@PostMapping("seckill")publicResultaddSeckillVoucher(@RequestBodyVouchervoucher){voucherService.addSeckillVoucher(voucher);returnResult.ok(voucher.getId());}
  • 其中 voucher 是这个优惠券实体类:
    优惠券分为普通优惠券和秒杀优惠券;
@TableField(exist=false)privateIntegerstock;

这个字段在 Java 类里有,但 tb_voucher 表里没有对应的列。

  • SeckillVoucher
@TableId(value="voucher_id",type=IdType.INPUT)privateLongvoucherId;

把 SeckillVoucher 的字段放进 Voucher里,是为了省事——前端展示优惠券时基本都要同时显示库存和时间,每次手动合并太麻烦,干脆在 Voucher 类里用 @TableField(exist = false) 带上这些字段。

新增优惠券

@Override@TransactionalpublicvoidaddSeckillVoucher(Vouchervoucher){// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucherseckillVoucher=newSeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存秒杀库存到Redis中stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(),voucher.getStock().toString());}}

保存秒杀券的库存到缓存;

实现下单功能

1,秒杀时间是否开始或者结束
2,库存是否充足

@RestController@RequestMapping("/voucher-order")publicclassVoucherOrderController{@ResourceprivateIVoucherOrderServicevoucherOrderService;@PostMapping("seckill/{id}")publicResultseckillVoucher(@PathVariable("id")LongvoucherId){returnvoucherOrderService.seckillVoucher(voucherId);}}

略过部分简单的代码。

// 5. 扣减库存booleansuccess=seckillVoucherService.update().setSql("stock = stock - 1")// SET stock = stock - 1.eq("voucher_id",voucherId)// WHERE voucher_id = ?.update();// 执行更新
  • 这是 MyBatis-Plus 提供的链式查询语法。 MyBatis-Plus 的 ServiceImpl 里继承来的 update() 方法,它返回一个UpdateWrapper 对象,让你可以链式拼接 SQL 条件。
  • 在 MySQL 里完成加减,而不是在 Java里算好再传进去,能避免并发问题

超卖问题

悲观锁串行执行

/** * 悲观锁方式:秒杀扣减库存 * 必须加 @Transactional !!!(锁和事务绑定) */@TransactionalpublicResultseckillByPessimisticLock(LongvoucherId){LonguserId=UserHolder.getUser().getId();// 1.【核心】查询优惠券库存 + 加悲观锁 (FOR UPDATE)// 关键SQL:SELECT * FROM tb_seckill_voucher WHERE voucher_id = ? FOR UPDATESeckillVouchervoucher=seckillVoucherService.lambdaQuery().eq(SeckillVoucher::getVoucherId,voucherId).last("FOR UPDATE")// 这行就是加悲观锁!.one();// 2. 判断库存if(voucher.getStock()<=0){returnResult.fail("库存不足");}// 3. 扣减库存(因为加了锁,只有一个线程能执行到这)booleansuccess=seckillVoucherService.lambdaUpdate().set(SeckillVoucher::getStock,voucher.getStock()-1).eq(SeckillVoucher::getVoucherId,voucherId).update();if(!success){returnResult.fail("扣减失败");}// 4. 创建订单...returnResult.ok();}
  • @Transactional 到底是什么?为什么必须加?
  1. 它的作用:Spring 声明式事务
    保证方法内所有数据库操作要么全部成功,要么全部失败
    比如:扣减库存成功、创建订单失败 → 事务回滚,库存恢复
  2. 悲观锁必须加它的生死原因
    FOR UPDATE 加的锁,和事务绑定!
    事务开启 → 加锁
    事务执行完毕(提交 / 回滚) → 自动释放锁
    如果不加 @Transactional,Spring 不会开启事务,查询完数据锁立刻释放,等于没加锁!

@Transactional = 给悲观锁提供生命周期容器
没有它,悲观锁瞬间失效。

  • MySQL InnoDB 引擎的行锁分为两种:
    共享锁 (S 锁):读锁,多个线程可以同时加 S 锁,互不阻塞
    排他锁 (X 锁):写锁,一个线程加了 X 锁,其他线程不能加任何锁,必须阻塞等待
    所有写操作(INSERT/UPDATE/DELETE)都会自动加 X 锁,这是数据库的基本规则,没有例外。

利用MySQL的条件式乐观锁

乐观锁,判断之前查询的数据是否有被修改;
1,版本号
2,CAS
3,条件式乐观锁

booleansuccess=seckillVoucherService.update().setSql("stock = stock - 1")// 原子扣减.eq("voucher_id",voucherId).gt("stock",0)// ←这就是乐观锁!.update();

MySQL 执行 UPDATE 时,会用行锁锁住这一行,两个 UPDATE 不会同时执行,会排队;
在高并发的场景下,条件式的乐观锁比版本号性能更好,能让更多线程执行;

一人一单

同一个优惠券,一个用户只能下一单

通过悲观锁synchronized+事务来实现

@TransactionalpublicResultcreateVoucherOrder(LongvoucherId){// 5.一人一单LonguserId=UserHolder.getUser().getId();synchronized(userId.toString().intern()){// 5.1.查询订单intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();// 5.2.判断是否存在if(count>0){// 用户已经购买过了returnResult.fail("用户已经购买过一次!");}// 6.扣减库存booleansuccess=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){// 扣减失败returnResult.fail("库存不足!");}// 7.创建订单VoucherOrdervoucherOrder=newVoucherOrder();// 7.1.订单idlongorderId=redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2.用户idvoucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturnResult.ok(orderId);}}
  • synchronized (userId.toString().intern()) {
    锁住同一个用户的对象头,防止一人多单。

这里有个spring代理对象没搞懂

并发安全问题

  • 这个锁可以解决单机情况下的问题,但是如果是集群模式下就失效了
  • 两台服务器各有一个 JVM,各有各的常量池。服务器 A 的 "1001"和服务器 B 的 “1001” 是两个不同的对象头,synchronized管不到对方。
http://www.jsqmd.com/news/941576/

相关文章:

  • Mathtype 7.0安装后Word闪退?可能是6.9的‘幽灵文件’在捣乱(Win10/64位避坑指南)
  • 别再只调参了!从U-Net的‘跳跃连接’入手,聊聊如何用注意力机制(如CBAM)提升你的医学图像分割精度
  • 银行的 STG 缓冲层(Stage Layer)、数据备份、数据脱敏
  • 2026年西藏钢结构工程材料采购守则:源头工厂直供与物流保障完全剖析 - 企业名录优选推荐
  • 2026彭祖蜜深度测评:如何为健康饮品匹配最佳方案? - 资讯纵览
  • OFDM与OTFS信号智能识别工具:含多SNR实测数据集及可直接运行的CNN/Transformer模型
  • SWT桌面应用专用图表库:轻量Java组件,支持线图/柱状图/散点图等10余种交互式图表
  • 从工厂车间到智能家居:STM32F4 IAP升级的两种物理层实战(RS485 vs RS232)全解析
  • 别再乱装字体了!手把手教你用FontForge和Python批量检查字体版权与字符集
  • 告别分区烦恼!用Ventoy+VMware把Ubuntu塞进U盘,一个.vtoy文件走天下
  • 5分钟掌握BepInEx:让Unity游戏焕然一新的终极插件框架
  • 2025年Q3国内高纯石英砂优质供应商精选 - 安互工业信息
  • Scarab模组管理器:让空洞骑士模组安装变得前所未有的简单
  • 2026基坑气膜生产厂家哪家好?依托行业规范,高性价比基坑气膜生产厂家推荐 - 商业新知
  • Redis 入门必学:List 列表类型完全指南
  • Ubuntu登录界面黑屏?手把手教你用lightdm --debug排查‘Failed to Start Light Display Manager’
  • VLC for Android 架构深度解析:跨平台媒体播放器完整技术实现指南
  • VC++多线程Modbus RTU串口调试工具(含完整MFC界面与串口封装)
  • 哈尔滨黄金回收人气榜本地论坛票选,得票最高的竟是这家 - 奢侈品回收测评
  • Unraid新手必看:从群晖迁移到Unraid,我的磁盘阵列、SMB共享与权限设置心得
  • NHSE:5个核心功能解锁你的动森岛屿无限可能
  • 微软研究院教师奖学金:如何为青年学者提供科研自由与创新土壤
  • 智能自动化抢票解决方案:告别手动抢票的95%成功率技术方案
  • 2026年Q2高纯石英砂供应商精选榜单 - 安互工业信息
  • 基于Cortana与本地中间件构建智能学术研究助手:从语音交互到工作流自动化
  • 从“灵光一现”到“民主投票”:Self-Consistency如何改变了我们使用ChatGPT的方式?
  • AI模型注册不是加个API那么简单:12项核心元数据规范+8类自动化校验规则全披露
  • 2026 年 6 月长春市卫生间阳台屋顶漏水防水补漏避坑指南 - 吉修匠
  • 字节跳动AI4S团队核心成员顾全全离职,回顾三年两大前沿领域成果
  • # 2026年国内闸阀公司实力排行榜:广东佛山等地基于阀门的五大品牌 - 十大品牌榜