当前位置: 首页 > 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.gsyq.cn/news/1453009.html

相关文章:

  • SWT桌面应用专用图表库:轻量Java组件,支持线图/柱状图/散点图等10余种交互式图表
  • OFDM与OTFS信号智能识别工具:含多SNR实测数据集及可直接运行的CNN/Transformer模型
  • 2026彭祖蜜深度测评:如何为健康饮品匹配最佳方案? - 资讯纵览
  • 别再乱装字体了!手把手教你用FontForge和Python批量检查字体版权与字符集
  • 2025年Q3国内高纯石英砂优质供应商精选 - 安互工业信息
  • 2026基坑气膜生产厂家哪家好?依托行业规范,高性价比基坑气膜生产厂家推荐 - 商业新知
  • Ubuntu登录界面黑屏?手把手教你用lightdm --debug排查‘Failed to Start Light Display Manager’
  • 2026年Q2高纯石英砂供应商精选榜单 - 安互工业信息
  • AI模型注册不是加个API那么简单:12项核心元数据规范+8类自动化校验规则全披露
  • 如何快速掌握GetQzonehistory:QQ空间历史说说备份的完整实践指南
  • 用Python爬取中国大学MOOC近30万条评论,这份数据分析实战指南请收好
  • 遥感影像分割不再靠蒙:手把手教你用eCognition ESP2插件找到最佳尺度参数
  • 成都环保板材优质生产企业排行:核心资质与口碑一览(2026 年 6 月版,内含相关FAQ) - 互联网科技品牌测评
  • AI Agent 蓄势待发:五大趋势重塑未来,三大挑战待解!
  • 阿里云 SLS 日志服务完全指南 — 从配置到生产实践
  • # 2026年国内蝶阀公司实力排行榜:广东佛山等地五大权威推荐 - 十大品牌榜
  • 从HashMap到ConcurrentHashMap:聊聊Java 8中compute方法如何帮你写出更安全的并发代码
  • 杭州二手名表回收水深?实地测评五家门店避开压价陷阱 - 奢侈品回收测评
  • # 2026年国内不锈钢阀门公司实力排行榜:广东佛山基于阀门行业五大推荐榜单 - 十大品牌榜
  • 构建数据高速公路:从Kafka到Flink的实时数据处理架构与调优实践
  • 广州电磁流量计厂家十大品牌推荐——选型报价看这里! - 康宝莱智慧水务
  • 从边界防御到零信任:现代网络安全架构的范式转变与实践
  • 千兆像素全景技术:从图像采集到网页交互的完整实现指南
  • 2026年5月最新|熬夜亲测!将知网AIGC率从60%降到5%,5款降AI工具+免费去AI痕迹方案 - 降AI实验室
  • 为什么92%的AI配音视频被平台降权?深度解析声纹一致性、语速抖动率与平台审核阈值(附检测工具包)
  • 告别小打小闹!用NeurIPS 2023新数据集LargeST,在8600个传感器上跑通你的交通预测模型
  • 别再死记硬背了!用‘皇家间谍’的故事场景,高效记忆Linux命令行与系统状态侦察技巧
  • 2026北京奢侈品出手,五家实体回收门店避坑指南 - 奢侈品回收测评
  • 从零构建可信AI谈判系统,Claude博弈建模5步法,含可复用Python策略模板
  • 按装修风格选实木地板,配色纹理挑选小技巧|主流实木地板品牌优选排行榜 - 玖叁鹿