跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

秒杀场景下的 Redis 分布式锁优化与业务设计

秒杀场景涉及高并发库存扣减与订单生成,核心在于解决超卖与一人一单的并发安全问题。文章梳理了从全局 ID 生成到分布式锁优化的完整链路。首先指出数据库自增 ID 的局限性,提出基于 Redis 的时间戳拼接方案。针对库存超卖,采用乐观锁 CAS 机制在 SQL 层面控制。对于一人一单需求,分析了单机 synchronized 锁在集群环境下的失效问题,引出 Redis 分布式锁方案。详细阐述了基于 SETNX 的加锁、超时保护、Lua 脚本保证释放原子性等关键实现细节,并指出了不可重入、主从延迟等潜在风险。

DevStack发布于 2025/2/4更新于 2026/10/787 浏览
秒杀场景下的 Redis 分布式锁优化与业务设计

秒杀场景下的 Redis 分布式锁优化与业务设计

秒杀是高并发场景的典型代表,核心挑战在于库存扣减的准确性与订单生成的原子性。面对海量请求,如何保证数据一致性与系统可用性,是架构设计的重点。

全局唯一 ID 生成策略

商品秒杀本质仍是订单购买,需要一张订单表记录流水。直接使用数据库自增 ID 在高并发下存在明显缺陷:

  1. ID 规律暴露信息:用户可通过 ID 推算出下单时间间隔,甚至推测其他用户的下单情况,存在安全隐患。
  2. 单表性能瓶颈:高并发下订单量巨大,MySQL 单表难以承载,分库分表后自增 ID 容易重复。

因此,我们需要一个全局唯一且无规律的 ID 生成器。虽然 Redis 的 INCR 命令能保证递增性和高性能,但纯数字序列依然有规律可循。通常采用 Redis 计数器配合时间戳拼接的方式,或者使用 Snowflake 算法。

以 Redis 自增 ID 策略为例,每天重置 Key 方便统计,ID 结构包含时间戳与秒内计数器:

public class RedisIdWorker {
    /** 开始时间戳 */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /** 序列号的位数 */
    private static final int COUNT_BITS = 32;

    private 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. 生成序列号(每天一个 Key)
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue()
                .increment("icr:" + keyPrefix + ":" + date);

        // 3. 拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

超卖问题的解决

超卖是多线程并发修改库存导致的典型安全问题。常见的解决方案包括悲观锁和乐观锁。

乐观锁实现

乐观锁认为冲突概率低,更新时校验数据是否被修改。在秒杀场景中,主要有两种实现方式:

  1. 版本号机制:每次扣减库存更新版本号,操作前比对版本号。
  2. CAS 思想:直接利用库存数量作为标志位。

针对 CAS 方案,SQL 层面可以这样优化:

UPDATE seckill_voucher 
SET stock = stock - 1 
WHERE voucher_id = ? AND stock > 0;

这里的关键在于判断条件 stock > 0 而非 stock = old_stock。如果多个线程都读到库存不足,前者能确保只要还有库存就能继续扣减,避免'假死'导致无法买完的情况。

一人一单的需求设计

秒杀商品优惠力度大,平台常要求'一人一单'。实现逻辑通常是先查询该用户是否有订单,若无则扣减库存并创建订单。

// 查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
    return Result.fail("用户已经购买过一次!");
}

// 扣减库存
boolean success = seckillVoucherService.update()
    .setSql("stock = stock - 1")
    .eq("voucher_id", voucherId)
    .gt("stock", 0)
    .update();

并发安全陷阱

上述两步操作并非原子性,若不加锁,同一用户的多个请求可能同时通过查询,导致重复下单。单机环境下可用 synchronized 锁住用户对象:

@Transactional
public Result createVoucherOrder(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    synchronized (userId.toString().intern()) {
        // 查询、扣减、创建订单逻辑
        // ...
    }
    return Result.ok(orderId);
}

但在集群模式下,不同节点 JVM 内的锁监视器互不干扰,synchronized 无法解决跨节点的并发问题。此时必须引入分布式锁。

分布式锁的实现与优化

分布式锁需满足互斥、高可用及安全性。基于 Redis 的实现主要依赖 SETNX 命令。

基础加锁与释放

@Override
public boolean tryLock(long timeoutSec) {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

@Override
public void unlock() {
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    if (threadId.equals(id)) {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

关键隐患处理

  1. 锁超时与误删:设置过期时间防止死锁,但需确保只删除自己的锁。代码中已加入 threadId 比对。
  2. 原子性问题:判断标识与删除 Key 是两个独立操作,中间若发生阻塞可能导致误删他人锁。需使用 Lua 脚本保证这两步的原子性。
-- unlock.lua
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
end
return 0

Java 端调用 Lua 脚本执行解锁,确保逻辑不可分割。

总结

生产级分布式锁的设计要点如下:

  • 获取锁:使用 SETNX 并设置过期时间,保存线程唯一标识。
  • 释放锁:校验标识一致性,并通过 Lua 脚本保证原子性。
  • 特性保障:SETNX 提供互斥,EX 防止死锁,Redis 集群保障高可用。

当然,当前方案仍有改进空间,例如支持可重入锁、增加重试机制、处理主从同步延迟等边界情况。在实际落地时,建议结合业务场景权衡取舍。

目录

  1. 秒杀场景下的 Redis 分布式锁优化与业务设计
  2. 全局唯一 ID 生成策略
  3. 超卖问题的解决
  4. 乐观锁实现
  5. 一人一单的需求设计
  6. 并发安全陷阱
  7. 分布式锁的实现与优化
  8. 基础加锁与释放
  9. 关键隐患处理
  10. 总结

更多推荐文章

查看全部
  • Neo4j Desktop 2 安装与实战指南
  • 微软开源 TinyTroupe:基于 LLM 的多智能体角色模拟框架
  • AI 伪造语音检测模型实战:从算法选型到性能优化
  • 基于 Python+Vue 的养老院服务预订管理系统设计与实现
  • WebAssembly 技术详解:概念、优势与开发流程
  • AR/VR 与边缘计算融合下的测试扩展策略
  • 夸克网盘精选资源合集:书籍、软件、教程与素材
  • Linux 入门:常用命令、软件安装与项目部署指南
  • 基于大语言模型的学术写作全流程智能辅助方案
  • Python 装饰器泛化公有和私有属性访问控制
  • 微软 Edge 转向 Web Components,界面响应速度提升 42%
  • AI 时代后端程序员开发前端的技术选型与实践
  • MySQL 表约束核心指南:从基础到外键实战
  • KaiwuDB 3.1.0 在 Ubuntu 22.04 的单机部署与 TLS 配置实践
  • Llama-Factory 训练进度条卡死排查与优化指南
  • Java 语言入门:设计哲学与 JVM 基础
  • Spring Boot 线程池配置与使用实战
  • Blender 复杂物体减面流程与 Python 自动化实现
  • 深度学习在自主导航中的应用综述:基础神经网络结构解析
  • ChatGPT Tools 调用实战:基于 OpenAI Python SDK 实现系统命令执行

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online