news 2026/6/3 10:17:43

黑马复盘 -- 优惠券秒杀

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
黑马复盘 -- 优惠券秒杀

全局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管不到对方。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/3 10:16:08

别再用笨方法了!给Firefly RK3588开发板做系统备份,这招更省硬盘空间

给Firefly RK3588开发板做系统备份的两种高效方案手里这块Firefly RK3588开发板已经陪伴我完成了三个毕业设计项目&#xff0c;系统里塞满了各种开发环境、测试数据和调试工具。某天突然发现128GB的存储卡只剩下不到20GB空间&#xff0c;而实际文件只占用了35GB——这意味着有近…

作者头像 李华
网站建设 2026/6/3 10:14:24

RimSort:告别模组管理噩梦,让《环世界》体验丝滑如初

RimSort&#xff1a;告别模组管理噩梦&#xff0c;让《环世界》体验丝滑如初 【免费下载链接】RimSort RimSort is an open source mod manager for the video game RimWorld. There is support for Linux, Mac, and Windows, built from the ground up to be a reliable, comm…

作者头像 李华
网站建设 2026/6/3 10:13:05

ComfyUI-Manager批量清理指南:3步释放5GB空间的技术实践

ComfyUI-Manager批量清理指南&#xff1a;3步释放5GB空间的技术实践 【免费下载链接】ComfyUI-Manager ComfyUI-Manager is an extension designed to enhance the usability of ComfyUI. It offers management functions to install, remove, disable, and enable various cus…

作者头像 李华
网站建设 2026/6/3 10:12:21

F#正式集成Visual Studio:函数式编程在.NET生态的全面升级与实践指南

1. 项目概述&#xff1a;一次迟来但意义重大的“官宣”如果你是.NET生态的长期关注者&#xff0c;那么最近这条新闻可能让你有种“终于等到你”的感觉&#xff1a;F#正式加入了Visual Studio。这听起来像是一个技术新闻的标题&#xff0c;但其背后所代表的&#xff0c;远不止是…

作者头像 李华
网站建设 2026/6/3 10:11:35

atomic 原子操作到底有多快?我拿 Mutex 做了个对比测试

atomic 原子操作到底有多快&#xff1f;我拿 Mutex 做了个对比测试前言 写并发代码&#xff0c;锁是免不了的。Mutex 好用&#xff0c;但确实慢。高并发场景&#xff0c;atomic 原子操作是个好东西&#xff0c;但很多人不知道怎么用对。 今天我拿两百万次并发操作做了个测试&am…

作者头像 李华