【Redis】优惠券秒杀
全局唯一ID
全局唯一ID生成策略:
- UUID
- Redis自增
- snowflake算法
- 数据库自增
Redis自增ID策略: - 每天一个key,方便统计订单量
- ID构造是 时间戳 + 计数器
@Component
public class RedisIdWorker {// 2024的第一时刻private static final long BEGIN_TIMESTAMP = 1704067200L;private static final int COUNT_BITS = 32;private final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix){// 1. 获取当前时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timeStamp = nowSecond - BEGIN_TIMESTAMP;// 2. 获取序列号// 2.1 获取当天日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2 自增Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3. 拼接成IDreturn timeStamp << COUNT_BITS | count;}}
超卖问题
在处理大量请求时,可能会出现超卖问题。
可以通过加锁解决。

这里采用的是乐观锁。
一人一单
该任务需要每名用户只能抢到一张优惠券。
同时还要考虑到后端部署在多个服务器上可能会出现的异常,此时需要使用分布式锁进行解决。
这里的分布式锁基于Redis实现。
基于Redis的分布式锁实现思路
基于Redis的分布式锁实现思路:
- 利用set nx ex获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
- 利用set nx满足互斥性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
使用Redis优化秒杀
这里将库存判断与一人一单的校验使用Redis完成。
具体流程为:
- 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
- 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
- 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
- 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
这里的阻塞队列是基于Stream的消息队列
STREAM类型消息队列的XREADGROUP命令特点: - 消息可回溯
- 可以多消费者争抢消息,加快消费速度
- 可以阻塞读取
- 没有消息漏读的风险
- 有消息确认机制,保证消息至少被消费一次

新增秒杀优惠券的同时,将优惠券信息保存到Redis中
@Override@Transactionalpublic void addSeckillVoucher(Voucher voucher) {// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucher seckillVoucher = new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存优惠券至RedisstringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY +voucher.getId(), voucher.getStock().toString());}
基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
-- 1.参数列表
local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]
-- 2. 数据key
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId-- 3. 判断库存是否充足
if tonumber(redis.call('get', stockKey) )< 1 then-- 库存不足return 1
end-- 4. 判断用户是否已经抢购过
if redis.call('sismember', orderKey, userId) == 1 then-- 已经抢购过return 2
end-- 5. 减库存
redis.call('incrby', stockKey, -1)
-- 6. 记录用户抢购信息
redis.call('sadd', orderKey, userId)redis.call('xadd', "stream.orders", "*", "userId", userId, "voucherId", voucherId,"id", orderId)
return 0
异步下单
LUA脚本
-- 1.参数列表
local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]
-- 2. 数据key
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId-- 3. 判断库存是否充足
if tonumber(redis.call('get', stockKey) )< 1 then-- 库存不足return 1
end-- 4. 判断用户是否已经抢购过
if redis.call('sismember', orderKey, userId) == 1 then-- 已经抢购过return 2
end-- 5. 减库存
redis.call('incrby', stockKey, -1)
-- 6. 记录用户抢购信息
redis.call('sadd', orderKey, userId)redis.call('xadd', "stream.orders", "*", "userId", userId, "voucherId", voucherId,"id", orderId)
return 0
从消息队列取出,处理代码
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;private IVoucherOrderService proxy;// 静态代码块加载Lua脚本private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}// private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);// 线程池private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();@PostConstruct// 完成类的construct即执行下面的函数public void init() {// 交给线程池做SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}String queueName = "stream.orders";// 消费线程private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {// 4.1从消息队列中取出订单 xreadgroupnngroup g1 c1 count 1 block 200 streams streams.order >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create(queueName, ReadOffset.lastConsumed()));// 判断是否成功// 没有if (list == null || list.isEmpty()) {continue;}// 有// 4.2创建订单,解析消息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handleVoucherOrder(voucherOrder);// ACK确认stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());} catch (Exception e) {log.error("订单处理失败", e);handlePendingList();}}}}private void handlePendingList() {while (true) {try {// 4.1从消息队列中取出订单 xreadgroupnngroup g1 c1 count 1 block 200 streams streams.order >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1),StreamOffset.create(queueName, ReadOffset.from("0")));// 判断是否成功// 没有if (list == null || list.isEmpty()) {// pendingList 没有消息break;}// 有// 4.2创建订单,解析消息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handleVoucherOrder(voucherOrder);// ACK确认stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());} catch (Exception e) {log.error("订单处理失败", e);try {Thread.sleep(200);} catch (InterruptedException ex) {throw new RuntimeException(ex);}}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {// 因为为子线程,userId只能从数据中取Long userId = voucherOrder.getUserId();RLock lock = redissonClient.getLock("order:" + userId);boolean isLock = lock.tryLock();if (!isLock) {log.error("重复抢购");return ;}try {proxy.createVoucherOrder(voucherOrder);return;} finally {lock.unlock();}}
创建订单
@Transactionalpublic Result createVoucherOrder(VoucherOrder voucherOrder) {// 4.一人一单Long userId = voucherOrder.getUserId();// 4.1 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();if (count > 0) {return Result.fail("每人限购一张");}// 4.是// 4.1扣减库存,基于乐观锁boolean success = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();if (!success) {return Result.fail("库存不足");}//4.2创建订单save(voucherOrder);//4.3返回订单idreturn Result.ok(voucherOrder.getId());}
@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();// 0. 生成订单idlong orderId = redisIdWorker.nextId("order");// 1. 执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(), String.valueOf(orderId));// 2. 结果是否为0int r = result.intValue();// 2.1 不为0if (r!=0) {return Result.fail(r == 1 ? "库存不足" : "不能重复抢购");}// 注解底层基于aop实现,需要获得代理对象,进行执行proxy = (IVoucherOrderService)AopContext.currentProxy();// 3. 返回结果订单idreturn Result.ok(orderId);}
相关文章:
【Redis】优惠券秒杀
全局唯一ID 全局唯一ID生成策略: UUIDRedis自增snowflake算法数据库自增 Redis自增ID策略:每天一个key,方便统计订单量ID构造是 时间戳 计数器 Component public class RedisIdWorker {// 2024的第一时刻private static final long BEGIN…...
【几何】平面方程
文章目录 平面方程一般式截距式点法式法线式 平面方程 平面方程是用一个方程来表示平面,平面上的所有点代入方程,方程都成立。因为用法的不同,平面方程一般有四种表现形式。 一般式 设 n ⃗ ( A , B , C ) \vec n(A,B,C) n (A,B,C) 为平…...
macOS访问samba文件夹的正确姿势,在哪里更改“macOS的连接身份“?还真不好找!
环境:路由器上需要身份认证的Mini NAS macOS Sonoma 14 这是一个非常简单的问题,但解决方法却藏得比较深,不够直观,GPT也没有给出明确的解决提示,特意记录一下。 macOS很多地方都很自动,有时候让人找不到设…...
linux进程切换
内核堆栈:每个进程在内核模式下运行时都有自己的内核堆栈。这个堆栈保存了进程在内核模式下的运行状态,包括函数调用时传递的参数、局部变量和返回地址等。 用户态与内核态:进程通常在用户态下运行,当执行系统调用或响应中断时进…...
spring boot 如何升级 Tomcat 版本
在Spring Boot应用程序中升级内嵌的Tomcat版本通常涉及以下几个步骤: 1. 确定当前使用的Tomcat版本 首先,你需要确定你的Spring Boot应用程序当前使用的Tomcat版本。这可以通过查看项目的pom.xml或build.gradle文件来完成,其中会列出所有的…...
sentinel中StatisticSlot数据采集的原理
StatisticSlot数据采集的原理 时间窗口 固定窗口 在固定的时间窗口内,可以允许固定数量的请求进入;超过数量就拒绝或者排队,等下一个时间段进入, 如下图 时间窗长度划分为1秒 单个时间窗的请求阈值为3 上述存在一个问题, 假如9:18:04:…...
图像去噪与增强技术
图像去噪与增强技术是数字图像处理领域中的两个重要方面,它们分别关注消除图像中的噪声和改善图像的质量。 图像去噪技术的主要目的是从受噪声干扰的图像中去除不必要的随机信号,以恢复图像的真实内容。这对于图像的进一步分析和理解至关重要。去噪技术包…...
SpringJPA 做分页条件查询
前言: 相信小伙伴们的项目很多都用到SpringJPA框架的吧,对于单表的增删改查利用jpa是很方便的,但是对于条件查询并且分页 是不是很多小伙伴不经常写到. 今天我整理了一下在这里分享一下. 话不多说直接上代码: Controller: RestController public class ProductInstanceContr…...
[Java基础揉碎]单例模式
目录 什么是设计模式 什么是单例模式 饿汉式与懒汉式 饿汉式vs懒汉式 懒汉式存在线程安全问题 什么是设计模式 1.静态方法和属性的经典使用 2.设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。设计模式就像是经典的棋谱&am…...
unity无法使用道路生成插件Road Architect(ctrl和shift无法标点)
切换一下布局就行了。 附:Road Architect教学地址...
Django下载使用、文件介绍
【一】下载并使用 【1】下载框架 (1)注意事项 计算机名称不要出现中文python解释器版本不同可能会出现启动报错项目中所有的文件名称不要出现中文多个项目文件尽量不要嵌套,做到一项一夹 (2)下载 Django属于第三方模块&#…...
Docker进阶:Docker-cpmpose 实现服务弹性伸缩
Docker进阶:Docker-cpmpose 实现服务弹性伸缩 一、Docker Compose基础概念1.1 Docker Compose简介1.2 Docker Compose文件结构 二、弹性伸缩的原理和实现步骤2.1 弹性伸缩原理2.2 实现步骤 三、技术实践案例3.1 场景描述3.2 配置Docker Compose文件3.3 使用 docker-…...
opencv各个模块介绍(2)
Features2D 模块:特征检测和描述子计算模块,包括SIFT、SURF等算法。 Features2D 模块提供了许多用于特征检测和描述子匹配的函数和类,这些函数和类可用于图像特征的提取、匹配和跟踪。 FeatureDetector:特征检测器的基类…...
HTTPS:原理、使用方法及安全威胁
文章目录 一、HTTPS技术原理1.1 主要技术原理1.2 HTTPS的工作过程1.2.1 握手阶段1.2.2 数据传输阶段 1.3 HTTPS的安全性 二、HTTPS使用方法三、HTTPS安全威胁四、总结 HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer)&am…...
【云开发笔记No.6】腾讯CODING平台
腾讯云很酷的一个应用,现在对于研发一体化,全流程管理,各种工具层出不穷。 云时代用云原生,再加上AI,编码方式真是发生了质的变化。 从前,一个人可以写一个很酷的软件,后来,这变得…...
20.Ubuntu下安装GCC
文章目录 Ubuntu下安装GCC查看官方安装指导错误缺少gmp库缺少32位开发库libcg: error: gengtype-lex.c: No such file or directoryreference 欢迎访问个人网络日志🌹🌹知行空间🌹🌹 Ubuntu下安装GCC 为了支持新的c标准ÿ…...
2.windows ubuntu子系统配置
打开UBuntu后, > wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-latest-Linux-x86_64.sh #下载conda软件。 > bash Miniconda3-latest-Linux-x86_64.sh #下载完conda后执行这步 > source ~/.bashrc > conda-h #出现一下…...
vscode的一些技巧
技巧1:调试时传参数 在launch.json的configuration中"pwd"或者"program"选项之后添加如下选项: “--args”:["参数1", "参数2", ..., "参数3] 参数之间使用逗号隔开 技巧2:断点 普通断点使…...
JavaEE企业级分布式高级架构师课程
教程介绍 本课程主要面向1-5年及以上工作经验的Java工程师,大纲由IT界知名大牛 — 廖雪峰老师亲自打造,由来自一线大型互联网公司架构师、技术总监授课,内容涵盖深入spring5设计模式/高级web MVC开发/高级数据库设计与开发/高级响应式web开发…...
c语言函数大全(K开头)
c语言函数大全(K开头) There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should leave quickly. 函数名…...
seo网络推广的关键词选择技巧有哪些
SEO网络推广的关键词选择技巧有哪些 在当前的互联网时代,搜索引擎优化(SEO)已成为网络推广中不可或缺的一部分。其中,关键词选择技巧是SEO的核心之一。选择合适的关键词,不仅能够提高网站的搜索引擎排名,还…...
跨平台文件同步:OpenClaw+百川2-13B-4bits量化模型智能归档方案
跨平台文件同步:OpenClaw百川2-13B-4bits量化模型智能归档方案 1. 为什么需要智能文件归档 作为一个长期在多台设备间切换工作的开发者,我的文件管理一直处于混乱状态。同一份文档可能同时存在于Mac的Downloads文件夹、Windows桌面的"临时"目…...
OpenClaw+Phi-3-vision-128k-instruct:电商商品截图自动比价系统
OpenClawPhi-3-vision-128k-instruct:电商商品截图自动比价系统 1. 为什么需要自动化比价系统 作为一个经常网购的技术爱好者,我发现自己花在比价上的时间越来越多。每次看到心仪的商品,都要手动打开多个电商平台,截图保存价格信…...
贾子科学定理(Kucius Science Theorem):重构科学本质的公理化范式
贾子科学定理:重构科学本质的公理化范式摘要:贾子科学定理由贾子邓于2026年4月提出,颠覆传统“可证伪性”标准,以“公理驱动可结构化”重新定义科学本质,构建TMM三层体系与四大定律(真理硬度、名实分离、逻…...
在AutoDL云平台高效部署YOLO训练:从零到一的实战避坑指南
1. 为什么选择AutoDL跑YOLO训练? 第一次接触YOLO目标检测项目时,我像大多数开发者一样被本地显卡性能劝退。直到发现AutoDL这个云GPU平台,才真正体会到什么叫"用多少付多少"的灵活。相比动辄上万的游戏显卡,AutoDL上每小…...
Windows系统下CUDA Toolkit与cuDNN的安装与配置全攻略
1. 环境准备:确认你的硬件和系统支持 在开始安装CUDA Toolkit和cuDNN之前,首先要确认你的Windows系统是否满足基本要求。我遇到过不少朋友兴冲冲下载安装包,结果发现显卡根本不支持CUDA加速,白白浪费了时间。这里分享几个快速检查…...
ILI9163C SPI TFT驱动库深度解析与嵌入式优化实践
1. TFT_ILI9163C 驱动库深度解析:面向嵌入式系统的高性能SPI TFT显示方案1.1 项目定位与工程价值TFT_ILI9163C 是一款专为嵌入式平台优化的高性能 SPI TFT 显示驱动库,核心目标是在资源受限的微控制器上实现接近硬件极限的图形刷新吞吐量。其设计哲学并非…...
Skills 系统——让 AI 秒变专家
1. 技能的本质:提示词工程 在 nanobot 中,一个技能就是一个文件夹,核心是里面的 SKILL.md。 nanobot内置的skills放在project_path/nanobot/skills目录下,用户自定义的skills放在workspace/.nanobot/skills目录下 以 weather 技…...
智慧校园软件怎么选?看懂这 5 个核心功能再决定不迟
✅作者简介:合肥自友科技 📌核心产品:智慧校园软件(包括教工管理、学工管理、教务管理、考务管理、后勤管理、德育管理、资产管理、公寓管理、实习管理、就业管理、离校管理、科研平台、档案管理、学生平台等26个子平台) 。公司所有人员均有多…...
Problem - 2180D - Codeforces
Problem - 2180D - Codeforces 题意很简单 要求圆的面积没有交点 然后求尽可能大的相切点的个数 首先每个点的半径的上界就是他到相邻两个节点的距离的最小值 对于一段合法的圆 我们可以求一下第一个圆的半径的范围 然后就可以根据圆之间的距离求出下一个圆的半径的范围 如…...
