【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. 拼接成ID

        return 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
    @Transactional
    public 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);
        // 保存优惠券至Redis
        stringRedisTemplate.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 {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private 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 {

        @Override
        public 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();
        }

    }

创建订单

    @Transactional
    public 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返回订单id
        return Result.ok(voucherOrder.getId());
    }
    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 0. 生成订单id
        long orderId = redisIdWorker.nextId("order");
        // 1. 执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );

        // 2. 结果是否为0
        int r = result.intValue();
        // 2.1 不为0
        if (r!=0) {
            return Result.fail(r == 1 ? "库存不足" : "不能重复抢购");
        }
        // 注解底层基于aop实现,需要获得代理对象,进行执行
        proxy = (IVoucherOrderService)AopContext.currentProxy();

        // 3. 返回结果订单id
        return Result.ok(orderId);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/484537.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】编译和链接----预处理详解【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为【C语言】文件操作揭秘&#xff1a;C语言中文件的顺序读写、随机读写、判断文件结束和文件缓冲区详细解析【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 欢迎来到本篇博客&…

易语言学习第一天(安装破解和配置)

一、引言 易语言是一个自主开发&#xff0c;适合国情&#xff0c;不同层次不同专业的人员易学易用的汉语编程语言。易语言降低了广大电脑用户编程的门槛&#xff0c;尤其是根本不懂英文或者英文了解很少的用户&#xff0c;可以通过使用本语言极其快速地进入Windows程序编写的大…

全新的分布式锁,功能简单且强大

分布式锁是分布式系统中一个极为重要的工具。 目前有多种分布式锁的设计方案&#xff0c;比如借助 redis&#xff0c;mq&#xff0c;数据库&#xff0c;zookeeper 等第三方服务系统来设计分布式锁。 tldb 提供的分布式锁&#xff0c;主要是要简化这个设计的过程&#xff0c;提…

安全之剑:深度解析 Apache Shiro 框架原理与使用指南

在现代软件开发中&#xff0c;安全性一直是至关重要的一个方面。随着网络攻击和数据泄露的不断增加&#xff0c;我们迫切需要一种强大而灵活的安全框架来保护我们的应用。Shiro框架就是这样一把利剑&#xff0c;它能够轻松地集成到你的项目中&#xff0c;为你的应用提供可靠的安…

用户增长的底层逻辑:从原理到实践

在互联网行业的洪流中&#xff0c;用户增长被视为企业生命力与竞争力的重要标志。理解并掌握用户增长的底层逻辑&#xff0c;是每一位产品经理、市场营销人员以及创业者不可或缺的基本功。 用户增长的底层逻辑&#xff1a;从原理到实践© 由 ZAKER科技 提供 一、用户增长的…

02.percona Toolkit工具pt-archiver命令实践

1.命令作用 Percona Toolkit有的32个命令&#xff0c;可以分为7大类 工具类别 工具命令 工具作用 备注 开发类 pt-duplicate-key-checker 列出并删除重复的索引和外键 pt-online-schema-change 在线修改表结构 pt-query-advisor 分析查询语句&#xff0c;并给出建议&#x…

深度学习知识点:神经网络

深度学习知识点&#xff1a;神经网络 前言神经网络激活函数的优缺点为什么ReLU常用于神经网络的激活函数&#xff1f;梯度消失和梯度爆炸的解决方案&#xff1f;梯度爆炸引发的问题&#xff1f;如何确定是否出现梯度爆炸&#xff1f;神经网络中有哪些正则化技术&#xff1f;批量…

知识图表示学习中的负抽样研究综述

摘要 知识图表示学习(KGRL)或知识图嵌入(KGE)在知识构建和信息探索的人工智能应用中起着至关重要的作用。这些模型旨在将知识图中的实体和关系编码到低维向量空间中。在KGE模型的训练过程中&#xff0c;使用正样本和负样本是区分的必要条件。然而&#xff0c;直接从现有的知识…

Qt 写一个邮件发送程序

最近在完成一个邮箱代替的告警功能&#xff0c;写了一个邮件发送的demo 以下为代码&#xff1a; #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include<QTcpSocket> namespace Ui { class MainWindow; }class MainWindow : public QMainWin…

C语言字节对齐关键字#pragma pack(n)的使用

0 前言 在进行嵌入式开发的过程中&#xff0c;我们经常会见到对齐操作。这些对齐操作有些是为了便于实现指针操作&#xff0c;有些是为了加速对内存的访问。因此&#xff0c;学习如何使用对齐关键字是对于嵌入式开发是很有必要的。 1 对齐规则 1.0 什么叫做对齐 众所周知&a…

深度学习pytorch——多层感知机反向传播(持续更新)

在讲解多层感知机反向传播之前&#xff0c;先来回顾一下多输出感知机的问题&#xff0c;下图是一个多输出感知机模型&#xff1a; 课时44 反向传播算法-1_哔哩哔哩_bilibili 根据上一次的分析深度学习pytorch——感知机&#xff08;Perceptron&#xff09;&#xff08;持续更新…

Django Ajax

【一】Json 【1】介绍 JSON&#xff08;javascript object otaition&#xff09;是一种轻量级的数据交换格式JSON使用了Javascript的一部分语法来定义其数据格式&#xff0c;但Json是独立于语言的Json采用完全独立于语言的文本格式&#xff0c;使得Json成为理想的数据交互语言…

从这15个简洁B端系统控制台页面,你悟到了什么?

从这15个简洁英文 B 端系统控制台页面中&#xff0c;我可以得出以下一些结论&#xff1a; 系统注重简洁性&#xff1a;这些页面采用了简洁的设计风格&#xff0c;避免了过多的装饰和冗余的信息&#xff0c;使用户界面更加清晰和易于使用。用户导航清晰&#xff1a;这些页面通常…

get_ipython()函数使用

今天发现一个很有意思的函数&#xff0c; 在jupyter notebook中&#xff0c;我也可以用命令实现这些值 get_ipython().system(nvidia-smi)结果如下 get_ipython().system(pwd)

学习大数据之JDBC(使用JAVA语句进行SQL操作)

文章目录 DCL语句创建用户授权用户撤销授权查看权限删除用户修改用户密码修改普通用户密码 JDBCjdbc介绍JDBC准备JDBC开发步骤以及详解JDBC注册驱动JDBC获取连接JDBC实现增删改操作JDBC 工具类的使用 DCL语句 我们现在默认使用的都是root用户&#xff0c;超级管理源&#xff0…

计算机基础系列 —— 汇编语言

Same hardware can run many different programs(Software) 文中提到的所有实现都可以参考&#xff1a;nand2tetris_sol&#xff0c;但是最好还是自己学习课程实现一遍&#xff0c;理解更深刻。 我们在之前的文章里&#xff0c;构建了 Register、RAM 和 ALU&#xff0c;使得我…

智慧物联-能源分析平台

物联能源分析平台是为了满足企业对能源管理和节能减排的需求而开发的一套在线平台。随着能源问题日益凸显&#xff0c;企业对能源的使用和管理面临着越来越大的挑战。因此&#xff0c;开发一个能够帮助企业实时监测、分析和优化能源消耗的平台变得尤为重要。 随着工业化和城市…

apifox创建接口含中文字符报错的两种解决方案

针对apifox的含中文报错解决方法&#xff1a; 方法一&#xff1a;创建相应接口后&#xff0c;在设置中URL自动编码为WHATING。 方法二&#xff1a;直接将浏览器的url复制到apifox中&#xff0c;浏览器会自动解析配置中文转换路径。

带大家做一个,易上手的家常水煮肉片

首先 我们泡一些腐竹 如果不会泡 可以查看我的文章 泡软超时干腐竹方法 然后 拿一块猪瘦肉 切 相对厚一点点的薄片 一包豆芽 洗干净 肉片装在大碗中 倒入 小半勺食用盐 适量胡椒粉 倒入一勺生抽 适量蚝油 一点点老抽 一勺淀粉 抓拌均匀 切一些 姜末 蒜末 一把花椒 七八个干…

常见技术难点及方案

1. 分布式锁 1.1 难点 1.1.1 锁延期 同一时间内不允许多个客户端同时获得锁&#xff1b; 1.1.2 防止死锁 需要确保在任何故障场景下&#xff0c;都不会出现死锁&#xff1b; 1.2.3 可重入 特殊的锁机制&#xff0c;它允许同一个线程多次获取同一个锁而不会被阻塞。 1.2…