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

【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生成策略&#xff1a; UUIDRedis自增snowflake算法数据库自增 Redis自增ID策略&#xff1a;每天一个key&#xff0c;方便统计订单量ID构造是 时间戳 计数器 Component public class RedisIdWorker {// 2024的第一时刻private static final long BEGIN…...

【几何】平面方程

文章目录 平面方程一般式截距式点法式法线式 平面方程 平面方程是用一个方程来表示平面&#xff0c;平面上的所有点代入方程&#xff0c;方程都成立。因为用法的不同&#xff0c;平面方程一般有四种表现形式。 一般式 设 n ⃗ ( A , B , C ) \vec n(A,B,C) n (A,B,C) 为平…...

macOS访问samba文件夹的正确姿势,在哪里更改“macOS的连接身份“?还真不好找!

环境&#xff1a;路由器上需要身份认证的Mini NAS macOS Sonoma 14 这是一个非常简单的问题&#xff0c;但解决方法却藏得比较深&#xff0c;不够直观&#xff0c;GPT也没有给出明确的解决提示&#xff0c;特意记录一下。 macOS很多地方都很自动&#xff0c;有时候让人找不到设…...

linux进程切换

内核堆栈&#xff1a;每个进程在内核模式下运行时都有自己的内核堆栈。这个堆栈保存了进程在内核模式下的运行状态&#xff0c;包括函数调用时传递的参数、局部变量和返回地址等。 用户态与内核态&#xff1a;进程通常在用户态下运行&#xff0c;当执行系统调用或响应中断时进…...

spring boot 如何升级 Tomcat 版本

在Spring Boot应用程序中升级内嵌的Tomcat版本通常涉及以下几个步骤&#xff1a; 1. 确定当前使用的Tomcat版本 首先&#xff0c;你需要确定你的Spring Boot应用程序当前使用的Tomcat版本。这可以通过查看项目的pom.xml或build.gradle文件来完成&#xff0c;其中会列出所有的…...

sentinel中StatisticSlot数据采集的原理

StatisticSlot数据采集的原理 时间窗口 固定窗口 在固定的时间窗口内&#xff0c;可以允许固定数量的请求进入&#xff1b;超过数量就拒绝或者排队&#xff0c;等下一个时间段进入, 如下图 时间窗长度划分为1秒 单个时间窗的请求阈值为3 上述存在一个问题, 假如9:18:04:…...

图像去噪与增强技术

图像去噪与增强技术是数字图像处理领域中的两个重要方面&#xff0c;它们分别关注消除图像中的噪声和改善图像的质量。 图像去噪技术的主要目的是从受噪声干扰的图像中去除不必要的随机信号&#xff0c;以恢复图像的真实内容。这对于图像的进一步分析和理解至关重要。去噪技术包…...

SpringJPA 做分页条件查询

前言: 相信小伙伴们的项目很多都用到SpringJPA框架的吧,对于单表的增删改查利用jpa是很方便的,但是对于条件查询并且分页 是不是很多小伙伴不经常写到. 今天我整理了一下在这里分享一下. 话不多说直接上代码: Controller: RestController public class ProductInstanceContr…...

[Java基础揉碎]单例模式

目录 什么是设计模式 什么是单例模式 饿汉式与懒汉式 饿汉式vs懒汉式 懒汉式存在线程安全问题 什么是设计模式 1.静态方法和属性的经典使用 2.设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。设计模式就像是经典的棋谱&am…...

unity无法使用道路生成插件Road Architect(ctrl和shift无法标点)

切换一下布局就行了。 附&#xff1a;Road Architect教学地址...

Django下载使用、文件介绍

【一】下载并使用 【1】下载框架 &#xff08;1&#xff09;注意事项 计算机名称不要出现中文python解释器版本不同可能会出现启动报错项目中所有的文件名称不要出现中文多个项目文件尽量不要嵌套,做到一项一夹 &#xff08;2&#xff09;下载 Django属于第三方模块&#…...

Docker进阶:Docker-cpmpose 实现服务弹性伸缩

Docker进阶&#xff1a;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 模块&#xff1a;特征检测和描述子计算模块&#xff0c;包括SIFT、SURF等算法。 Features2D 模块提供了许多用于特征检测和描述子匹配的函数和类&#xff0c;这些函数和类可用于图像特征的提取、匹配和跟踪。 FeatureDetector&#xff1a;特征检测器的基类&#xf…...

HTTPS:原理、使用方法及安全威胁

文章目录 一、HTTPS技术原理1.1 主要技术原理1.2 HTTPS的工作过程1.2.1 握手阶段1.2.2 数据传输阶段 1.3 HTTPS的安全性 二、HTTPS使用方法三、HTTPS安全威胁四、总结 HTTPS&#xff08;全称&#xff1a;Hyper Text Transfer Protocol over Secure Socket Layer&#xff09;&am…...

【云开发笔记No.6】腾讯CODING平台

腾讯云很酷的一个应用&#xff0c;现在对于研发一体化&#xff0c;全流程管理&#xff0c;各种工具层出不穷。 云时代用云原生&#xff0c;再加上AI&#xff0c;编码方式真是发生了质的变化。 从前&#xff0c;一个人可以写一个很酷的软件&#xff0c;后来&#xff0c;这变得…...

20.Ubuntu下安装GCC

文章目录 Ubuntu下安装GCC查看官方安装指导错误缺少gmp库缺少32位开发库libcg: error: gengtype-lex.c: No such file or directoryreference 欢迎访问个人网络日志&#x1f339;&#x1f339;知行空间&#x1f339;&#x1f339; Ubuntu下安装GCC 为了支持新的c标准&#xff…...

2.windows ubuntu子系统配置

打开UBuntu后&#xff0c; > 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&#xff1a;调试时传参数 在launch.json的configuration中"pwd"或者"program"选项之后添加如下选项&#xff1a; “--args”:["参数1", "参数2", ..., "参数3] 参数之间使用逗号隔开 技巧2&#xff1a;断点 普通断点使…...

JavaEE企业级分布式高级架构师课程

教程介绍 本课程主要面向1-5年及以上工作经验的Java工程师&#xff0c;大纲由IT界知名大牛 — 廖雪峰老师亲自打造&#xff0c;由来自一线大型互联网公司架构师、技术总监授课&#xff0c;内容涵盖深入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. 函数名…...

【Oracle APEX开发小技巧12】

有如下需求&#xff1a; 有一个问题反馈页面&#xff0c;要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据&#xff0c;方便管理员及时处理反馈。 我的方法&#xff1a;直接将逻辑写在SQL中&#xff0c;这样可以直接在页面展示 完整代码&#xff1a; SELECTSF.FE…...

从零实现富文本编辑器#5-编辑器选区模型的状态结构表达

先前我们总结了浏览器选区模型的交互策略&#xff0c;并且实现了基本的选区操作&#xff0c;还调研了自绘选区的实现。那么相对的&#xff0c;我们还需要设计编辑器的选区表达&#xff0c;也可以称为模型选区。编辑器中应用变更时的操作范围&#xff0c;就是以模型选区为基准来…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

Nuxt.js 中的路由配置详解

Nuxt.js 通过其内置的路由系统简化了应用的路由配置&#xff0c;使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...

相机从app启动流程

一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!

一、引言 在数据驱动的背景下&#xff0c;知识图谱凭借其高效的信息组织能力&#xff0c;正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合&#xff0c;探讨知识图谱开发的实现细节&#xff0c;帮助读者掌握该技术栈在实际项目中的落地方法。 …...

华硕a豆14 Air香氛版,美学与科技的馨香融合

在快节奏的现代生活中&#xff0c;我们渴望一个能激发创想、愉悦感官的工作与生活伙伴&#xff0c;它不仅是冰冷的科技工具&#xff0c;更能触动我们内心深处的细腻情感。正是在这样的期许下&#xff0c;华硕a豆14 Air香氛版翩然而至&#xff0c;它以一种前所未有的方式&#x…...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

JVM虚拟机:内存结构、垃圾回收、性能优化

1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...

安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲

文章目录 前言第一部分&#xff1a;体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分&#xff1a;体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...