news 2026/6/2 21:02:02

Redis篇5-实战-优惠券秒杀(Redisson、Redis消息队列)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis篇5-实战-优惠券秒杀(Redisson、Redis消息队列)

一、全局ID生成器

1.1 概念

订单表的订单号使用数据库自增id会存在一些问题
(1)id规律性太明显,会暴露一些信息如下单数量。
(2)受表单数据量影响(时间长了几百万、几千万的订单)。

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具。具有以下特点:
(1)唯一性;(2)高可用(稳定);(3)高性能(生成快);(4)递增性(有利于数据库创建索引);(5)安全性(不会暴露信息)。

使用Redis来满足上面的要求
(1)使用redis的string的自增(incr)来保证唯一性。
(2)使用redis的集群、哨兵策略来保证高可用。
(3)redis本身性能高。
(4)使用redis的string的自增(incr)。
(5)使用redis保证安全性的方案如下(使用String的Long型):

当然,redis不是全局ID的唯一方案,还有其他很多方案。

1.2 Redis自增实现

@ComponentpublicclassRedisIdWorker{privatestaticfinallongBEGIN_TIMESTAM=1640995200L;privateStringRedisTemplatestringRedisTemplate;/* *序列号位数 */privatefinalintCOUNT_BITS=32;publicRedisIdWorker(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplate=stringRedisTemplate;}publiclongnextId(StringkeyPrefix){//1.生成时间戳LocalDateTimenow=LocalDateTime.now();longnowSecond=now.toEpochSecond(ZoneOffset.UTC);longtimestamp=nowSecond-BEGIN_TIMESTAM;//2.生成序列号Stringdate=now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));longcount=stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);//3.拼接并返回returntimestamp<<COUNT_BITS|count;}}

其他的全局唯一ID生成策略
UUID
snowflake算法
数据库自增

二、线程并发安全问题

2.1 秒杀下单功能(一般实现)

2.1.1 功能点描述


2.1.2 代码实现






@ServicepublicclassVoucherOrderServiceImplextendsServiceImpl<VoucherOrderMapper,VoucherOrder>implementsIVoucherOrderService{@ResourceprivateISeckillVoucherServiceseckillVoucherService;@ResourceprivateRedisIdWorkerredisIdWorker;@Override@TransactionalpublicResultseckillVoucher(LongvoucherId){// 1.查询优惠券SeckillVouchervoucher=seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀已经结束!");}// 4.判断库存是否充足if(voucher.getStock()<1){// 库存不足returnResult.fail("库存不足!");}//5.扣减库存booleansuccess=seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id",voucherId).update();if(!success){returnResult.fail("库存不足");}// 6.创建订单VoucherOrdervoucherOrder=newVoucherOrder();// 6.1.订单idlongorderId=redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户idlonguserId=UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturnResult.ok(orderId);}}

启动服务测试:




补充:使用jmeter模拟真实秒杀场景进行测试

上面的测试方法是在前端页面手动点击的,但现实场景中会有多个并发请求,可以使用jmeter进行模拟。


注意:要在请求头里添加authorization
可以查到刚才请求的:





问题分析:
正常情况下:

异常情况:

引发了并发安全问题,解决方法是锁机制

2.1.3 悲观锁与乐观锁


悲观锁方法简单,下面说明实现乐观锁的方法,常见的两种方法是版本号法(表有一个字段为版本号,将版本号的值作为更新的条件)和CAS法(用库存数量代替版本号字段,原理与版本号相同)。

booleansuccess=seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id",voucherId).eq("stock",voucher.getStock()).update();

重启服务,删除数据库数据进行重试:



结果:

原因:因版本号字段前后不一致更新失败的线程下单失败,失败率非常高
解决失败率高的问题:
把版本号前后一致的条件改为库存大于0.

重启服务,删除数据库数据进行测试:



补充:乐观锁的缺点

乐观锁要访问数据库,对数据库的压力还是很大的,对于高并发场景还是有缺点的。

2.2 一人一单功能

目标:一个用户只能抢购一张优惠券。

2.2.1 实现思路

2.2.2 代码实现

// 一人一单LonguserId=UserHolder.getUser().getId();intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();// 5.2.判断是否存在if(count>0){// 用户已经购买过了returnResult.fail("用户已经购买过一次!");}

重启服务,删除数据库数据进行测试:




还是发生了线程安全问题。但这里不能使用乐观锁方案,只能使用悲观锁方案,将“是否下过单,更新库存,下单”的过程上锁。

2.2.3 解决线程安全问题(悲观锁)

@ServicepublicclassVoucherOrderServiceImplextendsServiceImpl<VoucherOrderMapper,VoucherOrder>implementsIVoucherOrderService{@ResourceprivateISeckillVoucherServiceseckillVoucherService;@ResourceprivateRedisIdWorkerredisIdWorker;@OverridepublicResultseckillVoucher(LongvoucherId){// 1.查询优惠券SeckillVouchervoucher=seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){// 尚未开始returnResult.fail("秒杀已经结束!");}// 4.判断库存是否充足if(voucher.getStock()<1){// 库存不足returnResult.fail("库存不足!");}returncreateVoucherOrder(voucherId);}@TransactionalpublicResultcreateVoucherOrder(LongvoucherId){// 一人一单LonguserId=UserHolder.getUser().getId();synchronized(userId.toString().intern()){//intern()可以保证对象唯一(toString方法底层是new一个String,即时值相同也不是同一个对象),去字符串常量池找值一样的字符串的地址返回intcount=query().eq("user_id",userId).eq("voucher_id",voucherId).count();// 5.2.判断是否存在if(count>0){// 用户已经购买过了returnResult.fail("用户已经购买过一次!");}//5.扣减库存booleansuccess=seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id",voucherId).gt("stock",0).update();if(!success){returnResult.fail("库存不足");}// 6.创建订单VoucherOrdervoucherOrder=newVoucherOrder();// 6.1.订单idlongorderId=redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户id//long userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单idreturnResult.ok(orderId);}}}

但上述代码还是有问题,因为createVoucherOrder()里的锁是在方法执行完,事务提交前释放的,所以可能出现一个线程释放了锁没提交事务(未写入数据库),另一个线程查询不到更新结果导致线程安全问题。

补充:悲观锁写法优化


但上面的写法会发生事务失效的问题,这里把老师原话写在下面(是的,我还不懂):
createVoucherOrder()方法加了事务,但外边的seckillVoucher()没有加事务,createVoucherOrder()方法调用拿到的是当前的VoucherOrderServiceImpl对象,而不是它的代理对象。事务要想生效是因为spring对类做了动态代理,获取了类(这里是VoucherOrderServiceImpl)的代理对象,用代理对象做的事务处理,当前的VoucherOrderServiceImpl对象不是代理对象,无事务功能。
举个例子可能会理解一些:
不会失效的例子1:

@ServicepublicclassOrderService{@AutowiredprivateUserServiceuserService;// 这里注入的是代理对象publicvoidplaceOrder(){userService.saveUser();// 通过代理调用}}@ServicepublicclassUserService{@TransactionalpublicvoidsaveUser(){// 业务逻辑}}

会失效的例子1:

publicinterfaceIOrderService{publicvoidplaceOrder();}@ServicepublicclassOrderServiceImplimplement
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/2 21:01:53

Mac鼠标指针个性化终极指南:Mousecape让你的光标与众不同

Mac鼠标指针个性化终极指南&#xff1a;Mousecape让你的光标与众不同 【免费下载链接】Mousecape Cursor Manager for OSX 项目地址: https://gitcode.com/gh_mirrors/mo/Mousecape 厌倦了Mac系统千篇一律的白色箭头光标&#xff1f;想要为你的数字工作空间注入个性和活…

作者头像 李华
网站建设 2026/6/2 21:01:05

3天彻底改变你的文献管理:Zotero-Style插件完全实战指南

3天彻底改变你的文献管理&#xff1a;Zotero-Style插件完全实战指南 【免费下载链接】zotero-style Ethereal Style for Zotero 项目地址: https://gitcode.com/GitHub_Trending/zo/zotero-style 你是否曾面对海量文献感到无从下手&#xff1f;是否在寻找某篇重要论文时…

作者头像 李华
网站建设 2026/6/2 20:59:00

网易云音乐NCM加密文件完全解密指南:3步解锁你的音乐自由

网易云音乐NCM加密文件完全解密指南&#xff1a;3步解锁你的音乐自由 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 你是否曾经在网易云音乐下载了喜欢的歌曲&#xff0c;却发现只能在官方App里播放&#xff0c;无法在其他设备上欣…

作者头像 李华
网站建设 2026/6/2 20:58:57

MeiGen-MultiTalk入门指南:如何快速创建你的第一个对话视频

MeiGen-MultiTalk入门指南&#xff1a;如何快速创建你的第一个对话视频 【免费下载链接】MeiGen-MultiTalk 项目地址: https://ai.gitcode.com/hf_mirrors/MeiGen-AI/MeiGen-MultiTalk MeiGen-MultiTalk是一款强大的开源音频驱动多人对话视频生成模型&#xff0c;以其最…

作者头像 李华
网站建设 2026/6/2 20:58:03

开源3D打印神器:让失败率降低80%的智能解决方案

开源3D打印神器&#xff1a;让失败率降低80%的智能解决方案 【免费下载链接】UVtools MSLA/DLP, file analysis, calibration, repair, conversion and manipulation 项目地址: https://gitcode.com/gh_mirrors/uv/UVtools 你是否曾经面对这样的困扰&#xff1a;精心设计…

作者头像 李华