优化上一篇:通过 Redis Stream 队列进一步提升秒杀系统性能

引言

在之前的文章中,我们介绍了如何使用 Redis 和 Lua 脚本来应对秒杀活动中的高并发请求,并通过引入阻塞队列实现异步下单来提升系统性能。然而,在高并发场景下,阻塞队列的容量和处理速度可能会成为瓶颈。这篇文章将介绍如何使用 Redis Stream 队列进一步优化秒杀系统,提升整体性能和稳定性。

方案设计
基本思路
  1. 用户发起秒杀请求,通过 Redis Lua 脚本进行资格判断和库存扣减。
  2. Lua 脚本将订单信息写入 Redis Stream 队列。
  3. 后台线程从 Redis Stream 队列中读取订单信息并处理订单,确保数据库操作的线程安全和高效。
  4. 返回订单 ID 给用户。
具体实现
Lua 脚本

Lua 脚本负责判断秒杀资格、扣减库存,并将订单信息写入 Redis Stream 队列。

-- 1.参数列表
-- 1.1.优惠券ID
local voucherId = ARGV[1]
-- 1.2.用户ID
local userId = ARGV[2]
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本逻辑
-- 3.1.判断库存是否足够
if (tonumber(redis.call('get', stockKey)) <= 0) then
    return 1
end
-- 3.2.判断用户是否已经抢购过
if (redis.call('sismember', orderKey, userId) == 1) then
    return 2
end
-- 3.3.减少库存
redis.call('incrby', stockKey, -1)
-- 3.4.记录用户下单
redis.call('sadd', orderKey, userId)
-- 3.5.将订单信息写入Redis Stream
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
Java 代码

在 Java 代码中,我们通过 Redis Stream 队列实现异步下单,并利用 Redisson 分布式锁确保订单操作的线程安全。


@Service
@Slf4j
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 static final DefaultRedisScript<Long> SECKILL_SCRIPT;

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }

    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {
        String queueName = "stream.orders";

        @Override
        public void run() {
            while (true) {
                try {
                    // 从Redis Stream队列中获取订单信息
                    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;
                    }
                    // 解析订单信息
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> values = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, 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 {
                    // 从Pending List中获取未处理的订单信息
                    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()) {
                        break;
                    }
                    // 解析订单信息
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> values = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);
                    // 处理订单
                    handleVoucherOrder(voucherOrder);
                    // ACK确认
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理Pending List订单异常", e);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("不允许重复下单");
            return;
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            lock.unlock();
        }
    }

    private IVoucherOrderService proxy;

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        long orderId = redisIdWorker.nextId("order");
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(),
                userId.toString(), String.valueOf(orderId));
        int r = result.intValue();
        if (r != 0) {
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }

    @Override
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        if (count > 0) {
            log.error("不允许重复下单!");
            return;
        }
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id",
                        voucherOrder.getVoucherId())
                .gt("stock", 0).update();
        if (!success) {
            log.error("库存不足!");
            return;
        }
        save(voucherOrder);
    }
}

代码详解

初始化和启动订单处理线程

我们在服务启动时,通过 @PostConstruct 注解确保订单处理线程被初始化和启动。

订单处理逻辑

通过 VoucherOrderHandler 类,从 Redis Stream 队列中读取订单信息并处理订单,确保订单的创建和库存的扣减是原子操作。

订单处理方法

在订单处理方法 handleVoucherOrder 中,我们通过 Redisson 分布式锁确保同一用户不会重复下单。

结论

通过引入 Redis Stream 队列,我们进一步优化了秒杀系统的性能和稳定性。Stream 队列不仅解决了阻塞队列容量的限制问题,还提供了更强大的消息处理能力,适用于各种高并发场景。这种优化方法不仅适用于秒杀活动,还可以推广到其他需要高性能处理的系统中。希望这些改进对你的系统设计有所帮助。

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

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

相关文章

Go 语言环境搭建

本篇文章为Go语言环境搭建及下载编译器后配置Git终端方法。 目录 安装GO语言SDK Window环境安装 下载 安装测试 安装编辑器 下载编译器 设置git终端方法 总结 安装GO语言SDK Window环境安装 网站 Go下载 - Go语言中文网 - Golang中文社区 还有 All releases - The…

PyTorch使用GPU进行Tensor及模型计算

文章目录 1. 计算设备&#xff1a;GPU/CPU2. Tensor的GPU计算3. 模型的GPU计算 对复杂的神经网络和大规模的数据来说&#xff0c;使用CPU来计算可能不够高效。这里&#xff0c;我们将介绍如何使用单块NVIDIA GPU来计算。 首先&#xff0c;需要确保已经安装好了PyTorch GPU版本…

系统运维面试总结(shell编程)

SYNDDOS攻击&#xff0c;需要判断这个访问是正常访问还是信包攻击&#xff0c;当前这个信包发起的访问数量是多少&#xff0c;例如看到30个信包同时再访问时设置监控报警。

使用LabVIEW报告生成工具包时报错97

问题详情&#xff1a; 在运行使用Excel/Word调用节点的程序时&#xff0c;收到错误97&#xff1a;LabVIEW&#xff1a;&#xff08;十六进制0x61&#xff09;输入中传递了一个空引用句柄或先前已删除的引用句柄。 当运行报告生成工具包中的一个示例程序时&#xff0c;收到错误…

什么是 人工智能(AI)与机器学习(ML)?

人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09;和机器学习&#xff08;Machine Learning&#xff0c;ML&#xff09;是现代科技的核心概念&#xff0c;它们在不同领域中应用广泛。了解它们之间的关系及其工作原理对理解现代技术至关重要。本文将详细介…

Django 页面展示模型创建表的数据

1&#xff0c;添加视图函数 Test/app8/urls.py from django.shortcuts import render from .models import Userdef create_user(request):if request.method POST:username request.POST.get(username)email request.POST.get(email)# ... 获取其他字段的值# 创建用户实例…

什么是TOGAF架构框架的ADM方法?

ADM是架构开发方法&#xff08; Architecture Development Method&#xff09;&#xff0c;为开发企业架构所要执行的各个步骤以及它们质检的关系进行详细的定义&#xff0c;它是TOGAF规范中最为核心的内容。 ADM的具体步骤&#xff1a; 预备阶段&#xff08;Preliminary Phas…

C# Benchmark

创建控制台项目&#xff08;或修改现有项目的Main方法代码&#xff09;&#xff0c;Nget导入Benchmark0.13.12&#xff0c;创建测试类&#xff1a; public class StringBenchMark{int[] numbers;public StringBenchMark() {numbers Enumerable.Range(1, 20000).ToArray();}[Be…

【每日一练】python运算符

1. 算术运算符 编写一个Python程序&#xff0c;要求用户输入两个数&#xff0c;并执行以下运算&#xff1a;加法、减法、乘法、求余、除法、以及第一个数的第二个数次方。将结果打印出来。 a input("请输入第一个数&#xff1a;") b input("请输入第二个数&…

ffmpeg使用bmp编码器把bgr24编码为bmp图像

version #define LIBAVCODEC_VERSION_MAJOR 60 #define LIBAVCODEC_VERSION_MINOR 15 #define LIBAVCODEC_VERSION_MICRO 100 note 不使用AVOutputFormat code void CFfmpegOps::EncodeBGR24ToBMP(const char* infile, const char* width_str, const char* height_str…

Axure使用小技巧

Axure的小技巧 在这个数字化时代&#xff0c;用户体验设计已成为产品成功的关键。 Axure RP&#xff0c;作为一款强大的原型设计和规格文档工具&#xff0c;为设计师和产品经理提供了一个平台&#xff0c;让他们能够将创意转化为交互式的原型。 然而&#xff0c;Axure的强大…

【87 backtrader期权策略】基于50ETF期权的covered-call-strategy

前段时间有读者希望能够实现一个期权策略的模板,这段时间通过akshare下载了期权的数据,并进行了清洗,写了一个最简单的期权策略,供大家参考。 策略逻辑: 这是151 trading strategies中的一个期权策略。 买入50ETF基金,手续费按照万分之二计算,一直持有卖出一个最远期的…

nginx架构学习

前言 这篇文章主要记录下对nginx架构的学习记录。 架构设计 优秀的模块化设计 高度模块化的设计是Nginx的架构基础。在Nginx中&#xff0c;除了少量的核心代码&#xff0c;其他一切皆 为模块。 在这5种模块中&#xff0c;配置模块与核心模块都是与Nginx框架密切相关的&…

PCIe物理层_CTLE(continuous time linear equalizer)

1.CTLE&#xff08;continuous time linear equalizer&#xff09; 的作用 信号在介质的传输过程中存在趋肤效应(skin effiect)和能量损耗&#xff0c;在接收端数据会存在失真&#xff0c;并且呈现出低通特性。什么意思呢&#xff1f;就是低频率的信号衰减幅度小&#xff0c…

StringUTF_16错误认识字节长度

众所周知&#xff0c;在 UTF-8 编码中&#xff0c;中文字符通常占用 3 个字节: import java.nio.charset.StandardCharsets;/*** author shenyang* version 1.0* info untitled* since 2024/6/30 上午9:42*/ public class Test {public static void main(String[] args) {Stri…

[Go 微服务] Kratos 验证码业务

文章目录 1.环境准备2.验证码服务2.1 kratos 初始化验证码服务项目2.2 使用 Protobuf 定义验证码生成接口2.3 业务逻辑代码实现 1.环境准备 protoc和protoc-gen-go插件安装和kratos工具安装 protoc下载 下载二进制文件&#xff1a;https://github.com/protocolbuffers/protobu…

【大数据】StarRocks的系统架构

StarRocks 架构简洁&#xff0c;整个系统的核心只有 FE&#xff08;Frontend&#xff09;、BE (Backend) 或 CN (Compute Node) 两类进程&#xff0c;方便部署与维护&#xff0c;节点可以在线水平扩展&#xff0c;元数据和业务数据都有副本机制&#xff0c;确保整个系统无单点。…

SCCB协议介绍,以及与IIC协议对比

在之前的文章里已经介绍了IIC协议&#xff1a;iic通信协议 这篇内容主要介绍一下SCCB协议。 文章目录 SCCB协议&#xff1a;SCCB时序图iic时序图SCCB时序 VS IIC时序 总&#xff1a;SCCB协议常用在摄像头配置上面&#xff0c;例如OV5640摄像头&#xff0c;和IIC协议很相似&…

5.4符号三角形问题

#include<iostream> #include<stdio.h> using namespace std; int half; int ssum; int cnt0;//减号的个数 int n; int p[100][100]; int countt0; void BackTrack(int s) {if(cnt>half||s*(s-1)/2-cnt>half)return ;if(s>n){countt;return ;}for(int i0;…