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

黑马点评-day03-秒杀笔记

全局ID生成器核心整理

一、定义

全局ID生成器是分布式系统下用于生成全局唯一ID的工具,为分布式环境下的数据提供唯一标识,避免多节点数据ID冲突。


二、核心特性

特性含义作用/意义
唯一性生成的ID在整个分布式系统中绝对不重复保证数据在全系统内可唯一识别,避免主键冲突、数据混乱
高可用⚙️服务稳定可靠,故障时仍能正常生成ID保障分布式系统不因为ID生成器故障而整体不可用
高性能生成速度快、并发能力强支撑高并发场景(如秒杀、订单系统)下的大量ID生成需求
递增性📈ID整体保持递增趋势(不一定严格连续)有利于数据库创建索引,提升插入和查询性能(避免页分裂)
安全性🔒ID不易被猜测、泄露,避免业务信息被枚举保护业务数据安全,防止恶意爬取或业务逻辑被推测

三、补充说明

  • 递增性细节:不需要严格连续递增,只要整体趋势向上即可(如雪花算法的时间戳部分保证递增),这样既满足数据库索引优化,又避免ID泄露业务量。
  • 核心地位:全局唯一ID是分布式系统的基础组件,广泛应用于订单号、流水号、主键ID等场景。

四、一句话记忆

全局ID生成器 = 分布式环境下,生成唯一、高可用、高性能、递增、安全的全局唯一标识工具。

@ComponentpublicclassRedisIdWorker{privatestaticfinalintCOUNT_BITS=32;//序列号的位数privatefinalStringRedisTemplatestringRedisTemplate;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;}}

悲观锁:解决「一人一单」问题(防止同一个用户重复下单)
乐观锁:解决「超卖」问题(防止库存扣成负数)

@OverridepublicResultseckillVoucher(LongvoucherId){//1.查询优惠券信息SeckillVouchervoucher=seckillService.getById(voucherId);//2.判断秒杀是否开始LocalDateTimebegin=voucher.getBeginTime();if(begin.isAfter(LocalDateTime.now())){//尚未开始returnResult.fail("秒杀尚未开始!");}//3.判断秒杀是否结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){returnResult.fail("秒杀已结束");}//4.判断库存是否充足if(voucher.getStock()<1){//库存不足returnResult.fail("库存不足");}//5.一人一单LonguserId=UserHolder.getUser().getId();//保证事务提交存入db后,再释放锁,才能确保线程安全synchronized(userId.toString().intern()){IVoucherOrderServiceproxy=(IVoucherOrderService)AopContext.currentProxy();returnproxy.creatVoucherOrder(voucherId);}}
@TransactionalpublicResultcreatVoucherOrder(Long voucherId){//这里是包装类对象,这里需要的是值一样,toString返回的是地址,// intern去字符串常量池里找:如果池中已经有 "1001" → 直接返回池中的对象,如果没有 → 把 "1001" 放进池,再返回//这使得同一个用户,不管new多少对象,//不同用户,就不会被锁定//5.一人一单Long userId=UserHolder.getUser().getId();//5.1查询订单intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();//5.2判断是否存在if(count>0){//用户购买过了returnResult.fail("您已经购买过一次了");}//6.扣减库存boolean success=seckillService.update().setSql("stock = stock - 1").eq("voucher_id",voucherId).gt("stock",0).update();// ✅ 核心:只要库存大于0就可以扣.update(); //乐观锁,cas法//6.1库存不足if(!success){// 扣减失败returnResult.fail("库存不足!");}//6.创建订单VoucherOrder voucherOrder=newVoucherOrder();voucherOrder.setVoucherId(voucherId);// Long userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);Long orderId=redisIdWorker.nextId("order");voucherOrder.setId(orderId);//订单id(主键), 由id生成器生成。voucherOrderService.save(voucherOrder);//返回订单idreturnResult.ok(orderId);}

秒杀业务核心知识点整理

1. 实体类注解详解

@TableId主键注解

代码示例:

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

详细解读:

  • 作用:标识该字段为数据库主键。
  • value = "voucher_id":指定数据库表中的主键列名为voucher_id(对应实体字段voucherId)。
  • type = IdType.INPUT关键点
    • 表示主键值由开发者手动设置(Input)。
    • MyBatis-Plus不会自动生成 ID(非自增)。
    • 应用场景:秒杀优惠券 ID 需要使用全局 ID 生成器(如雪花算法)生成,因此必须使用INPUT类型。

@EqualsAndHashCode注解

代码entity类示例:

@EqualsAndHashCode(callSuper=false)

详细解读:

  • 作用:重写equals()hashCode()方法,让对象比较基于内容而非内存地址。
  • 业务意义
    • 不加此注解:Java 默认比较地址,两个new出来的对象即使数据相同也不相等。
    • 加此注解:只要业务关键字段(如 ID、库存等)相同,即视为同一个对象。这对于将对象放入HashMapHashSet或进行去重判断至关重要。
  • callSuper = false
    • 因为该类未继承父类,无需调用父类的equalshashCode
    • 设置为false仅比较当前类字段,设置为true反而会报错或逻辑错误。

2. MyBatis-Plus (MP) 操作技巧

Wrapper 构造器的简化写法

MP 提供了query()update()方法,内部封装了new QueryWrapper<>()new UpdateWrapper<>(),旨在简化代码。
对比示例:

  • 原写法(繁琐):
    UpdateWrapper<SeckillVoucher>wrapper=newUpdateWrapper<>();wrapper.setSql("stock = stock - 1").eq("voucher_id",voucherId);seckillService.update(wrapper);
  • 简化写法(推荐):
    seckillService.update().setSql("stock = stock - 1").eq("voucher_id",voucherId).update();// 链式调用,最后执行 update

3. 并发安全与锁策略

乐观锁解决超卖问题

核心思路:CAS (Compare And Swap),即更新时检查数据是否被修改。

方案演进:
  1. CAS 严格版本(不推荐):
    .eq("stock",voucher.getStock())
    • 原理:要求数据库当前库存必须等于查询出来的旧值。
    • 问题:虽然解决了超卖,但成功率极低。高并发下,一旦库存被其他线程修改,当前线程就会失败,即使库存充足。
  2. CAS 优化版本(推荐):
    booleansuccess=seckillService.update().setSql("stock = stock - 1").eq("voucher_id",voucherId).gt("stock",0)// 核心优化:只要库存 > 0 就允许扣减.update();
    • 原理:不关心库存是否变化,只关心当前是否还有库存。
    • SQL 翻译UPDATE table SET stock = stock - 1 WHERE voucher_id = ? AND stock > 0
    • 效果:既保证了不超卖(stock > 0),又大幅提高了并发成功率。

为什么扣减后还要判断库存不足?

代码逻辑:

// 步骤 4:判断库存(查询)if(voucher.getStock()<1){returnResult.fail("库存不足");}// ... 中间可能穿插其他业务逻辑(如一人一单校验)...// 步骤 6:扣减库存(更新)booleansuccess=seckillService.update()...update();if(!success){returnResult.fail("库存不足!");}// 为什么还要判断?

原因解析:

  • 非原子操作:查询(Step 4)和更新(Step 6)之间存在时间差。
  • 并发场景
    1. 线程 A 和线程 B 同时通过 Step 4(此时库存=1)。
    2. 线程 A 扣减成功(库存变 0)。
    3. 线程 B 再去扣减时,数据库已无库存。
  • 结论:Step 4 用于性能优化(提前拦截大部分请求),Step 6 的判断才是数据安全的最终防线

4. “一人一单” 并发安全问题

业务逻辑与隐患

需求:同一个用户对同一个优惠券只能购买一次。
代码逻辑

intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();if(count>0){returnResult.fail("...");}

为什么必须两个条件 (user_id+voucher_id)?

  • 只用user_id:用户买过 A 券,就永远不能买 B 券(错误)。
  • 只用voucher_id:只要有一个人买了,所有人都不能买(错误)。
  • 联合判断:锁定“用户”与“优惠券”的关系,确保唯一性。
    并发隐患
    高并发下,两个线程可能同时查询到count = 0,随后都去下单,导致“一人多单”。这和超卖问题本质一样:查询与更新非原子性

5. 事务与锁的终极解决方案

核心问题:事务失效

在类内部直接调用this.creatVoucherOrder(),即使方法上有@Transactional,事务也不会生效

  • 原因:Spring 事务基于AOP 代理实现。this指的是目标对象本身,而非代理对象,未经过 AOP 增强逻辑。

解决方案:AOP 上下文获取代理

关键代码:

// 1. 开启 AOP 代理暴露(启动类或配置类)@EnableAspectJAutoProxy(exposeProxy=true)// 2. 在业务逻辑中获取代理对象synchronized(userId.toString().intern()){// 获取当前类的代理对象IVoucherOrderServiceproxy=(IVoucherOrderService)AopContext.currentProxy();// 通过代理对象调用事务方法returnproxy.creatVoucherOrder(voucherId);}

以下是整理后的 Markdown 笔记:

并发锁细节:为什么使用userId.toString().intern()

在实现“一人一单”时,加锁代码如下:

synchronized(userId.toString().intern()){...}

之所以不直接锁userId,而是要转字符串并调用intern(),核心逻辑如下:

1. 锁对象的本质

  • synchronized锁的是对象的内存地址,而不是对象的值。
  • userIdLong类型(包装类),每次获取都是new出来的新对象。
  • 如果直接synchronized(userId):即使 ID 值相同,内存地址也不同,导致锁失效(各锁各的,起不到互斥作用)。

2.toString()的作用

  • Long对象转换为字符串对象(例如100L->"100")。
  • 虽然转成了字符串,但如果直接new String,地址依然可能不同。

3.intern()的核心作用/为什么不锁key?(关键)

  • 作用:强制在【字符串常量池】中查找或创建字符串。
  • 机制
    • 如果常量池中已有该字符串(如"100"),直接返回池中的对象。
    • 如果没有,将字符串放入池中,并返回该对象。
  • 结果:无论代码运行多少次,只要是同一个用户 ID,最终拿到的都是常量池中同一个对象

4. 最终效果

  • 同一个用户 ID:拿到的是同一个字符串对象 →锁得住,线程排队执行。
  • 不同用户 ID:拿到的是不同的字符串对象 →互不干扰,并行执行。
    总结
    这就实现了用户级别的细粒度锁,既保证了同一用户的线程安全,又避免了锁整个方法导致所有用户排队,最大化了并发性能。
    我给你整理成纯错误总结 + 错误代码示范,专门给你复习背的,只讲错的,不讲对的

错误代码示范(事务包锁,有并发漏洞)

@TransactionalpublicResultcreateVoucherOrder(LongvoucherId){LonguserId=UserHolder.getUser().getId();// 锁写在 @Transactional 方法内部synchronized(userId.toString().intern()){// 1. 判断一人一单intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();if(count>0){returnResult.fail("用户已经购买过一次!");}// 2. 扣库存booleansuccess=seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id",voucherId).gt("stock",0).update();if(!success){returnResult.fail("库存不足!");}// 3. 创建订单VoucherOrdervoucherOrder=newVoucherOrder();// ... 设置订单信息 ...save(voucherOrder);returnResult.ok(orderId);}}

这段错误代码的问题总结

  1. 锁的位置错误
    synchronized写在了@Transactional方法内部,属于事务包锁

  2. 执行顺序问题

    • 先开启事务
    • 加锁、执行业务逻辑
    • 释放锁
    • 方法结束后才提交事务
  3. 核心漏洞
    锁释放了,但事务还没提交
    这中间存在一个极短的时间窗口。

  4. 线程安全风险
    锁释放后,下一个线程可以立刻进入,
    但事务未提交,数据库里订单/库存数据还没更新,
    新线程查询不到订单,会重复下单、超卖

  5. 根本原因
    锁释放时机 早于 事务提交时机,导致并发安全失效。


一句话记忆(复习专用)

锁写在 @Transactional 方法内部 → 锁释放早于事务提交 → 出现并发安全问题。

为什么必须“锁 + 代理对象调用”?

执行顺序至关重要:

  1. 加锁(synchronized):防止并发线程同时进入。
  2. 开启事务(proxy.create...):通过代理对象进入事务逻辑。
  3. 执行业务:查单、扣库存、下单。
  4. 提交事务:将数据写入数据库。
  5. 释放锁
    如果不用代理(事务失效):锁释放了,但事务没提交。下一个线程进来查不到上一个人的订单(因为还在内存中未落库),导致并发安全问题。

知识点串联总结

  • AOP:面向切面编程,用于动态增强方法(如添加事务)。
  • 代理对象:AOP 生成的“包装壳”,内部包含了事务开启、提交、回滚的逻辑。
  • thisvsproxy
    • this.method():直接调用,无事务
    • proxy.method():代理调用,有事务
  • userId.toString().intern()intern()确保相同 ID 的字符串常量池对象唯一,从而保证锁粒度精确到“用户级别”。
    一句话总结:Spring 事务依赖代理对象生效;为了保证线程安全,锁必须包裹住整个事务过程(加锁 -> 事务提交 -> 释放锁)。

问题发现

userId.toString().intern():是在同一个jvm中的常量池中,同一个对象获取的锁才是相同的。
如果是在集群环境下由于部署了多个tomcat,每个tomcat都有一个属于自己的jvm,同一个用户在不同jvm获取的锁也就是不同的了。

解决方案欢迎大家看下一篇文章

http://www.jsqmd.com/news/577641/

相关文章:

  • Wan2.2-I2V-A14B显存优化教程:xFormers+FlashAttention-2配置与验证
  • 别再让AI瞎猜了!5个实战案例教你写出让Vibe Coding一次成功的提示词
  • 3步解锁抖音直播回放下载开源工具:告别录屏时代的高效解决方案
  • XSS攻击通用工具类
  • Python自然语言处理实战:从基础到应用
  • CUDA环境混乱导致bitsandbytes安装失败?3步彻底清理残留驱动(附A100实测)
  • 新手福音:通过快马平台ai生成可运行的spring项目快速入门
  • GLM-4-9B-Chat-1M企业级部署:vLLM算力适配方案与GPU利用率提升50%
  • 二极管基础全解(从原理、计算到选型应用)
  • 建筑工程环境检测迈入AI报告审核时代:IACheck实现全要素智能校验与质量升级
  • Android Navigation组件实战:从零构建高效Fragment导航系统
  • 2026年 防微振系统/平台/基台/装置/设备厂家推荐排行榜:精密主动与被动隔振技术,洁净室防微振解决方案深度解析 - 品牌企业推荐师(官方)
  • LightOnOCR-2-1B实战体验:11种语言混排图片识别效果实测
  • 法律AI助手调参实战:为什么我把temperature设为0.3,而不是0.7或0.1?
  • 利用快马AI快速生成xshell8风格终端管理界面原型
  • 全志T527以太网吞吐率上不去?别只调delay,这份性能排查指南帮你定位真凶
  • php方案 碎片化诊断
  • 数据工程师必备:DataX全量迁移与Flink CDC增量同步的黄金组合方案
  • 文脉定序系统一键部署教程:Ubuntu环境快速搭建指南
  • PyG安装踩坑实录:从CUDA版本冲突到ModuleNotFoundError,我的PyTorch Geometric环境搭建血泪史
  • 3个高效步骤:使用开源工具tchMaterial-parser下载国家中小学智慧教育平台电子课本
  • 2026年4月怎么搭建OpenClaw?云端部署OpenClaw、配置百炼APIKey、集成Skill喂饭级流程
  • 别再死磕逐位计算了!用C语言手撸一个CRC32查表函数(附完整代码和表格生成)
  • AI驱动的视频硬字幕精准修复技术:从痛点解决到行业革新
  • 2026年公交站台厂家推荐排行榜:智慧公交站台、综合公交站台、城市快速路公交站台、枢纽型公交站台、TOD配套公交站台、智能系统与升级改造方案深度解析 - 品牌企业推荐师(官方)
  • 别再只会用pywt.cwt了!手把手教你从零实现Python连续小波变换(附完整代码与调参避坑指南)
  • Oracle EBS FA 比例分摊惯例设置实例
  • 用JK触发器搭个10进制计数器:从真值表到自启动检查,手把手带你走一遍
  • 2026双层活动板房优质品牌推荐指南 - 优质品牌商家
  • Windows 系统下使用 ADB 的是详细的操作指南