Bug如山勤为径代码似海苦作舟。友友们好这里是苦瓜大王。今天学习的是黑马点评项目实战篇——优惠券秒杀部分的学习笔记如下后续会一直更新黑马点评学习过程中的笔记、问题等请多多支持哦文章目录一、全局ID生成器二、Redis实现全局唯一ID1.新建一个RedisIdWorker工具类2.用测试类测试生成ID效果三、添加优惠券使用ApiFox添加秒杀优惠券四、实现优惠券秒杀下单1.~~基本实现~~2.超卖问题3.乐观锁解决超卖1~~基本解决~~多此一举2改进乐观锁4.一人一单1~~基本实现~~2悲观锁实现一人一单*1~锁的粒度太小了2~事务的特性没保证3~事务需要代理来生效五、集群下的线程并发安全问题一、全局ID生成器防止信息泄露组织多表数据将redis的自增包装一下即可实现全局ID生成器二、Redis实现全局唯一ID1.新建一个RedisIdWorker工具类------------------------------------------RedisIdWorker------------------------------------------ComponentpublicclassRedisIdWorker{//开始时间戳publicstaticfinallongBEGIN_TIMESTAMP1640995200L;//序列号的位数privatestaticfinalintCOUNT_BITS32;ResourceprivateStringRedisTemplatestringRedisTemplate;publicRedisIdWorker(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplatestringRedisTemplate;}publiclongnextId(StringkeyPrefix){// 1.生成时间戳LocalDateTimenowLocalDateTime.now();longnowSecondnow.toEpochSecond(ZoneOffset.UTC);longtimestampnowSecond-BEGIN_TIMESTAMP;// 2.生成序列号// 2.1.获取当前日期精确到天Stringdatenow.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));// 2.2.自增长longcountstringRedisTemplate.opsForValue().increment(icr:keyPrefix:date);// 3.拼接并返回returntimestampCOUNT_BITS|count;}}精确到天的好处是便于统计每天、每月的订单量防止订单数太多超出位数2.用测试类测试生成ID效果-在测试类里添加如下代码------------------------------------HmDianPingApplicationTests-----------------------------------//id生成器ResourceprivateRedisIdWorkerredisIdWorker;//线程池privatestaticfinalExecutorServiceesExecutors.newFixedThreadPool(500);TestvoidtestIdWorker()throwsInterruptedException{CountDownLatchlatchnewCountDownLatch(300);Runnabletask()-{for(inti0;i100;i){longidredisIdWorker.nextId(order);System.out.println(id id);}latch.countDown();};longbeginSystem.currentTimeMillis();for(inti0;i300;i){es.submit(task);}latch.await();longendSystem.currentTimeMillis();System.out.println(time (end-begin));}知识小贴士关于countdownlatchcountdownlatch名为信号枪主要的作用是同步协调在多线程的等待于唤醒问题我们如果没有CountDownLatch 那么由于程序是异步的当异步程序没有执行完时主线程就已经执行完了然后我们期望的是分线程全部走完之后主线程再走所以我们此时需要使用到CountDownLatchCountDownLatch 中有两个最重要的方法1、countDown2、awaitawait 方法 是阻塞方法我们担心分线程没有执行完时main线程就先执行所以使用await可以让main线程阻塞那么什么时候main线程不再阻塞呢当CountDownLatch 内部维护的 变量变为0时就不再阻塞直接放行那么什么时候CountDownLatch 维护的变量变为0 呢我们只需要调用一次countDown 内部变量就减少1我们让分线程和变量绑定 执行完一个分线程就减少一个变量当分线程全部走完CountDownLatch 维护的变量就是0此时await就不再阻塞统计出来的时间也就是所有分线程执行完后的时间。三、添加优惠券平价卷由于优惠力度并不是很大所以可以任意领取而代金券由于优惠力度大所以像第二种卷就得限制数量从表结构上也能看出特价卷除了具有优惠卷的基本信息以外还具有库存抢购时间结束时间等等字段新增普通卷代码VoucherControllerPostMappingpublicResultaddVoucher(RequestBodyVouchervoucher){voucherService.save(voucher);returnResult.ok(voucher.getId());}新增秒杀卷代码VoucherControllerPostMapping(seckill)publicResultaddSeckillVoucher(RequestBodyVouchervoucher){voucherService.addSeckillVoucher(voucher);returnResult.ok(voucher.getId());}VoucherServiceImplOverrideTransactionalpublicvoidaddSeckillVoucher(Vouchervoucher){// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucherseckillVouchernewSeckillVoucher();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_KEYvoucher.getId(),voucher.getStock().toString());}使用ApiFox添加秒杀优惠券地址http://localhost:8081/voucher/seckill方式POST记得带上Token也就是Authorization登陆之后按F12查看任意一个需要登录的请求就能找到发送请求返回200则添加秒杀优惠券成功添加用的json贴在下面{shopId:1,title:100元代金券,subTitle:周一至周日均可使用,rules:全场通用\\n无需预约\\n可无限叠加\\n不兑现,不找零\\n仅限堂食,payValue:8000,actualValue:10000,type:1,status:1,stock:100,beginTime:2026-01-26T10:09:17,endTime:2026-08-26T14:09:04}四、实现优惠券秒杀下单现在用户可以抢购秒杀优惠券了1.基本实现VoucherOrderController------------------------------------VoucherOrderController---------------------------------------RestControllerRequestMapping(/voucher-order)publicclassVoucherOrderController{ResourceprivateIVoucherOrderServicevoucherOrderService;/** * 秒杀优惠券 * param voucherId 优惠券id * return 订单id */PostMapping(seckill/{id})publicResultseckillVoucher(PathVariable(id)LongvoucherId){returnvoucherOrderService.seckillVoucher(voucherId);}}IVoucherOrderServicepublicinterfaceIVoucherOrderServiceextendsIServiceVoucherOrder{/** * 秒杀优惠券 * param voucherId 优惠券id * return 订单id */ResultseckillVoucher(LongvoucherId);}VoucherOrderServiceImpl--------------------------------------VoucherOrderServiceImpl------------------------------------ServicepublicclassVoucherOrderServiceImplextendsServiceImplVoucherOrderMapper,VoucherOrderimplementsIVoucherOrderService{/** * 秒杀优惠券 * param voucherId 优惠券id * return 订单id */ResourceprivateISeckillVoucherServiceseckillVoucherService;ResourceprivateRedisIdWorkerredisIdWorker;TransactionalpublicResultseckillVoucher(LongvoucherId){//1. 查询优惠券SeckillVouchervoucherseckillVoucherService.getById(voucherId);//2. 判断是否在秒杀时间内if(voucher.getBeginTime().isAfter(LocalDateTime.now()))returnResult.fail(秒杀尚未开始);if(voucher.getEndTime().isBefore(LocalDateTime.now()))returnResult.fail(秒杀已经结束);//3.判断库存是否充足if(voucher.getStock()1)returnResult.fail(库存不足);//4. 扣减库存booleansuccessseckillVoucherService.update().setSql(stockstock-1).eq(voucher_id,voucherId).update();if(!success)//扣减失败returnResult.fail(库存不足);//5. 创建订单VoucherOrdervoucherOrdernewVoucherOrder();//5.1 订单idlongorderIdredisIdWorker.nextId(voucher_order);voucherOrder.setId(orderId);//5.2 用户idLonguserIdUserHolder.getUser().getId();voucherOrder.setUserId(userId);//5.3 代金券idvoucherOrder.setVoucherId(voucherId);//6.订单写入数据库save(voucherOrder);//7.返回订单idreturnResult.ok(orderId);}}涉及多张表的操作最好加上事务Transactional2.超卖问题仅仅实现基本的下单远远不够会造成并发安全问题导致超卖依据版本号来判断用库存代替版本3.乐观锁解决超卖1基本解决多此一举//4. 扣减库存booleansuccessseckillVoucherService.update().setSql(stockstock-1).eq(voucher_id,voucherId)//依据库存的乐观锁//stock是当前的库存//因为voucher是之前查出来的快照所以voucher.getStock()也是快照.eq(stock,voucher.getStock()).update();现在用jemter使用大量线程测试不会发生超卖但是又会发生下单失败率很高的情况因为大量线程涌进来发现被改过了就都失败了2改进乐观锁------------------------------------VoucherOrderServiceImpl--------------------------------------//扣减库存booleansuccessseckillVoucherService.update().setSql(stock stock -1).eq(voucher_id,voucherId).gt(stock,0).update();//where id ? and stock 04.一人一单1基本实现依旧是由于多线程并发穿插执行所以并不能直线真正的一人一单//4.一人一单//4.1 查询订单LonguserIdUserHolder.getUser().getId();intcountquery().eq(user_id,userId).eq(voucher_id,voucherId).count();//4.2 判断是否存在if(count0)returnResult.fail(您已经购买过一次了);//5. 扣减库存booleansuccessseckillVoucherService.update().setSql(stock stock -1).eq(voucher_id,voucherId).gt(stock,0).update();//where id ? and stock 0if(!success)//扣减失败returnResult.fail(库存不足);//6. 创建订单VoucherOrdervoucherOrdernewVoucherOrder();//6.1 订单idlongorderIdredisIdWorker.nextId(voucher_order);voucherOrder.setId(orderId);//6.2 用户idvoucherOrder.setUserId(userId);//6.3 代金券idvoucherOrder.setVoucherId(voucherId);//7.订单写入数据库save(voucherOrder);//8.返回订单idreturnResult.ok(orderId);2悲观锁实现一人一单*1~锁的粒度太小了由于每次来的userId对象不一样对象变了锁也变了所以不能直接用userId但是toString的底层调用还是new了一个字符串所以每次也还是一个全新的字符串对象所以要调用intern()返回字符串的规范表示从字符串常量池里找到地址一样的字符串返回给你这样可以保证用户id的值一样锁就一样会被锁定不同的用户不会被锁定2~事务的特性没保证虽然实现了一个用户一个锁但是事务提交是在锁外面的可能会发生别的进程来了这个进程的数据还没提交上去的情况所以在这个方法外加锁这样就能保证事务的特性同时也控制了锁的粒度3~事务需要代理来生效以上做法依然有问题因为调用的方法其实是this.的方式调用的事务想要生效还得利用代理来生效所以这个地方我们需要获得原始的事务对象 来操作事务获取代理对象IVoucherOrderServiceproxy(IVoucherOrderService)AopContext.currentProxy();IVoucherOrderService创建createVoucherOrderResultcreateVoucherOrder(LongvoucherId);添加依赖dependencygroupIdorg.aspectj/groupIdartifactIdaspectjweaver/artifactId/dependency启动类添加注解暴露代理对象不然获取不到EnableAspectJAutoProxy(exposeProxytrue)VoucherOrderServiceImpl代码如下-----------------------------------VoucherOrderServiceImpl---------------------------------------ServicepublicclassVoucherOrderServiceImplextendsServiceImplVoucherOrderMapper,VoucherOrderimplementsIVoucherOrderService{/** * 秒杀优惠券 * param voucherId 优惠券id * return 订单id */ResourceprivateISeckillVoucherServiceseckillVoucherService;ResourceprivateRedisIdWorkerredisIdWorker;publicResultseckillVoucher(LongvoucherId){//1. 查询优惠券SeckillVouchervoucherseckillVoucherService.getById(voucherId);//2. 判断是否在秒杀时间内if(voucher.getBeginTime().isAfter(LocalDateTime.now()))returnResult.fail(秒杀尚未开始);if(voucher.getEndTime().isBefore(LocalDateTime.now()))returnResult.fail(秒杀已经结束);//3.判断库存是否充足if(voucher.getStock()1)returnResult.fail(库存不足);LonguserIdUserHolder.getUser().getId();synchronized(userId.toString().intern()){//获取代理对象IVoucherOrderServiceproxy(IVoucherOrderService)AopContext.currentProxy();returnproxy.createVoucherOrder(voucherId);}}TransactionalpublicResultcreateVoucherOrder(LongvoucherId){LonguserIdUserHolder.getUser().getId();//4.一人一单//4.1 查询订单intcountquery().eq(user_id,userId).eq(voucher_id,voucherId).count();//4.2 判断是否存在if(count0)returnResult.fail(您已经购买过一次了);//5. 扣减库存booleansuccessseckillVoucherService.update().setSql(stock stock -1).eq(voucher_id,voucherId).gt(stock,0).update();//where id ? and stock 0if(!success)//扣减失败returnResult.fail(库存不足);//6. 创建订单VoucherOrdervoucherOrdernewVoucherOrder();//6.1 订单idlongorderIdredisIdWorker.nextId(voucher_order);voucherOrder.setId(orderId);//6.2 用户idvoucherOrder.setUserId(userId);//6.3 代金券idvoucherOrder.setVoucherId(voucherId);//7.订单写入数据库save(voucherOrder);//8.返回订单idreturnResult.ok(orderId);}}#待学习 代理对象五、集群下的线程并发安全问题通过加锁可以解决在单机情况下的一人一单安全问题但是在集群模式下就不行了。原因下一章将会使用分布式锁来解决这个问题以上就是黑马点评实战篇——优惠券秒杀部分的学习笔记仅供参考多多支持