redis(day03-优惠券秒杀)

目录

[实战篇 - 01. 优惠券秒杀 - 全局唯一 ID](#实战篇 - 01. 优惠券秒杀 - 全局唯一 ID)

问题:解释下面代码?

[实战篇 - 02. 优惠券秒杀 - Redis 实现全局唯一 id](#实战篇 - 02. 优惠券秒杀 - Redis 实现全局唯一 id)

[实战篇 - 03. 优惠券秒杀 - 添加优惠券](#实战篇 - 03. 优惠券秒杀 - 添加优惠券)

[实战篇 - 04. 优惠券秒杀 - 实现秒杀下单](#实战篇 - 04. 优惠券秒杀 - 实现秒杀下单)

[实战篇 - 05. 优惠券秒杀 - 库存超卖问题分析](#实战篇 - 05. 优惠券秒杀 - 库存超卖问题分析)

问题:乐观锁有哪两种实现方法?

[实战篇 - 06. 优惠券秒杀 - 乐观锁解决超卖](#实战篇 - 06. 优惠券秒杀 - 乐观锁解决超卖)

[实战篇 - 07. 优惠券秒杀 - 实现一人一单功能](#实战篇 - 07. 优惠券秒杀 - 实现一人一单功能)

问题:下图在有@Transaction的方法使用this调用别的方法有没有问题?

问题:AopContext.currentProxy()作用及步骤?

[实战篇 - 08. 优惠券秒杀 - 集群下的线程并发安全问题](#实战篇 - 08. 优惠券秒杀 - 集群下的线程并发安全问题)

[实战篇 - 09. 分布式锁 - 基本原理和不同实现方式对比](#实战篇 - 09. 分布式锁 - 基本原理和不同实现方式对比)

​编辑

问题:上图怎么保证添加锁和添加过期时间的原子性?

[实战篇 - 10. 分布式锁 - Redis 的分布式锁实现思路](#实战篇 - 10. 分布式锁 - Redis 的分布式锁实现思路)

[实战篇 - 11. 分布式锁 - 实现 Redis 分布式锁版本 1](#实战篇 - 11. 分布式锁 - 实现 Redis 分布式锁版本 1)

[实战篇 - 12. 分布式锁 - Redis 分布式锁误删问题](#实战篇 - 12. 分布式锁 - Redis 分布式锁误删问题)

[实战篇 - 13. 分布式锁 - 解决 Redis 分布式锁误删问题](#实战篇 - 13. 分布式锁 - 解决 Redis 分布式锁误删问题)

[实战篇 - 14. 分布式锁 - 分布式锁的原子性问题](#实战篇 - 14. 分布式锁 - 分布式锁的原子性问题)

[实战篇 - 15. 分布式锁 - Lua 脚本解决多条命令原子性问题](#实战篇 - 15. 分布式锁 - Lua 脚本解决多条命令原子性问题)

[实战篇 - 16. 分布式锁 - Java 调用 lua 脚本改造分布式锁](#实战篇 - 16. 分布式锁 - Java 调用 lua 脚本改造分布式锁)

问题:解释下面代码?

[实战篇 - 17. 分布式锁 - Redisson 功能介绍](#实战篇 - 17. 分布式锁 - Redisson 功能介绍)

[实战篇 - 18. 分布式锁 - Redisson 快速入门](#实战篇 - 18. 分布式锁 - Redisson 快速入门)

[实战篇 - 19. 分布式锁 - Redisson 的可重入锁原理](#实战篇 - 19. 分布式锁 - Redisson 的可重入锁原理)

问题:exists和Hexists有什么区别?

[实战篇 - 20. 分布式锁 - Redisson的锁重试和WatchDog机制](#实战篇 - 20. 分布式锁 - Redisson的锁重试和WatchDog机制)

问题:redission是怎么解决上图四个问题的?

问题:这里面的ttl是什么?

[实战篇 - 21. 分布式锁 - Redisson 的 multiLock 原理](#实战篇 - 21. 分布式锁 - Redisson 的 multiLock 原理)

[实战篇 - 22. 秒杀优化 - 异步秒杀思路](#实战篇 - 22. 秒杀优化 - 异步秒杀思路)

问题:如何在Redis中完成对秒杀库存的判断和校验一人一单?

[实战篇 - 23. 秒杀优化 - 基于 Redis 完成秒杀资格判断](#实战篇 - 23. 秒杀优化 - 基于 Redis 完成秒杀资格判断)

问题:下图Lua代码表示什么?

问题:解释下面代码?

redis.call()

xadd

stream.orders

*

[后面的 key value key value...](#后面的 key value key value...)

问题:这个intValue是干嘛的?

[实战篇 - 24. 秒杀优化 - 基于阻塞队列实现秒杀异步下单](#实战篇 - 24. 秒杀优化 - 基于阻塞队列实现秒杀异步下单)

问题:实现阻塞队列代码?

​编辑

[实战篇 - 25.Redis 消息队列 - 认识消息队列](#实战篇 - 25.Redis 消息队列 - 认识消息队列)

[实战篇 - 26.Redis 消息队列 - 基于 List 实现消息队列](#实战篇 - 26.Redis 消息队列 - 基于 List 实现消息队列)

[实战篇 - 27.Redis 消息队列 - PubSub 实现消息队列](#实战篇 - 27.Redis 消息队列 - PubSub 实现消息队列)

[实战篇 - 28.Redis 消息队列 - Stream 的单消费模式](#实战篇 - 28.Redis 消息队列 - Stream 的单消费模式)

[实战篇 - 29.Redis 消息队列 - Stream 的消费者组模式](#实战篇 - 29.Redis 消息队列 - Stream 的消费者组模式)

问题:下面代码中>和0区别?

问题:为什么有isEmpty()还需要null判断?

[实战篇 - 30.Redis 消息队列 - 基于 Stream 消息队列实现异步秒杀](#实战篇 - 30.Redis 消息队列 - 基于 Stream 消息队列实现异步秒杀)

问题:解释下面代码?

末尾页


实战篇 - 01. 优惠券秒杀 - 全局唯一 ID

问题:解释下面代码?

increment(key):Redis 的原子自增命令 (对应 Redis 原生INCR命令),作用是:

  • 如果 key 不存在:自动创建 key,值设为 1,返回 1。
  • 如果 key 存在:把 key 对应的值 + 1,返回新值。
  • 原子性保证 :Redis 单线程执行,INCR是原子操作,高并发下不会出现计数错误,完美解决分布式自增问题。
java 复制代码
package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
//id生成器
@Component
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.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

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

实战篇 - 02. 优惠券秒杀 - Redis 实现全局唯一 id

实战篇 - 03. 优惠券秒杀 - 添加优惠券

实战篇 - 04. 优惠券秒杀 - 实现秒杀下单

实战篇 - 05. 优惠券秒杀 - 库存超卖问题分析

问题:乐观锁有哪两种实现方法?

CAS使用==弊端:成功率低

解决方法:只要判断库存>0即可

实战篇 - 06. 优惠券秒杀 - 乐观锁解决超卖

实战篇 - 07. 优惠券秒杀 - 实现一人一单功能

问题:下图在有@Transaction的方法使用this调用别的方法有没有问题?

问题:AopContext.currentProxy()作用及步骤?

AopContext.currentProxy () 获取的是当前 Spring 容器中的「代理对象」(Proxy Object),而不是你原本创建的目标对象 (Target Object)。

使用它需要引入依赖并在启动类上加以下注解

问题:获取的代理对象是什么?

实战篇 - 08. 优惠券秒杀 - 集群下的线程并发安全问题

实战篇 - 09. 分布式锁 - 基本原理和不同实现方式对比

问题:上图怎么保证添加锁和添加过期时间的原子性?

实战篇 - 10. 分布式锁 - Redis 的分布式锁实现思路

实战篇 - 11. 分布式锁 - 实现 Redis 分布式锁版本 1

实战篇 - 12. 分布式锁 - Redis 分布式锁误删问题

实战篇 - 13. 分布式锁 - 解决 Redis 分布式锁误删问题

实战篇 - 14. 分布式锁 - 分布式锁的原子性问题

实战篇 - 15. 分布式锁 - Lua 脚本解决多条命令原子性问题

实战篇 - 16. 分布式锁 - Java 调用 lua 脚本改造分布式锁

问题:解释下面代码?

java 复制代码
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
  1. stringRedisTemplate.execute(...)

执行 Redis 命令的通用方法 专门用来执行 Lua 脚本

  1. UNLOCK_SCRIPT

提前写好的 Lua 解锁脚本

java 复制代码
   //声明,方便后面调用Lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
Lua 复制代码
//Lua脚本
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0
  1. Collections.singletonList(KEY_PREFIX + name)

传给 Lua 脚本的 KEYS(Redis 的 key)

  • KEY_PREFIX + name = 锁的 key例如:lock:order:101

  • Collections.singletonList() = 转成单元素列表因为 Lua 脚本要求 KEYS 必须是列表格式


  1. ID_PREFIX + Thread.currentThread().getId()

传给 Lua 脚本的 ARGV(参数值)

  • Thread.currentThread().getId():当前线程 ID

  • ID_PREFIX:前缀,避免重复

  • 组合起来 = 当前线程的唯一标识 例如:uuid-10086

作用:用来判断 Redis 里存的锁,是不是当前线程加的

实战篇 - 17. 分布式锁 - Redisson 功能介绍

实战篇 - 18. 分布式锁 - Redisson 快速入门

实战篇 - 19. 分布式锁 - Redisson 的可重入锁原理

问题:exists和Hexists有什么区别?

实战篇 - 20. 分布式锁 - Redisson的锁重试和WatchDog机制

问题:redission是怎么解决上图四个问题的?

01 问题:不可重入(同一线程无法多次获取同一把锁)

问题本质

普通 SETNX 锁是「一把锁一个值」,同一个线程第二次加锁时,SETNX 会直接失败,无法重入,导致死锁。

Redisson 解决方案:可重入锁(基于 Hash 结构存储)

Redisson 用 Hash 数据结构 存储锁信息,结构如下:

  • Key :锁的名称(如 lock:order:1001

  • Field :当前线程的唯一标识(UUID + 线程ID,全局唯一)

  • Value:重入次数(计数器)

加锁逻辑(Lua 脚本原子实现):

  1. 先判断锁是否存在:

    • 不存在 → 直接设置 Hash 结构,重入次数设为 1,设置过期时间,加锁成功

    • 存在 → 判断当前线程标识是否在 Hash 中:

      • 存在 → 重入次数 +1,刷新过期时间,加锁成功(可重入)

      • 不存在 → 加锁失败

  2. 解锁时,重入次数 -1,直到次数为 0 才真正删除锁。

核心优势:

同一个线程可以多次获取同一把锁,不会死锁,完美解决不可重入问题。


02 问题:不可重试(获取锁只尝试一次就返回 false,无重试机制)

问题本质

普通 SETNX 锁加锁失败直接返回,无法自动重试,高并发下容易导致业务失败。

Redisson 解决方案:自旋重试 + 可配置等待时间

Redisson 提供了 lock() / tryLock() 两种加锁方式,支持灵活的重试机制:

  • lock():阻塞式加锁,底层通过「自旋 + 信号量」不断重试,直到获取到锁为止,不会直接返回失败

  • tryLock(long waitTime, long leaseTime, TimeUnit unit)

    • waitTime:最大等待时间(在这个时间内不断重试加锁)

    • leaseTime:锁的持有时间

    • 超时未获取到锁才返回 false,支持自定义重试策略

底层实现:

通过 订阅锁释放的消息 实现高效重试:

  1. 加锁失败时,订阅锁释放的 Pub/Sub 消息

  2. 等待锁释放后,被唤醒并重新尝试加锁

  3. 避免了无效的空转,大幅提升性能,解决了不可重试问题


03 问题:超时释放(业务执行过长导致锁提前释放,有安全隐患)

问题本质

普通 SETNX 锁设置固定过期时间,若业务执行时间超过过期时间,锁会自动释放,导致其他线程加锁成功,出现并发安全问题。

Redisson 解决方案:看门狗(WatchDog)自动续期机制

Redisson 内置了 看门狗(WatchDog) 线程,核心逻辑:

  1. 若用户未手动指定 leaseTime(锁过期时间),默认锁过期时间为 30s

  2. 看门狗每隔 30s / 3 = 10s 检查一次:

    • 若当前线程仍持有锁 → 自动将锁的过期时间重置为 30s(续期)

    • 若线程已释放锁 → 停止续期

  3. 只要业务未执行完成,锁就会一直续期,不会提前释放;业务执行完成后,主动释放锁,看门狗自动停止。

核心优势:

彻底解决了「业务超时导致锁提前释放」的安全隐患,同时避免了死锁(业务异常时,锁到期自动释放)。


04 问题:主从一致性(Redis 主从集群,主从同步延迟导致锁失效)

问题本质

普通 SETNX 锁只在主节点加锁,若主节点宕机,主从同步未完成,从节点升为主节点后,锁数据丢失,导致多个线程同时加锁成功,锁完全失效。

Redisson 解决方案:RedLock 红锁 + 多实例锁(MultiLock)

Redisson 提供了两种方案解决主从一致性问题:

方案 1:RedLock(红锁,官方推荐)

基于 Redis 官方的 RedLock 算法,核心逻辑:

  1. N 个独立的 Redis 主节点(至少 3 个,互不相关)同时加锁

  2. 只有当 超过 N/2 个节点加锁成功 ,且总耗时小于锁过期时间,才认为加锁成功

  3. 解锁时,向所有节点发送解锁请求

  4. 即使单个节点宕机,只要多数节点锁有效,锁就不会失效,彻底解决主从同步延迟问题。

方案 2:MultiLock(多实例锁)

针对 Redis 主从集群 / 哨兵集群,将多个主节点的锁组合成一把「联锁」,只有所有节点加锁成功,锁才生效,保证锁的全局一致性。

补充:主从环境下的优化

即使使用普通的 RLock,Redisson 也会优先在主节点操作,同时通过「锁的唯一标识 + 原子操作」保证锁的安全性,最大程度降低主从同步延迟的影响。

问题 核心痛点 Redisson 解决方案 核心原理
不可重入 同一线程无法多次加锁 可重入锁(Hash 结构存储) 用 Hash 存储线程标识 + 重入次数,支持多次加锁
不可重试 加锁失败直接返回,无重试 自旋重试 + 可配置等待时间 阻塞式加锁 + Pub/Sub 消息唤醒,支持自定义重试
超时释放 业务超时导致锁提前释放 看门狗(WatchDog)自动续期 定时检查锁状态,业务未完成则自动续期
主从一致性 主从同步延迟导致锁失效 RedLock 红锁 / MultiLock 多实例锁 多节点独立加锁,多数节点成功才生效,保证全局一致

问题:这里面的ttl是什么?

实战篇 - 21. 分布式锁 - Redisson 的 multiLock 原理

实战篇 - 22. 秒杀优化 - 异步秒杀思路

问题:如何在Redis中完成对秒杀库存的判断和校验一人一单?

实战篇 - 23. 秒杀优化 - 基于 Redis 完成秒杀资格判断

问题:下图Lua代码表示什么?

Lua 复制代码
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
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.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

问题:解释下面代码?

Lua 复制代码
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)

redis.call()

Lua 脚本里调用 Redis 命令的固定写法

xadd

Redis Stream 消息队列添加消息命令= 往消息队列里扔一条消息

stream.orders

消息队列的名称 你可以理解为:订单专用消息队列

*

自动生成消息 IDRedis 自动生成唯一 ID,不用我们自己传

后面的 key value key value...

要发送的消息内容(订单数据):

  • userId:用户 ID
  • voucherId:优惠券 ID
  • id:订单 ID

问题:这个intValue是干嘛的?

Lua 复制代码
        // 1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString(), String.valueOf(orderId)
        );
        int r = result.intValue();

intValue() 就是把 Lua 返回的 Long 类型数字,转成 int 类型数字,方便后面做判断,只是类型转换!

实战篇 - 24. 秒杀优化 - 基于阻塞队列实现秒杀异步下单

问题:实现阻塞队列代码?

java 复制代码
//线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
    private class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while (true){
                try {
                    // 1.获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2.创建订单
                    createVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }

实战篇 - 25.Redis 消息队列 - 认识消息队列

实战篇 - 26.Redis 消息队列 - 基于 List 实现消息队列

实战篇 - 27.Redis 消息队列 - PubSub 实现消息队列

实战篇 - 28.Redis 消息队列 - Stream 的单消费模式

实战篇 - 29.Redis 消息队列 - Stream 的消费者组模式

问题:下面代码中>和0区别?

  • > = 只消费新消息,用于正常实时消费
  • 0 = 消费历史待处理消息,用于重试 / 故障恢复

问题:为什么有isEmpty()还需要null判断?

实战篇 - 30.Redis 消息队列 - 基于 Stream 消息队列实现异步秒杀

问题:解释下面代码?

末尾页

本文摘要: 本文详细讲解了分布式系统中优惠券秒杀的核心技术实现。主要内容包括:1)使用Redis原子自增实现全局唯一ID生成;2)通过乐观锁解决库存超卖问题;3)实现一人一单功能;4)Redis分布式锁的实现与优化,包括Lua脚本保证原子性和Redisson的可重入锁;5)秒杀优化方案,包括异步秒杀思路、Redis资格判断和消息队列实现;6)Redis消息队列的多种实现方式对比。文章通过代码示例详细解析了各项技术的实现原理和解决方案,涵盖了分布式系统开发中的典型并发问题和解决思路。

相关推荐
七夜zippoe2 小时前
DolphinDB入门:时序数据库的正确打开方式
数据库·struts·时序数据库·工业互联网·dolphindb
数厘2 小时前
2.4MySQL安装配置指南(电商数据分析专用)
数据库·mysql·数据分析
一只小白0002 小时前
数据库对象实例化流程模板 + 常见错误
数据库
一江寒逸2 小时前
零基础从入门到精通MySQL(下篇):精通篇——吃透索引底层、锁机制与性能优化,成为MySQL实战高手
数据库·mysql·性能优化
DevOpenClub2 小时前
全国三甲医院主体信息 API 接口
java·大数据·数据库
一勺菠萝丶3 小时前
管理后台使用手册在线预览与首次登录引导弹窗实现
java·前端·数据库
无忧智库3 小时前
某大型银行“十五五”金融大模型风控与智能投顾平台建设方案深度解读(WORD)
数据库·金融
爱码小白3 小时前
数据库多表命名的通用规范
数据库·python·mysql
huohuopro3 小时前
Hbase伪分布式远程访问配置
数据库·分布式·hbase