Redis 分布式锁

一、定义

1. 基本概念

Redis 分布式锁是基于 Redis 单线程特性实现的分布式协同工具,用于解决多进程 / 多节点间的资源竞争问题。

二、特点

1. 轻量高效

  • 基于内存操作,性能远高于数据库锁;

2. 原子性

  • 通过 Redis 原子命令 / Lua 脚本保证加锁、解锁操作的原子性;

3. 可扩展

  • 支持单节点、多节点(Redlock)部署,适配不同可用性要求;

4. 易集成

  • 支持多种客户端(Go/Java/Python),适配分布式系统架构。

三、核心特性

1. 互斥性

  • 核心特性,同一时刻仅允许一个客户端持有锁,避免资源竞争

2. 原子性

  • 加锁、解锁操作必须原子化,避免中间态导致的锁异常

3. 超时释放

  • 避免客户端持有锁后宕机导致死锁,需设置合理的锁超时时间

4. 高可用性

  • 避免 Redis 单点故障导致锁服务不可用,支持多节点部署(Redlock)

5. 可重入性

  • 可选特性,同一客户端多次获取同一把锁不阻塞(需额外实现)

6. 公平性

  • 可选特性,按请求顺序获取锁(Redis 原生不支持,需额外设计)

7. 容错性

  • 部分 Redis 节点故障时,锁服务仍能正常工作(Redlock 保证)

四、实现原理

1. 核心基础

Redis 分布式锁的核心依赖其 单线程模型(命令串行执行)和 原子命令,核心逻辑:

  • 加锁:通过 SET key value NX EX seconds 命令实现原子加锁(NX = 仅当 key 不存在时设置,EX = 自动过期时间);
  • 解锁:通过 Lua 脚本实现「判断唯一标识 + 删除锁」的原子操作,避免误删其他客户端的锁;
  • 高可用:Redlock 算法通过多 Redis 实例加锁(超过半数实例加锁成功则认为锁有效),解决单点故障问题

2. 核心命令解析

  • SET lock_key unique_val NX EX 10:原子加锁,lock_key 为锁名,unique_val 为客户端唯一标识,NX 保证互斥,EX 设置 10s 超时
  • Lua 脚本解锁:原子判断 unique_val 是否匹配,匹配则删除锁,避免误删

五、业务场景

1. 电商超卖 / 库存扣减

  • 场景:多节点并发扣减库存时,避免库存数量为负;
  • 核心:通过分布式锁保证库存扣减操作的原子性。

2. 秒杀活动

  • 场景:限制商品秒杀的并发抢购,避免超量下单;
  • 核心:锁粒度控制(商品维度),避免全量锁导致性能瓶颈。

3. 分布式任务调度

  • 场景:多节点定时任务避免重复执行(如数据同步、报表生成);
  • 核心:通过锁保证同一任务仅一个节点执行

4. 订单状态更新

  • 场景:多系统并发更新订单状态(如支付、发货),避免状态不一致;
  • 核心:锁绑定订单 ID,保证状态更新的互斥性。

5. 缓存更新

  • 场景:避免多节点同时更新缓存导致的缓存击穿;
  • 核心:加锁后更新缓存,未获取锁的节点等待或直接读取数据库

六、电商超卖场景:分布式锁渐进式实现(Go)

1. 场景背景

电商商品库存为 100,多节点并发下单扣减库存,若未做并发控制,会出现库存为负(超卖)。以下通过 6 个版本逐步优化分布式锁实现。

2. 依赖准备

package main

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"github.com/go-redis/redis/v8"
	"math/big"
	"sync"
	"time"
)

// 全局变量
var (
	// Redis 客户端(Redis 8.4 兼容 go-redis/v8)
	redisClient = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})
	ctx = context.Background()
	// 模拟库存(实际应存储在 Redis/数据库)
	stock = 100
	stockMu sync.Mutex // 本地锁保护库存变量
)

// 生成客户端唯一标识(避免误删锁)
func generateUniqueID() string {
	b := make([]byte, 16)
	rand.Read(b)
	return base64.URLEncoding.EncodeToString(b)
}

3. 版本 1:基础 SETNX 实现(存在死锁风险)

(1)代码实现

// 加锁(版本1:仅SETNX,无超时)
func lockV1(lockKey string) bool {
	// SETNX:key不存在时返回1,存在返回0
	result, err := redisClient.SetNX(ctx, lockKey, "default", 0).Result()
	if err != nil {
		fmt.Println("加锁失败:", err)
		return false
	}
	return result
}

// 解锁(版本1:直接DEL)
func unlockV1(lockKey string) {
	_, err := redisClient.Del(ctx, lockKey).Result()
	if err != nil {
		fmt.Println("解锁失败:", err)
	}
}

// 扣减库存(版本1)
func deductStockV1() {
	lockKey := "lock:stock:1001"
	// 加锁
	if !lockV1(lockKey) {
		fmt.Println("获取锁失败,放弃扣减")
		return
	}

	// 业务逻辑:扣减库存
	stockMu.Lock()
	if stock > 0 {
		stock--
		fmt.Printf("库存扣减成功,剩余库存:%d\n", stock)
	} else {
		fmt.Println("库存不足,扣减失败")
	}
	stockMu.Unlock()

	// 解锁
	unlockV1(lockKey)
}

(2)问题分析

  • 死锁风险:若加锁后服务宕机 / 进程崩溃,DEL 未执行,锁永远无法释放,导致后续所有请求无法获取锁;
  • 无超时机制:锁没有自动过期,一旦异常则死锁
  • 加锁时,缺少自旋机制(多次加锁重试)

4. 版本 2:SETNX + EXPIRE(非原子,仍有问题)

(1)代码实现

// 加锁(版本2:SETNX + EXPIRE)
func lockV2(lockKey string, expire int) bool {
	// 第一步:SETNX加锁
	result, err := redisClient.SetNX(ctx, lockKey, "default", 0).Result()
	if err != nil || !result {
		fmt.Println("加锁失败:", err)
		return false
	}

	// 第二步:设置过期时间
	_, err = redisClient.Expire(ctx, lockKey, time.Duration(expire)*time.Second).Result()
	if err != nil {
		fmt.Println("设置过期时间失败:", err)
		// 回滚:删除已加的锁
		redisClient.Del(ctx, lockKey)
		return false
	}
	return true
}

// 扣减库存(版本2)
func deductStockV2() {
	lockKey := "lock:stock:1001"
	// 加锁(超时10s)
	if !lockV2(lockKey, 10) {
		fmt.Println("获取锁失败,放弃扣减")
		return
	}

	// 业务逻辑:扣减库存
	stockMu.Lock()
	if stock > 0 {
		stock--
		fmt.Printf("库存扣减成功,剩余库存:%d\n", stock)
	} else {
		fmt.Println("库存不足,扣减失败")
	}
	stockMu.Unlock()

	// 解锁
	unlockV1(lockKey)
}

(2)优化点

  • 增加超时过期

(3)问题分析

  • 非原子操作:SETNX 和 EXPIRE 是两个独立命令,若 SETNX 成功后、EXPIRE 执行前服务宕机,仍会导致锁无超时,引发死锁;
  • 无唯一标识:解锁时直接 DEL,可能误删其他客户端持有的锁(比如锁超时自动释放后,其他客户端加锁,当前客户端执行 DEL)

5. 版本 3:原子 SET NX EX + 唯一标识(解决原子加锁 + 避免误删)

(1)代码实现

// 加锁(版本3:原子SET NX EX + 唯一标识)
func lockV3(lockKey string, expire int) (string, bool) {
	// 生成客户端唯一标识
	uniqueID := generateUniqueID()
	// SET key value NX EX seconds:原子加锁+超时+唯一标识
	result, err := redisClient.Set(ctx, lockKey, uniqueID, time.Duration(expire)*time.Second).SetNX().Result()
	if err != nil || !result {
		fmt.Println("加锁失败:", err)
		return "", false
	}
	return uniqueID, true
}

// 解锁(版本3:先判断唯一标识,再删除)
func unlockV3(lockKey, uniqueID string) {
	// 第一步:获取锁的value
	val, err := redisClient.Get(ctx, lockKey).Result()
	if err != nil {
		fmt.Println("获取锁标识失败:", err)
		return
	}

	// 第二步:判断是否为当前客户端的标识
	if val == uniqueID {
		// 第三步:删除锁
		_, err = redisClient.Del(ctx, lockKey).Result()
		if err != nil {
			fmt.Println("解锁失败:", err)
		}
	} else {
		fmt.Println("锁标识不匹配,拒绝解锁")
	}
}

// 扣减库存(版本3)
func deductStockV3() {
	lockKey := "lock:stock:1001"
	// 加锁(超时10s)
	uniqueID, ok := lockV3(lockKey, 10)
	if !ok {
		fmt.Println("获取锁失败,放弃扣减")
		return
	}

	// 业务逻辑:扣减库存
	stockMu.Lock()
	if stock > 0 {
		stock--
		fmt.Printf("库存扣减成功,剩余库存:%d\n", stock)
	} else {
		fmt.Println("库存不足,扣减失败")
	}
	stockMu.Unlock()

	// 解锁
	unlockV3(lockKey, uniqueID)
}

(2)优化点

  • 用 SET NX EX 原子命令替代 SETNX + EXPIRE,解决加锁 + 超时的原子性;
  • 引入唯一标识,解锁前先校验,避免误删其他客户端的锁。

(3)问题分析

  • 解锁操作(GET + DEL)非原子:若 GET 后、DEL 前,锁超时自动释放,其他客户端加锁,当前客户端仍会执行 DEL,误删新锁;
  • Redis 单点故障:若 Redis 节点宕机,锁服务不可用。

6. 版本 4:Lua 脚本原子解锁(解决解锁原子性)

(1)代码实现

// 解锁(版本4:Lua脚本原子解锁)
func unlockV4(lockKey, uniqueID string) error {
	// Lua脚本:判断value匹配则删除,保证原子性
	unlockScript := `
		if redis.call('get', KEYS[1]) == ARGV[1] then
			return redis.call('del', KEYS[1])
		else
			return 0
		end
	`
	// 执行Lua脚本
	result, err := redisClient.Eval(ctx, unlockScript, []string{lockKey}, uniqueID).Result()
	if err != nil {
		return fmt.Errorf("执行解锁脚本失败:%w", err)
	}
	if res, ok := result.(int64); !ok || res == 0 {
		return errors.New("锁标识不匹配或锁已过期,解锁失败")
	}
	return nil
}

// 扣减库存(版本4)
func deductStockV4() {
	lockKey := "lock:stock:1001"
	// 加锁(超时10s)
	uniqueID, ok := lockV3(lockKey, 10)
	if !ok {
		fmt.Println("获取锁失败,放弃扣减")
		return
	}

	// 模拟业务耗时(比如网络请求)
	time.Sleep(2 * time.Second)

	// 业务逻辑:扣减库存
	stockMu.Lock()
	if stock > 0 {
		stock--
		fmt.Printf("库存扣减成功,剩余库存:%d\n", stock)
	} else {
		fmt.Println("库存不足,扣减失败")
	}
	stockMu.Unlock()

	// 解锁
	if err := unlockV4(lockKey, uniqueID); err != nil {
		fmt.Println("解锁失败:", err)
	}
}

(2)优化点

  • 用 Lua 脚本将「判断标识 + 删除锁」封装为原子操作,彻底解决误删锁问题

(3)问题分析

  • 锁超时问题:若业务执行时间超过锁超时时间(比如耗时 15s,锁超时 10s),锁会自动释放,其他客户端加锁,导致并发扣减;
  • Redis 单点故障:Redis 主节点宕机,从节点未同步锁数据,导致锁失效

7. 版本 5:Redlock 算法(解决 Redis 单点故障)

(1)代码实现

// Redlock 分布式锁结构体
type RedLock struct {
	clients []*redis.Client // 多Redis实例客户端
	quorum  int             // 最小成功节点数(len(clients)/2 + 1)
}

// NewRedLock 初始化Redlock
func NewRedLock(addrs []string) *RedLock {
	clients := make([]*redis.Client, len(addrs))
	for i, addr := range addrs {
		clients[i] = redis.NewClient(&redis.Options{
			Addr:     addr,
			Password: "",
			DB:       0,
		})
	}
	return &RedLock{
		clients: clients,
		quorum:  len(clients)/2 + 1,
	}
}

// Lock Redlock加锁
func (rl *RedLock) Lock(lockKey string, expire int) (string, bool) {
	uniqueID := generateUniqueID()
	successCount := 0
	// 记录开始时间(用于判断超时)
	start := time.Now()

	// 遍历所有Redis实例加锁
	for _, client := range rl.clients {
		result, err := client.Set(ctx, lockKey, uniqueID, time.Duration(expire)*time.Second).SetNX().Result()
		if err == nil && result {
			successCount++
		}
		// 超过半数实例加锁成功,且总耗时 < 锁超时时间的1/3,认为加锁成功
		if successCount >= rl.quorum && time.Since(start) < time.Duration(expire)*time.Second/3 {
			return uniqueID, true
		}
	}

	// 加锁失败,回滚已加锁的节点
	if successCount > 0 {
		rl.Unlock(lockKey, uniqueID)
	}
	return "", false
}

// Unlock Redlock解锁
func (rl *RedLock) Unlock(lockKey, uniqueID string) {
	unlockScript := `
		if redis.call('get', KEYS[1]) == ARGV[1] then
			return redis.call('del', KEYS[1])
		else
			return 0
		end
	`
	// 遍历所有Redis实例解锁
	for _, client := range rl.clients {
		client.Eval(ctx, unlockScript, []string{lockKey}, uniqueID)
	}
}

// 扣减库存(版本5:Redlock)
func deductStockV5(rl *RedLock) {
	lockKey := "lock:stock:1001"
	// 加锁(超时10s)
	uniqueID, ok := rl.Lock(lockKey, 10)
	if !ok {
		fmt.Println("获取Redlock失败,放弃扣减")
		return
	}

	// 模拟业务耗时
	time.Sleep(2 * time.Second)

	// 业务逻辑:扣减库存
	stockMu.Lock()
	if stock > 0 {
		stock--
		fmt.Printf("库存扣减成功,剩余库存:%d\n", stock)
	} else {
		fmt.Println("库存不足,扣减失败")
	}
	stockMu.Unlock()

	// 解锁
	rl.Unlock(lockKey, uniqueID)
}

(2)优化点

  • 基于 Redlock 算法,多 Redis 实例(至少 3 个独立节点)加锁,超过半数节点加锁成功则认为锁有效,解决单点故障问题。

(3)问题分析

  • 锁超时问题仍存在;
  • Redlock 部署成本高(需多个独立 Redis 节点),性能略低于单节点。

8. 版本 6:带看门狗的可重入锁(解决锁超时 + 支持重入)

(1)代码实现

// 可重入分布式锁结构体(带看门狗)
type ReentrantLock struct {
	client     *redis.Client
	lockKey    string
	uniqueID   string        // 客户端唯一标识
	expire     time.Duration // 锁基础超时
	count      int           // 重入次数
	watchdog   *time.Ticker  // 看门狗定时器
	ctx        context.Context
	cancelFunc context.CancelFunc
}

// NewReentrantLock 初始化可重入锁
func NewReentrantLock(client *redis.Client, lockKey string, expire time.Duration) *ReentrantLock {
	ctx, cancel := context.WithCancel(context.Background())
	return &ReentrantLock{
		client:     client,
		lockKey:    lockKey,
		uniqueID:   generateUniqueID(),
		expire:     expire,
		ctx:        ctx,
		cancelFunc: cancel,
	}
}

// Lock 加锁(支持重入)
func (rl *ReentrantLock) Lock() bool {
	// 重入判断:若已持有锁,直接增加重入次数
	if rl.count > 0 {
		rl.count++
		return true
	}

	// 首次加锁:原子SET NX EX
	result, err := rl.client.Set(ctx, rl.lockKey, rl.uniqueID, rl.expire).SetNX().Result()
	if err != nil || !result {
		return false
	}

	// 启动看门狗:每 expire/3 时间续期一次
	rl.count = 1
	rl.watchdog = time.NewTicker(rl.expire / 3)
	go func() {
		for {
			select {
			case <-rl.watchdog.C:
				// Lua脚本:判断标识匹配则续期
				renewScript := `
					if redis.call('get', KEYS[1]) == ARGV[1] then
						return redis.call('expire', KEYS[1], ARGV[2])
					else
						return 0
					end
				`
				rl.client.Eval(ctx, renewScript, []string{rl.lockKey}, rl.uniqueID, int(rl.expire.Seconds()))
			case <-rl.ctx.Done():
				rl.watchdog.Stop()
				return
			}
		}
	}()
	return true
}

// Unlock 解锁(支持重入)
func (rl *ReentrantLock) Unlock() error {
	// 重入次数减1
	if rl.count > 1 {
		rl.count--
		return nil
	}

	// 最后一次解锁:停止看门狗 + 原子删除锁
	rl.cancelFunc()
	unlockScript := `
		if redis.call('get', KEYS[1]) == ARGV[1] then
			return redis.call('del', KEYS[1])
		else
			return 0
		end
	`
	result, err := rl.client.Eval(ctx, unlockScript, []string{rl.lockKey}, rl.uniqueID).Result()
	if err != nil {
		return err
	}
	if res, ok := result.(int64); !ok || res == 0 {
		return errors.New("锁已过期或标识不匹配")
	}
	rl.count = 0
	return nil
}

// 扣减库存(版本6:可重入+看门狗)
func deductStockV6(rl *ReentrantLock) {
	// 加锁
	if !rl.Lock() {
		fmt.Println("获取可重入锁失败,放弃扣减")
		return
	}

	// 模拟重入(比如嵌套调用)
	rl.Lock()

	// 模拟长耗时业务(超过基础超时,看门狗自动续期)
	time.Sleep(15 * time.Second)

	// 业务逻辑:扣减库存
	stockMu.Lock()
	if stock > 0 {
		stock--
		fmt.Printf("库存扣减成功,剩余库存:%d\n", stock)
	} else {
		fmt.Println("库存不足,扣减失败")
	}
	stockMu.Unlock()

	// 解锁(重入次数2,需解锁2次)
	if err := rl.Unlock(); err != nil {
		fmt.Println("第一次解锁失败:", err)
	}
	if err := rl.Unlock(); err != nil {
		fmt.Println("第二次解锁失败:", err)
	}
}

(2)优化点

  • 看门狗机制:定时(锁超时的 1/3)自动续期锁,解决业务耗时超过锁超时的问题;
  • 可重入性:通过本地计数 + 唯一标识,支持同一客户端多次加锁;
    • 也可以用 hash 解决可重入性
  • 原子操作:加锁 / 解锁 / 续期均通过原子命令 / Lua 脚本实现。

9. 测试代码(模拟并发超卖)

func main() {
	// 模拟100个并发请求扣减库存
	var wg sync.WaitGroup
	wg.Add(100)

	// 版本6测试(可重入+看门狗)
	rl := NewReentrantLock(redisClient, "lock:stock:1001", 10*time.Second)
	for i := 0; i < 100; i++ {
		go func() {
			defer wg.Done()
			deductStockV6(rl)
		}()
	}

	wg.Wait()
	fmt.Printf("最终库存:%d\n", stock) // 最终库存应为0,无超卖
}

七、Redlock

1. 现状

  • Redis 官方态度:Redis 作者 Salvatore Sanfilippo(antirez)明确表示 Redlock 存在设计缺陷,不推荐在高一致性要求的场景使用
  • 社区实践:主流分布式锁框架(如 Redisson)仍保留 Redlock 实现,但标注为「非强一致性」方案,且更多场景优先推荐单节点 + 主从 / 哨兵架构
  • 适用场景收缩:Redlock 仅在「Redis 单节点不可用、且无法接受主从切换导致的锁短暂失效」的小众场景中少量使用,绝大多数业务场景无需依赖

2. 争议的核心原因

核心缺陷使其无法保证「严格的分布式一致性」。

  • 时钟漂移问题:Redlock 依赖多个 Redis 节点的「时间一致性」;若某节点时钟跳变(如手动调整、NTP 同步异常),可能导致锁超时时间失效,进而引发多客户端同时持有锁,破坏互斥性
  • 性能与复杂度问题:
    • Redlock 需向至少 3 个独立 Redis 节点发起加锁请求,网络耗时是单节点锁的 3 倍以上,高并发场景下性能损耗显著;
    • 部署成本高(需独立 Redis 节点,不能复用主从集群),运维复杂度提升。
  • 一致性模型缺陷:Redlock 试图用「过半节点加锁成功」模拟分布式一致性,但未遵循严格的分布式一致性协议(如 Paxos/Raft),在节点网络分区、超时等异常场景下,仍可能出现锁失效

3. 替代案例:多重锁

八、常见问题

1. 死锁

  • 原因:加锁后服务宕机,锁无超时 / 超时未生效;
  • 解决方案:使用 SET NX EX 原子命令设置超时,避免手动 SETNX + EXPIRE

2. 误删锁

  • 原因:解锁时未校验唯一标识,或 GET + DEL 非原子;
  • 解决方案:加锁时设置唯一标识,解锁时用 Lua 脚本原子校验 + 删除

3. 锁超时

  • 原因:业务执行时间超过锁超时,锁自动释放;
  • 解决方案:看门狗定时续期,或预估合理的超时时间(略大于业务最大耗时)

4. 单点故障

  • 原因:Redis 单节点宕机,锁服务不可用;
  • 解决方案:Redlock 多节点部署,或 Redis 主从 + 哨兵架构

5. 重入问题

  • 原因:同一客户端多次加锁导致死锁;
  • 解决方案:实现可重入锁(本地计数 + 唯一标识)

6. 公平性问题

  • 原因:Redis 分布式锁为非公平锁,可能导致某些客户端长期获取不到锁;
  • 解决方案:结合有序集合(ZSET)实现公平锁(按请求时间排队)
    • score 记录请求时间戳

九、注意事项

1. 锁粒度设计

  • 避免「全量锁」(如整个库存系统一把锁),应按资源维度拆分(如商品 ID 维度),降低锁竞争;
  • 示例:锁名设计为 lock:stock:{商品ID},而非 lock:stock。

2. 超时时间设置

  • 超时时间需略大于业务最大执行时间(比如业务耗时 5s,超时设为 10s);
  • 避免超时过短(导致锁提前释放)或过长(死锁时影响范围大)

3. 看门狗实现

  • 看门狗需在单独 goroutine 中运行,避免阻塞业务;
  • 解锁时需停止看门狗,避免无效续期

4. Redlock 适用场景

  • Redlock 适合对可用性要求极高的场景(如金融、电商核心交易);
  • 普通场景可使用 Redis 主从 + 哨兵,无需过度设计

5. 异常处理

  • 加锁失败时,应重试(带退避策略)而非直接放弃;
  • 解锁失败时,需记录日志并监控,避免锁残留

十、面试题

1. 分布式锁的核心特性有哪些?Redis 如何保证这些特性?

(1)核心特性

  • 互斥性、原子性、超时释放、高可用、可重入

(2)Redis 实现

  • 互斥性:SET NX 命令保证同一时刻仅一个客户端加锁;
  • 原子性:SET NX EX 原子命令、Lua 脚本保证加锁 / 解锁原子性;
  • 超时释放:EX 参数设置自动过期,避免死锁;
  • 高可用:Redlock 多节点部署,或主从 + 哨兵架构;
  • 可重入:本地计数 + 唯一标识,多次加锁仅增加计数

2. Redis 分布式锁和 Zookeeper 分布式锁的对比?

(1)对比

image

3. Redis 实现分布式锁时,为什么要用 Lua 脚本解锁?直接 GET + DEL 不行吗?

  • 直接 GET + DEL 非原子:若 GET 后、DEL 前,锁超时自动释放,其他客户端加锁,当前客户端会误删新锁;
  • Lua 脚本将「判断标识 + 删除锁」封装为原子操作,Redis 单线程执行脚本,避免中间态,彻底解决误删问题

4. Redlock 算法的核心原理?有哪些缺陷?

(1)核心原理

  • 部署至少 3 个独立 Redis 节点;
  • 客户端向所有节点发送加锁请求;
  • 超过半数节点加锁成功,且总耗时 < 锁超时时间的 1/3,则认为加锁成功;
  • 解锁时向所有节点发送解锁请求

(2)缺陷

  • 部署成本高(需多个独立节点);
  • 性能低于单节点;
  • 时钟漂移可能导致锁失效(节点时间不一致)

5. 如何解决 Redis 分布式锁的超时问题?

  • 看门狗机制:客户端启动定时任务,每锁超时的 1/3 时间续期一次(Lua 脚本原子续期);
  • 预估合理超时:基于业务压测结果,设置略大于最大执行时间的超时;
  • 分段锁:将长耗时业务拆分为多个短任务,每个任务加锁执行,降低单次锁持有时间

6. 电商超卖问题的根本原因是什么?如何用分布式锁彻底解决?

(1)根本原因

  • 多节点并发扣减库存时,未保证库存操作的原子性(读 - 改 - 写非原子)

(2)分布式锁解决方案

  • 锁粒度:按商品 ID 加锁,避免全量锁;
  • 原子加锁:SET NX EX 原子命令,避免死锁;
  • 原子解锁:Lua 脚本校验唯一标识后删除,避免误删;
  • 看门狗续期:解决业务耗时超过锁超时的问题;
  • 兜底校验:扣减库存前再次校验库存是否大于 0,避免极端情况超卖

7. Redis 分布式锁在高并发场景下的性能优化手段?

  • 锁粒度拆分:按资源维度拆分锁(如商品 ID、用户 ID),降低锁竞争;
  • 本地缓存锁:热点资源的锁可本地缓存,减少 Redis 访问;
  • 批量解锁 / 加锁:合并多个锁操作,减少 Redis 网络 IO;
  • 非阻塞加锁:加锁失败时直接返回,而非阻塞等待,避免线程阻塞;
  • 连接池优化:增大 Redis 连接池,避免连接耗尽导致的性能瓶颈

8. Redis 分布式锁的可重入性如何实现?有哪些注意事项?

(1)实现方式

  • 加锁时生成唯一标识(客户端 ID + 线程 ID);
  • 本地维护重入计数,首次加锁计数为 1,重复加锁计数 + 1;
  • 解锁时计数 - 1,计数为 0 时才执行 Redis DEL 操作

(2)注意事项

  • 唯一标识需全局唯一,避免不同客户端的重入计数冲突;
  • 看门狗续期需与重入计数绑定,避免续期已释放的锁;
  • 进程崩溃时,本地计数丢失,需依赖 Redis 超时释放锁。

9. 如果 Redis 分布式锁失效,有哪些兜底方案?

  • 数据库乐观锁:库存表增加版本号,扣减时 WHERE 库存>0 AND 版本号=xxx,保证原子性;
  • Redis 原子操作:用 DECRBY 原子扣减库存,扣减前判断结果是否 >=0;
  • 限流兜底:网关层限制并发请求数,避免超出系统处理能力;
  • 监控告警:实时监控库存数量,超卖时立即告警并熔断下单接口。