当前位置: 首页 > news >正文

Redis分布式锁详解:原理、实现与实战案例

目录

1. 什么是分布式锁?

分布式锁的核心要求

2. 基于Redis的分布式锁实现方案

(1)基础方案:SETNX + EXPIRE

(2)优化方案:SET NX PX(原子性加锁)

(3)进阶方案:RedLock(Redis官方推荐)

3. 实战案例

案例1:防止重复下单

1. 加锁阶段

2. 业务逻辑阶段

3. 释放锁阶段

案例2:秒杀库存扣减

案例3:分布式定时任务调度

4. 常见问题与解决方案

(1)锁过期但业务未执行完?

(2)锁被其他客户端误删?

(3)Redis主从切换导致锁丢失?

5. 总结


1. 什么是分布式锁?

在分布式系统中,多个服务实例可能同时访问共享资源(如数据库、缓存等),为了避免并发问题(如超卖、重复提交等),我们需要一种跨JVM的锁机制——分布式锁

分布式锁的核心要求

  1. 互斥性:同一时刻只有一个客户端能持有锁。

  2. 防死锁:即使客户端崩溃,锁也能自动释放。

  3. 高可用:锁服务必须高可用(如Redis集群)。

  4. 可重入性(可选):同一个客户端可以多次获取同一把锁。


2. 基于Redis的分布式锁实现方案

Redis因其高性能和原子性操作(如SETNX),成为实现分布式锁的常用方案。

(1)基础方案:SETNX + EXPIRE

// 加锁(错误示范,非原子性)
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order123", "1");
if (locked) {redisTemplate.expire("lock:order123", 10, TimeUnit.SECONDS);  // 设置过期时间// 执行业务逻辑...redisTemplate.delete("lock:order123");  // 释放锁
}

问题SETNXEXPIRE不是原子操作,如果加锁后客户端崩溃,锁永远不会释放!


(2)优化方案:SET NX PX(原子性加锁)

Redis 2.6+ 支持SET命令的NX(不存在才设置)和PX(毫秒级过期时间)参数:

// 正确方式:原子性加锁 + 设置过期时间
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order123", "client1", 10, TimeUnit.SECONDS
);if (locked) {try {// 执行业务逻辑...} finally {// 释放锁(需判断是否是自己加的锁)if ("client1".equals(redisTemplate.opsForValue().get("lock:order123"))) {redisTemplate.delete("lock:order123");}}
}

关键改进

  • 使用SET NX PX保证原子性。

  • 设置唯一标识(如client1),避免误删其他客户端的锁。


(3)进阶方案:RedLock(Redis官方推荐)

如果单点Redis不可靠,可以使用RedLock算法(需多个独立Redis实例):

// RedLock示例(使用Redisson客户端)
RLock lock1 = redissonClient1.getLock("lock:order123");
RLock lock2 = redissonClient2.getLock("lock:order123");
RLock lock3 = redissonClient3.getLock("lock:order123");RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {if (redLock.tryLock(10, 30, TimeUnit.SECONDS)) {  // 最多等待10秒,锁30秒后自动过期// 执行业务逻辑...}
} finally {redLock.unlock();
}

适用场景:对一致性要求极高的场景(如金融交易)。


3. 实战案例

案例1:防止重复下单

public String createOrder(String userId, String productId) {String lockKey = "lock:order:" + userId + ":" + productId;String clientId = UUID.randomUUID().toString();  // 唯一标识try {// 尝试加锁(等待5秒,锁10秒后自动释放)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("操作太频繁,请稍后再试!");}// 检查是否已下单if (orderService.hasOrder(userId, productId)) {throw new RuntimeException("请勿重复下单!");}// 创建订单...return orderService.create(userId, productId);} finally {// 释放锁(需校验clientId)if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}}
}

这段代码实现了一个防并发重复下单的订单创建逻辑,核心是使用Redis分布式锁来保证同一用户对同一商品的订单操作是串行化的。下面逐部分解析:


1. 加锁阶段
String lockKey = "lock:order:" + userId + ":" + productId;
String clientId = UUID.randomUUID().toString();
  • lockKey:锁的键,格式为lock:order:{userId}:{productId},确保不同用户或不同商品的锁互不影响。

  • clientId:生成唯一标识(UUID),用于后续校验锁的归属,防止误删其他客户端的锁。

Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS
);
  • setIfAbsent:Redis的SETNX命令(原子性操作),如果lockKey不存在则加锁,并设置:

    • clientId(锁的持有者标识)。

    • 过期时间:10秒(防止死锁)。

  • 返回值true表示加锁成功,false表示锁已被占用。

if (!locked) {throw new RuntimeException("操作太频繁,请稍后再试!");
}
  • 如果加锁失败,直接抛出异常,提示用户"操作太频繁"(类似秒杀场景的限流)。


2. 业务逻辑阶段
if (orderService.hasOrder(userId, productId)) {throw new RuntimeException("请勿重复下单!");
}
  • 检查是否已下单:在锁的保护下查询订单系统,防止重复下单(即使通过了前端校验,仍需后端保证幂等性)。

return orderService.create(userId, productId);
  • 创建订单:执行业务逻辑(如扣减库存、生成订单等)。


3. 释放锁阶段
finally {if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}
}
  • finally:确保锁一定会被释放,即使业务逻辑抛出异常。

  • 校验clientId

    • 只释放自己加的锁(避免误删其他客户端的锁)。

    • 如果锁已自动过期(10秒后),get操作返回null,不会执行删除。

  • 原子性问题

    • 这里的getdelete是两步操作,非原子性,极端情况下可能误删锁(如锁过期后,其他客户端加锁成功,但当前线程仍执行删除)。

    • 改进方案:使用Lua脚本保证原子性(见下文补充)。


案例2:秒杀库存扣减

public boolean seckill(Long productId, Long userId) {String lockKey = "lock:seckill:" + productId;String stockKey = "stock:" + productId;String clientId = UUID.randomUUID().toString();try {// 加锁(防止超卖)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 3,  // 锁3秒(避免长时间阻塞)TimeUnit.SECONDS);if (!locked) {return false;  // 抢锁失败}// 检查库存Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(stockKey));if (stock <= 0) {return false;  // 已售罄}// 扣减库存(原子操作)redisTemplate.opsForValue().decrement(stockKey);// 生成订单...orderService.createSeckillOrder(userId, productId);return true;} finally {// 释放锁if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}}
}

案例3:分布式定时任务调度

多个服务实例同时运行定时任务时,需确保只有一个实例执行:

@Scheduled(cron = "0 */5 * * * ?")  // 每5分钟执行一次
public void scheduledTask() {String lockKey = "lock:scheduled:report";String clientId = "server-" + System.getProperty("server.port");  // 用服务实例标识try {// 尝试加锁(锁5分钟)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 5, TimeUnit.MINUTES);if (!locked) {return;  // 其他实例已执行}// 执行业务逻辑(生成报表...)reportService.generateDailyReport();} finally {// 释放锁if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}}
}

4. 常见问题与解决方案

(1)锁过期但业务未执行完?

  • 问题:锁自动释放后,其他客户端可能获取锁,导致并发问题。

  • 解决方案:使用看门狗机制(如Redisson的lockWatchdogTimeout),自动续期锁。

(2)锁被其他客户端误删?

  • 问题:客户端A释放了客户端B的锁。

  • 解决方案:加锁时设置唯一标识(如UUID),释放时校验。

(3)Redis主从切换导致锁丢失?

  • 问题:主节点加锁后崩溃,从节点晋升但未同步锁数据。

  • 解决方案:使用RedLock(多Redis实例)或ZooKeeper替代。


5. 总结

方案优点缺点适用场景
SETNX + EXPIRE简单高效非原子性,可能死锁低并发场景
SET NX PX原子操作单点故障一般分布式系统
RedLock高可用实现复杂金融级高一致性场景

最佳实践

  • 优先使用SET NX PX + 唯一标识。

  • 高可用场景选择Redisson的RLock或RedLock。

  • 结合业务设置合理的锁超时时间。

http://www.dtcms.com/a/322833.html

相关文章:

  • 【C++11新特性】智能指针,右值引用,移动语义与完美转发,函数对象...
  • Linux运维新手的修炼手扎之第27天
  • pyqt5 ECU编辑demo
  • NX二次开发——面有关的函数
  • 1.2.3 迅猛发展期(2020年至今)
  • 让大模型 “睡觉”:把版本迭代当作人类睡眠来设计(附可直接改造的训练作息表与代码)
  • 104-基于Flask的优衣库销售数据可视化分析系统
  • 100-基于Python的智联招聘数据可视化分析推荐系统
  • 一周学会Matplotlib3 Python 数据可视化-网格 (Grid)
  • 力扣(删除有序数组中的重复项I/II)
  • [优选算法专题一双指针——四数之和]
  • 配送算法10 Batching and Matching for Food Delivery in Dynamic Road Networks
  • Java 8特性(一)
  • 新手向:Python开发简易待办事项应用
  • 顺风车软件系统架构分析
  • 大语言模型提示工程与应用:ChatGPT提示工程技术指南
  • PDF编辑工具,免费OCR识别表单
  • ST语法介绍
  • GloVe词向量:原理详解及用python进行训练和应用GloVe
  • 【第四章:大模型(LLM)】05.LLM实战: 实现GPT2-(1)文本token到Embedding的代码实现
  • 【数据分享】各省农业土地流转率(2010-2023)
  • Easysearch 冷热架构实战
  • 分治-快排-面试题 17.14.最小k个数-力扣(LeetCode)
  • Redhat Linux 9.6 配置本地 yum 源
  • 【数据结构入门】栈和队列
  • 网盘短剧资源转存项目源码 支持垮克 带后台 附教程
  • Kafka服务端NIO操作原理解析(二)
  • MX 播放器:安卓设备上的全能视频播放器
  • 【解决方法】华为电脑的亮度调节失灵
  • 本地部署接入 whisper + ollama qwen3:14b 总结字幕