一、分布式锁的基础认知
1.1 什么是分布式锁?
分布式锁是一种在分布式环境下,用于协调多个节点(进程或服务)对共享资源访问的同步机制。它能够保证在同一时间,只有一个节点可以获取到锁,从而独占对共享资源的操作权,避免并发问题导致的脏读、脏写、数据不一致等情况。
分布式锁与单机锁的区别
与单机环境下的锁(如 Java 中的 synchronized、ReentrantLock)相比,分布式锁面临更复杂的网络环境和节点故障问题:
- 网络延迟:锁请求和响应需要经过网络传输,存在不确定性
- 节点故障:持有锁的节点可能突然宕机,需要自动释放机制
- 时钟不同步:分布式系统中各节点时钟可能存在差异
核心特性要求
一个健壮的分布式锁需要满足以下核心特性:
-
互斥性:同一时间只能有一个节点获取到锁,确保共享资源的独占访问。
- 示例:在秒杀系统中,同一商品ID的库存扣减操作必须串行执行
-
安全性:避免出现死锁,即当持有锁的节点因故障无法释放锁时,其他节点仍能正常获取锁。
- 实现方式:通常通过设置锁的自动过期时间(TTL)
-
可用性:在分布式系统部分节点故障的情况下,锁服务仍能正常工作,不能出现单点故障。
- 解决方案:采用Redis集群或Sentinel模式提高可用性
-
一致性:锁的获取和释放操作在分布式环境中具有一致性,不能出现"锁已释放但其他节点仍认为锁存在"或"锁未释放但其他节点认为锁已释放"的情况。
- Redis Lua脚本可以保证原子性操作
-
可重入性(可选):同一节点在持有锁的情况下,再次请求获取锁时能够成功,避免自己阻塞自己。
- 实现方式:记录锁持有者信息和重入次数
1.2 为什么选择 Redis 实现分布式锁?
主流实现方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ZooKeeper | 强一致性,可靠性高 | 性能较低,部署复杂 | 对一致性要求极高的场景 |
| Etcd | 强一致性,支持租约 | 功能相对单一 | Kubernetes生态相关系统 |
| 数据库 | 实现简单,无需额外组件 | 性能差,容易成为系统瓶颈 | 低并发量,简单业务场景 |
| Redis | 高性能,功能丰富,部署简单 | 需要额外处理一致性问题 | 高并发,对性能要求高的场景 |
Redis的具体优势
-
高性能:
- Redis基于内存操作,单机QPS可达10万+
- 纯内存操作避免了磁盘I/O瓶颈
- 示例:在10万QPS的秒杀场景下,Redis分布式锁能很好应对
-
部署灵活:
- 单机模式:开发测试环境使用
- 主从复制:提高读取性能
- 哨兵模式:实现自动故障转移
- 集群模式:支持水平扩展和高可用
- 示例:某电商系统采用Redis哨兵模式确保锁服务的高可用
-
丰富的命令支持:
SETNX:原子性设置不存在的键值EXPIRE:设置键的过期时间DEL:删除键释放锁EVAL:执行Lua脚本保证原子性- 示例:
SET lock_key unique_value NX EX 10命令实现原子性加锁和过期设置
-
轻量级:
- 相比ZooKeeper,Redis资源占用更少
- 配置简单,学习成本低
- 社区支持完善,文档丰富
- 示例:中小型团队可在1天内完成Redis分布式锁的集成部署
1.3 分布式锁的典型应用场景
1. 秒杀系统
- 场景描述:在秒杀活动中,多个用户同时抢购有限的商品
- 问题:超卖问题(库存减为负数)
- 解决方案:
// 伪代码示例 public boolean seckill(Long itemId) { String lockKey = "seckill:" + itemId; try { // 尝试获取分布式锁,设置10秒过期 boolean locked = redis.set(lockKey, "1", "NX", "EX", 10); if (!locked) return false; // 检查并扣减库存 int stock = checkStock(itemId); if (stock <= 0) return false; reduceStock(itemId); return true; } finally { redis.del(lockKey); // 释放锁 } } - 注意事项:锁粒度要尽可能细(按商品ID锁定),避免影响系统吞吐量
2. 订单处理
- 场景描述:订单创建后需要进行库存检查、支付验证、物流创建等操作
- 问题:重复处理导致数据不一致
- 解决方案:
# 伪代码示例 def process_order(order_id): lock_key = f"order:{order_id}" with redis.lock(lock_key, timeout=30): if check_order_processed(order_id): return # 执行订单处理逻辑 process_payment(order_id) update_inventory(order_id) create_shipping(order_id) - 最佳实践:设置合理的锁超时时间,避免长时间阻塞
3. 缓存更新
- 问题背景:缓存失效时,多个请求同时穿透到数据库(缓存击穿)
- 解决方案架构:
- 请求1发现缓存失效,获取分布式锁
- 请求1查询数据库并更新缓存
- 其他请求等待或短暂sleep后重试
- 请求1释放锁后,其他请求可直接读取缓存
4. 定时任务协调
- 场景示例:每日凌晨的报表生成任务
- 解决方案:
// 伪代码示例 func runDailyReport() { lockKey := "task:daily_report" ok, err := redis.SetNX(lockKey, "1", 24*time.Hour).Result() if err != nil || !ok { return // 其他节点已获取锁 } defer redis.Del(lockKey) generateReport() // 执行实际报表生成逻辑 } - 优化建议:结合Redis的EXPIRE命令,避免任务失败导致锁无法释放
二、Redis 分布式锁的核心原理与命令
2.1 核心命令解析
(1)SETNX:获取锁的关键命令
SETNX key value 命令是 Redis 实现分布式锁的基础命令,全称"SET if Not eXists"。
深入解析:
- 该命令在 Redis 中是原子性操作,保证了并发安全
- 客户端需要为每个锁设置唯一标识(如业务ID+资源ID)
- 建议value包含客户端标识(如IP+进程ID+线程ID)和随机值,便于后续验证
- 返回1表示成功获取锁,0表示锁已被占用
典型应用场景:
- 电商系统中防止重复下单
- 支付系统中防止重复支付
- 库存系统中防止超卖
(2)EXPIRE:避免死锁的过期命令
EXPIRE key seconds 命令为锁设置自动释放时间。
关键要点:
- 过期时间设置需要权衡(太长会导致故障时资源长时间锁定,太短可能导致业务未完成锁已释放)
- 常规业务场景建议设置5-30秒
- 对于长时间操作,建议实现锁续期机制(Watch Dog)
- 必须与SETNX配合使用,避免死锁
常见问题:
- 如果SETNX和EXPIRE非原子性执行,可能导致死锁
- 过期时间设置不当可能导致业务逻辑错误
(3)SET:合并命令(推荐使用)
Redis 2.6.12+版本的SET命令支持扩展参数。
优势分析:
- 原子性:同时完成SET和EXPIRE操作
- 高性能:减少网络往返次数
- 可靠性:避免分步执行可能导致的死锁问题
参数说明:
- NX:只有当key不存在时才执行
- EX:设置过期时间,单位为秒
- PX:设置过期时间,单位为毫秒
(4)DEL:释放锁的命令
DEL key 命令用于主动释放锁。
注意事项:
- 只能由锁的持有者释放
- 释放前需要验证锁的归属
- 错误的释放操作可能导致并发问题
- 建议使用Lua脚本保证原子性
(5)GET:验证锁的归属
GET key 命令用于检查锁的持有者。
验证流程:
- 获取锁的当前值
- 与本地保存的锁标识比较
- 只有匹配时才执行释放操作
典型问题:
- GET和DEL非原子性执行可能导致锁被错误释放
- 锁可能在被GET后、DEL前过期
(6)Lua 脚本:保证释放锁的原子性
Redis支持执行Lua脚本,可以保证多个命令的原子性执行。
脚本优势:
- 执行期间不会被其他命令打断
- 减少网络开销
- 简化客户端逻辑
脚本优化建议:
- 使用KEYS和ARGV数组传递参数
- 添加错误处理逻辑
- 避免执行耗时操作
2.2 锁的获取与释放流程
(1)获取锁流程的详细实现
-
生成唯一标识
- 使用UUID.randomUUID()生成随机部分
- 拼接客户端标识(IP+进程ID+线程ID)
- 示例:192.168.1.100:8080:thread-1:a1b2c3d4
-
执行SET命令
String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);- 推荐使用Jedis或Lettuce等客户端库
- 设置合理的过期时间(根据业务需求)
-
处理获取结果
- 成功:执行业务逻辑
- 失败:实现重试机制
- 固定间隔重试
- 指数退避重试
- 最大重试次数限制
(2)释放锁流程的最佳实践
-
Lua脚本实现
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end -
脚本执行优化
- 预加载脚本获取SHA1摘要
- 使用EVALSHA执行提高性能
- 添加异常处理
-
释放结果处理
- 成功:记录日志,继续后续流程
- 失败:分析原因,必要时告警
(3)异常处理的完整方案
-
锁自动过期的应对措施
- 实现锁续期机制
- 定时任务(如每隔过期时间的1/3续期一次)
- 使用Redisson的WatchDog机制
- 业务逻辑幂等设计
- 添加事务补偿机制
- 实现锁续期机制
-
节点故障的容错方案
- 合理设置过期时间
- 实现健康检查机制
- 添加监控告警
-
网络分区的处理
- 设置合理的锁超时时间
- 实现fencing token机制
- 添加异步心跳检测
高级技巧:
- 可重入锁实现
- 公平锁实现
- 读写锁实现
- 红锁(RedLock)算法
三、Redis 分布式锁的常见问题与解决方案
虽然基于 Redis 的核心命令可以实现分布式锁,但在实际应用中,仍会遇到一些复杂问题,如锁过期、主从同步延迟、集群脑裂等。本节将深入分析这些常见问题,并提供对应的解决方案,同时结合实际应用场景给出具体示例。
3.1 问题 1:锁过期导致的并发问题
问题描述
假设节点 A 获取锁后,设置的过期时间为 10 秒,但节点 A 处理共享资源的时间超过了 10 秒(如业务逻辑复杂、网络延迟等)。此时锁会自动过期,节点 B 成功获取锁并开始操作共享资源。而节点 A 在 10 秒后完成操作,执行释放锁命令(此时释放的是节点 B 的锁),导致两个节点同时操作共享资源,出现并发问题。
典型场景:在电商秒杀系统中,两个用户同时抢购同一件商品,由于锁过期导致超卖问题。
解决方案
(1)合理设置过期时间
在设置锁的过期时间时,需根据业务逻辑的平均执行时间,预留一定的冗余时间(如平均执行时间为 5 秒,可设置过期时间为 15 秒),确保大部分情况下节点能在锁过期前完成操作并释放锁。
实现示例:
// 获取当前时间戳
long startTime = System.currentTimeMillis();
// 执行业务逻辑
doBusiness();
// 计算执行耗时
long costTime = System.currentTimeMillis() - startTime;
// 设置过期时间为执行时间的3倍
String result = redis.set(lockKey, requestId, "NX", "EX", costTime * 3);
但这种方案无法应对极端情况(如业务逻辑执行时间远超预期),因此需要结合其他方案。
(2)实现锁续期(Watch Dog 机制)
锁续期机制(也称为 Watch Dog)的核心思想是:当节点成功获取锁后,启动一个后台定时任务(如每隔 3 秒执行一次),在锁过期前(如剩余过期时间小于 5 秒时),自动延长锁的过期时间(如重新设置为 10 秒),确保节点在操作共享资源期间,锁不会过期。
实现逻辑:
- 节点成功获取锁后,记录锁的过期时间(如当前时间 + 10 秒)。
- 启动一个定时任务,每隔 3 秒检查一次:
- 如果节点仍在操作共享资源(如业务逻辑未执行完),且锁的剩余过期时间小于 5 秒,则执行
SET key value NX EX 10命令(由于key已存在,NX参数会确保只有当前节点能成功设置,避免覆盖其他节点的锁),延长锁的过期时间。
- 如果节点仍在操作共享资源(如业务逻辑未执行完),且锁的剩余过期时间小于 5 秒,则执行
- 当节点完成共享资源操作后,停止定时任务,并执行释放锁命令。
Java实现示例:
private ScheduledExecutorService watchDogExecutor = Executors.newScheduledThreadPool(1);
public boolean renewLock(String lockKey, String requestId, int expireTime) {
// 设置看门狗定时任务
watchDogExecutor.scheduleAtFixedRate(() -> {
// 检查锁是否仍由当前线程持有
if (redis.get(lockKey).equals(requestId)) {
// 延长锁过期时间
redis.expire(lockKey, expireTime);
}
}, 0, expireTime / 3, TimeUnit.SECONDS);
return true;
}
(3)确保业务逻辑幂等性
即使出现锁过期的情况,通过确保业务逻辑的幂等性,也能避免数据不一致。幂等性是指:多次执行同一操作,最终的结果与执行一次的结果一致。
实现方式:
- 数据库唯一约束
- 乐观锁机制
- 状态机模式
示例:
-- 扣减库存的幂等操作
UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE product_id = 1001 AND stock > 0 AND version = 1;
3.2 问题 2:主从同步延迟导致的锁丢失
问题描述
在 Redis 主从架构中,主节点负责处理写请求(如获取锁的SET命令),从节点通过主从同步机制复制主节点的数据。由于主从同步存在延迟(即使是异步复制,也会有毫秒级的延迟),如果主节点在执行SET命令后、数据同步到从节点前突然故障,从节点会升级为新的主节点。此时新主节点中不存在之前的锁数据,其他节点可以成功获取锁,导致 "同一时间多个节点持有锁" 的问题。
典型场景:在分布式任务调度系统中,多个调度节点同时执行同一任务。
解决方案
(1)使用 Redis Sentinel 或 Cluster 集群
Redis Sentinel(哨兵)和 Cluster(集群)架构可以提高 Redis 的可用性,但无法完全解决主从同步延迟导致的锁丢失问题。不过,通过哨兵的故障转移机制,可以快速将从节点升级为主节点,减少锁丢失的概率。
配置示例:
# sentinel.conf
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
(2)采用 Redlock 算法
Redlock 算法是 Redis 作者 Antirez 提出的一种基于多 Redis 实例的分布式锁方案,其核心思想是:通过在多个独立的 Redis 实例(通常为 5 个)上获取锁,只有当在大多数实例(至少 3 个)上成功获取锁时,才认为整体锁获取成功。
Redlock 算法流程:
- 客户端获取当前时间(毫秒级)。
- 客户端依次向 5 个独立的 Redis 实例发送
SET key value NX EX seconds命令,获取锁。在发送命令时,为每个实例设置超时时间(如 50 毫秒),避免因某个实例故障导致客户端阻塞。 - 客户端计算从开始获取锁到成功获取锁的总时间(当前时间 - 步骤 1 的时间)。如果总时间小于锁的过期时间,且成功获取锁的实例数大于等于 3 个,则认为成功获取锁,锁的有效时间为 "过期时间 - 总时间"。
- 如果获取锁失败(如成功实例数小于 3 个,或总时间超过过期时间),客户端依次向所有 Redis 实例发送
DEL命令,释放已获取的锁。 - 如果获取锁失败,客户端等待一段时间(如随机等待 100-200 毫秒)后,重新尝试获取锁。
Redlock 的优势:
由于锁数据存储在多个独立的 Redis 实例上,即使某个实例出现主从同步延迟或故障,其他实例仍能提供正确的锁状态,从而降低锁丢失的风险。例如,即使 5 个实例中有 2 个出现故障,只要剩余 3 个实例的锁状态一致,客户端仍能正确获取和释放锁。
Redlock 的注意事项:
- 多个 Redis 实例需完全独立,避免部署在同一台物理机或虚拟机上,防止因硬件故障导致所有实例同时不可用。
- 锁的过期时间需大于客户端获取锁的总时间(包括与多个实例通信的时间),否则可能出现 "锁已在部分实例过期,但客户端仍认为锁有效" 的情况。
- Redlock 算法的性能会随着实例数量的增加而下降(需与多个实例通信),因此在高并发场景下需权衡可用性和性能。
3.3 问题 3:集群脑裂导致的锁失效
问题描述
在 Redis Cluster 集群中,当集群的网络出现分区(如主节点所在的分区与其他分区断开连接),集群可能会出现 "脑裂" 现象:即原本的一个集群被分割成多个独立的子集群,每个子集群都认为自己是正常的集群,并可能选举出新的主节点。
典型场景:在金融交易系统中,由于网络分区导致多个节点同时处理同一笔交易。
解决方案
(1)配置 Redis Cluster 的最小主节点数
在 Redis Cluster 中,可以通过配置min-replicas-to-write和min-replicas-max-lag参数,限制主节点的写操作。例如,设置min-replicas-to-write = 2和min-replicas-max-lag = 10,表示主节点必须至少有 2 个从节点的复制延迟小于 10 秒,才能接受写操作(如获取锁的SET命令)。
配置示例:
# redis.conf
min-replicas-to-write 2
min-replicas-max-lag 10
(2)结合 Redlock 算法
将 Redis Cluster 与 Redlock 算法结合,在多个独立的 Cluster 集群上实现分布式锁。即使某个 Cluster 集群发生脑裂,其他 Cluster 集群仍能提供正确的锁状态,从而降低锁失效的风险。
(3)业务层兜底校验
在业务逻辑中增加兜底校验,例如:
- 在操作共享资源前,再次检查锁的状态(如通过 Redis 的
GET命令确认锁是否仍归自己所有) - 通过数据库的唯一约束(如订单 ID 的唯一索引)避免数据不一致
- 使用事务机制确保操作的原子性
实现示例:
public void processOrder(String orderId) {
// 获取分布式锁
boolean locked = redisLock.lock(orderId, 30, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取锁失败");
}
try {
// 再次检查订单状态
Order order = orderDao.getById(orderId);
if (order.getStatus() != OrderStatus.NEW) {
return;
}
// 处理订单
process(order);
} finally {
// 释放锁
redisLock.unlock(orderId);
}
}
四、Redis 分布式锁的代码实现示例
4.1 Java 实现(基于 Redisson)
4.1.1 环境准备
- JDK要求:建议使用JDK 8及以上版本
- Redis版本:建议Redis 3.0及以上版本
- Maven配置:确保Maven环境已正确配置
4.1.2 详细实现步骤
(1)引入依赖
在Maven项目的pom.xml中引入Redisson依赖,建议使用最新稳定版本以确保功能完整性和安全性:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.5</version> <!-- 2023年最新稳定版本 -->
</dependency>
(2)初始化Redisson客户端
支持多种Redis部署模式,以下是完整配置示例:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonConfig {
/**
* 获取Redisson客户端实例
* @return RedissonClient实例
*/
public static RedissonClient getRedissonClient() {
Config config = new Config();
// 单机模式(生产环境建议配置连接池参数)
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("your-redis-password")
.setDatabase(0)
.setConnectionPoolSize(64) // 连接池大小
.setConnectionMinimumIdleSize(10) // 最小空闲连接数
.setIdleConnectionTimeout(10000) // 空闲连接超时时间(ms)
.setConnectTimeout(5000); // 连接超时时间(ms)
// 哨兵模式配置示例
/*
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://sentinel1:26379")
.addSentinelAddress("redis://sentinel2:26379")
.setPassword("your-redis-password");
*/
// 集群模式配置示例
/*
config.useClusterServers()
.addNodeAddress("redis://node1:6379")
.addNodeAddress("redis://node2:6379")
.setPassword("your-redis-password");
*/
return Redisson.create(config);
}
}
(3)分布式锁核心实现
完整实现包含锁获取、释放和业务应用示例:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
public class RedisDistributedLock {
private final RedissonClient redissonClient;
public RedisDistributedLock(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 获取分布式锁(带详细日志)
* @param lockKey 锁的Key(建议使用业务前缀,如"order:lock:")
* @param waitTime 最大等待时间(秒)
* @param leaseTime 锁自动释放时间(秒)
* @return RLock实例
*/
public RLock acquireLock(String lockKey, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
long startTime = System.currentTimeMillis();
boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
if (isLocked) {
long costTime = System.currentTimeMillis() - startTime;
System.out.printf("[成功]获取锁 key=%s 耗时%dms 线程ID=%d%n",
lockKey, costTime, Thread.currentThread().getId());
return lock;
} else {
System.out.printf("[失败]获取锁 key=%s 等待超时 线程ID=%d%n",
lockKey, Thread.currentThread().getId());
return null;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.printf("[异常]获取锁被中断 key=%s 线程ID=%d%n",
lockKey, Thread.currentThread().getId());
return null;
}
}
/**
* 释放分布式锁(增加异常处理)
* @param lock 锁实例
* @param lockKey 锁Key(用于日志)
*/
public void releaseLock(RLock lock, String lockKey) {
try {
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.printf("[成功]释放锁 key=%s 线程ID=%d%n",
lockKey, Thread.currentThread().getId());
} else {
System.out.printf("[警告]非法释放 key=%s 当前线程未持有锁%n", lockKey);
}
} catch (Exception e) {
System.out.printf("[异常]释放锁失败 key=%s 错误:%s%n",
lockKey, e.getMessage());
}
}
/**
* 业务应用示例:库存扣减
* @param productId 商品ID
* @param quantity 扣减数量
*/
public boolean reduceInventory(String productId, int quantity) {
String lockKey = "inventory:lock:" + productId;
RLock lock = null;
try {
// 获取锁:最多等待2秒,持有10秒
lock = acquireLock(lockKey, 2, 10);
if (lock == null) return false;
// 模拟数据库操作
System.out.println("执行库存扣减操作...");
Thread.sleep(500); // 模拟业务处理
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
releaseLock(lock, lockKey);
}
}
// 多线程测试
public static void main(String[] args) {
RedissonClient client = RedissonConfig.getRedissonClient();
RedisDistributedLock lockService = new RedisDistributedLock(client);
// 模拟10个并发请求
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lockService.reduceInventory("product_1001", 1);
}).start();
}
}
}
4.1.3 高级特性
-
锁续期机制:
- 当leaseTime设置为-1时,Redisson会启动WatchDog线程
- 默认每10秒检查一次,如果业务仍在执行,则自动续期30秒
- 可通过
config.setLockWatchdogTimeout(30000L)调整续期时间
-
锁类型选择:
// 公平锁(按请求顺序获取) RLock fairLock = redissonClient.getFairLock("fairLock"); // 联锁(多个锁同时获取) RLock lock1 = redissonClient.getLock("lock1"); RLock lock2 = redissonClient.getLock("lock2"); RLock multiLock = redissonClient.getMultiLock(lock1, lock2); // 读写锁 RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock"); -
锁监控:
// 查看锁状态 boolean isLocked = lock.isLocked(); boolean isHeld = lock.isHeldByCurrentThread();
4.2 Python实现(基于redis-py)
4.2.1 环境准备
- Python版本:建议Python 3.7+
- Redis版本:建议Redis 3.0+
- 依赖安装:
pip install redis pip install uuid
4.2.2 完整实现代码
import redis
import uuid
import time
from threading import Timer
from typing import Optional
class RedisDistributedLock:
def __init__(self,
redis_host: str = "127.0.0.1",
redis_port: int = 6379,
redis_password: Optional[str] = None,
redis_db: int = 0):
"""
初始化Redis连接
:param redis_host: Redis主机地址
:param redis_port: Redis端口
:param redis_password: Redis密码
:param redis_db: Redis数据库编号
"""
self.redis = redis.Redis(
host=redis_host,
port=redis_port,
password=redis_password,
db=redis_db,
socket_timeout=5, # 网络超时
socket_connect_timeout=5, # 连接超时
decode_responses=True
)
self.lock_key = None
self.lock_value = None
self.lease_time = 30 # 默认锁持有时间
self.renew_interval = 10 # 续期间隔
self.timer = None
def _acquire_lock(self, key: str, expire: int) -> bool:
"""
尝试获取锁(原子操作)
:param key: 锁键名
:param expire: 过期时间(秒)
:return: 是否成功
"""
# 使用setnx+expire组合命令(Redis 2.6.12+支持set带px参数)
result = self.redis.set(
key,
value=self.lock_value,
nx=True,
ex=expire
)
return result is True
def acquire(self,
key: str,
wait_time: float = 5.0,
lease_time: int = 30) -> bool:
"""
获取分布式锁
:param key: 锁键名
:param wait_time: 最大等待时间(秒)
:param lease_time: 锁持有时间(秒)
:return: 是否成功
"""
self.lock_key = key
self.lock_value = str(uuid.uuid4())
self.lease_time = lease_time
start_time = time.time()
while True:
if self._acquire_lock(key, lease_time):
# 启动续期定时器
if lease_time > 0:
self._start_renew_timer()
return True
# 检查是否超时
if time.time() - start_time >= wait_time:
break
# 随机等待避免活锁
time.sleep(0.1 + random.random() * 0.1)
return False
def _start_renew_timer(self):
"""启动锁续期定时器"""
if self.timer:
self.timer.cancel()
self.timer = Timer(
interval=self.renew_interval,
function=self._renew_lock
)
self.timer.daemon = True
self.timer.start()
def _renew_lock(self):
"""执行锁续期"""
if not self.lock_key or not self.lock_value:
return
try:
# 使用Lua脚本保证原子性
script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('expire', KEYS[1], ARGV[2])
else
return 0
end
"""
result = self.redis.eval(
script,
numkeys=1,
keys=[self.lock_key],
args=[self.lock_value, self.lease_time]
)
if result:
self._start_renew_timer()
except Exception as e:
print(f"锁续期失败: {str(e)}")
def release(self):
"""释放分布式锁"""
if not self.lock_key:
return
try:
# 使用Lua脚本保证原子性
script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
self.redis.eval(
script,
numkeys=1,
keys=[self.lock_key],
args=[self.lock_value]
)
finally:
if self.timer:
self.timer.cancel()
self.lock_key = None
self.lock_value = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.release()
# 使用示例
if __name__ == "__main__":
# 创建锁实例
lock = RedisDistributedLock(redis_password="your_password")
try:
# 尝试获取锁(等待5秒,持有30秒)
if lock.acquire("order_lock", wait_time=5, lease_time=30):
print("成功获取锁,执行关键业务逻辑...")
time.sleep(10) # 模拟业务处理
else:
print("获取锁失败")
finally:
lock.release()
4.2.3 关键点说明
-
原子性保证:
- 使用Redis的
SET key value NX EX命令实现原子操作 - 释放锁时使用Lua脚本确保验证值和删除操作的原子性
- 使用Redis的
-
锁续期机制:
- 通过后台线程定时执行续期
- 默认每10秒续期一次,保持30秒有效期
-
异常处理:
- 网络超时设置
- 连接重试机制
- 资源释放确保在finally块中执行
-
上下文管理器:
- 实现
__enter__和__exit__方法支持with语法
with RedisDistributedLock() as lock: if lock.acquire("resource_lock"): # 执行业务逻辑 pass - 实现
-
集群支持:
# Redis集群连接示例 from redis.cluster import RedisCluster startup_nodes = [ {"host": "127.0.0.1", "port": "7000"}, {"host": "127.0.0.1", "port": "7001"} ] rc = RedisCluster(startup_nodes=startup_nodes)
4.3 生产环境建议
-
性能优化:
- 合理设置锁超时时间(不宜过长或过短)
- 避免锁粒度过细(减少锁竞争)
- 使用连接池管理Redis连接
-
监控指标:
- 锁获取成功率
- 平均等待时间
- 锁持有时间分布
-
容灾方案:
- 实现降级策略(如本地锁)
- 设置熔断机制
- 监控Redis节点健康状态
-
最佳实践:
- 锁的key设计要有业务含义
- 必须设置超时时间防止死锁
- 确保释放锁的代码在finally块中执行
- 避免在锁内执行耗时操作
五、Redis 分布式锁的选型与最佳实践
5.1 锁的选型建议
| 场景 | 推荐锁类型 | 原因 |
|---|---|---|
| 高并发、高性能需求 | Redisson 可重入锁 | 内置 Watch Dog 自动续期机制,性能优化较好,支持多种锁类型(可重入锁、公平锁等),开发效率高。适用于秒杀、库存扣减等高并发场景。 |
| 公平性需求 | Redisson 公平锁 | 按照请求顺序获取锁,避免"饥饿"问题(即某个请求长期无法获取锁),适合对公平性要求较高的场景(如任务调度、排队系统)。 |
| 读写分离场景 | Redisson 读写锁(RReadWriteLock) | 支持"多读一写",读操作之间不互斥,写操作与读写操作互斥,提高读密集型场景的并发效率。适用于配置中心、缓存热点数据等场景。 |
| 分布式计数器/信号量 | Redisson 信号量(RSemaphore) | 可用于控制并发访问的资源数量(如限制同时访问数据库的连接数、限制API调用频率),功能比普通分布式锁更灵活。 |
| 高可用、强一致性需求 | Redlock 算法(基于多 Redis 实例) | 通过多个独立Redis实例确保锁的可靠性,避免单点故障或主从同步延迟导致的锁失效,适合金融、交易等核心业务。建议至少5个独立Redis主节点部署。 |
5.2 最佳实践总结
(1)锁 Key 的设计规范
-
唯一性:锁Key需能唯一标识共享资源,避免不同资源共用同一把锁导致的并发问题。例如:
- 操作订单ID为"123"的共享资源时,锁Key应设计为
lock:order:123 - 操作商品ID为"456"的库存时,锁Key应为
lock:stock:456 - 避免使用通用的
lock:order或lock:stock
- 操作订单ID为"123"的共享资源时,锁Key应设计为
-
可读性:锁Key的命名需清晰易懂,便于后续排查问题。建议采用"lock:业务模块:资源标识"的格式:
lock:payment:txn_789(支付事务789的锁)lock:inventory:sku_101(SKU101的库存锁)- 可通过Redis命令
KEYS lock:*查看所有锁的状态
-
避免过长:Redis的Key长度不宜过长(建议不超过1024字节),过长的Key会增加:
- 内存占用(每个Key都会占用额外内存)
- 网络传输开销(特别是在频繁获取/释放锁时)
- 影响Redis性能(特别是在集群模式下)
(2)锁的过期时间与续期策略
-
合理设置过期时间:
- 计算公式:
过期时间 = 平均执行时间 × (1 + 冗余系数) - 例如:业务逻辑平均执行时间为10秒,冗余系数设为50%,则过期时间设置为15秒
- 对于不确定执行时间的任务,建议初始设置为30秒,并通过续期机制动态调整
- 计算公式:
-
必选续期机制:
- 对于执行时间不稳定的业务(如复杂数据分析、第三方接口调用),必须实现锁续期机制
- Redisson的Watch Dog默认续期间隔为过期时间的1/3(如过期时间30秒,则每10秒续期一次)
- 自定义续期实现时,建议使用单独的守护线程,并处理续期失败的情况
-
避免"永久锁":
- 严禁不设置过期时间的锁,即使业务逻辑执行时间不确定
- 极端情况下,可通过设置较长的过期时间(如24小时)+ 续期机制来避免锁过早释放
- 关键业务场景建议结合数据库事务状态来检测和处理异常锁
(3)锁的获取与释放规范
-
非阻塞获取锁:
- 优先使用
tryLock(long waitTime, long leaseTime, TimeUnit unit)方法 - 示例:
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS)(最多等待5秒,锁持有30秒) - 避免使用无参的
lock()方法,该方式会无限期阻塞线程
- 优先使用
-
原子释放锁:
- 必须使用Lua脚本实现"验证归属+删除锁"的原子操作
- 错误示例:直接调用
redis.del(lockKey)可能导致误删其他客户端持有的锁 - 正确实现:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
-
finally中释放锁:
- 标准代码结构:
try { if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } catch (Exception e) { // 异常处理 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } - 注意检查当前线程是否仍持有锁,避免重复释放
- 标准代码结构:
(4)高可用部署与监控
-
Redis集群部署:
- 生产环境建议至少3主3从的Redis Cluster部署
- 或1主2从+3哨兵的标准部署模式
- 跨机房部署时,需评估网络延迟对锁获取的影响
-
监控锁状态:
- 关键监控指标:
- 锁获取成功率/失败率
- 平均锁持有时间
- 锁等待时间分布
- 锁续期失败次数
- 告警阈值建议:
- 锁获取失败率 > 1%
- 锁持有时间 > 预设过期时间的80%
- 续期失败次数连续3次
- 关键监控指标:
-
定期清理无效锁:
- 清理脚本示例:
# 查找过期但未自动删除的锁 redis-cli --scan --pattern 'lock:*' | while read key; do ttl=$(redis-cli ttl "$key") if [ $ttl -eq -1 ]; then echo "Found permanent lock: $key" # 验证后删除 redis-cli del "$key" fi done - 建议在业务低峰期执行(如凌晨2-4点)
- 清理脚本示例:
(5)业务层兜底方案
-
幂等性设计:
- 数据库层面:使用乐观锁或状态机
UPDATE orders SET status = 'paid' WHERE order_id = '123' AND status = 'unpaid' - 业务层面:使用唯一事务ID或请求ID
- 数据库层面:使用乐观锁或状态机
-
降级与熔断:
- 降级方案:
- 本地缓存模式(如Guava Cache)
- 基于ZooKeeper的备用锁服务
- 直接放行+事后对账(适用于非核心业务)
- 熔断配置:
- 熔断器:Hystrix或Resilience4j
- 触发条件:连续10次获取锁失败
- 熔断时间:30秒
- 降级方案:
-
日志记录:
- 标准日志格式:
[时间] [级别] [traceId] 操作 锁Key=key 持有者=value 结果=success/fail 耗时=ms 示例: [2024-05-20 14:30:00] [INFO] [txn-abc123] 获取锁 lock:order:789 持有者=client_1 结果=success 耗时=45ms - 关键字段:操作时间、traceId、锁Key、持有者标识、操作结果、耗时
- 标准日志格式:
六、附录:常用 Redis 命令与工具
(1)Redis 锁相关命令详解
| 命令 | 作用 | 详细说明 | 示例 | 注意事项 |
|---|---|---|---|---|
SET key value NX EX seconds | 原子获取锁并设置过期时间 | 使用NX参数确保只有键不存在时才设置,EX参数设置过期时间(秒),实现分布式锁的基本操作 | SET lock:order:123 uuid123 NX EX 30 | 1. value建议使用UUID等唯一标识<br>2. 过期时间需根据业务合理设置 |
GET key | 获取锁的Value | 用于验证锁的归属权,防止误删其他客户端的锁 | GET lock:order:123 | 应与SET命令中的value配合使用 |
EVAL script keys args | 执行Lua脚本 | 实现原子性操作,确保验证和删除操作不会被其他命令打断 | EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) end" 1 lock:order:123 uuid123 | 1. 推荐使用SHA1缓存脚本<br>2. 注意参数传递的正确性 |
EXPIRE key seconds | 延长锁过期时间 | 用于锁续约,防止业务未完成但锁已过期 | EXPIRE lock:order:123 30 | 1. 应配合看门狗机制使用<br>2. 需确保客户端仍持有锁 |
DEL key | 删除锁 | 强制删除键(仅用于测试) | DEL lock:order:123 | 生产环境应使用Lua脚本验证后删除 |
KEYS lock:* | 查看所有锁的Key | 用于监控和调试 | KEYS lock:* | 1. 生产环境慎用<br>2. 大数据量可能阻塞服务器 |
TTL key | 查看剩余过期时间 | 监控锁的生命周期 | TTL lock:order:123 | 返回-2表示键已过期不存在,-1表示永不过期 |
(2)推荐工具完整指南
-
Redis客户端工具:
- Java: Redisson
- 提供分布式锁、限流器等高级功能
- 支持自动续约、可重入锁等特性
- 示例:
RLock lock = redisson.getLock("myLock");
- Python: redis-py
- 支持连接池、管道等特性
- 示例:
r = redis.Redis(host='localhost', port=6379)
- Go: go-redis
- 支持集群模式和哨兵模式
- 示例:
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
- .NET: StackExchange.Redis
- 支持多路复用和异步操作
- 示例:
ConnectionMultiplexer.Connect("localhost")
- Java: Redisson
-
监控工具组合:
- Prometheus + Grafana
- 配置redis_exporter采集指标
- 关键监控项:内存使用、QPS、慢查询、连接数
- 可设置锁争用告警规则
- Redis Insight
- 可视化查看键空间
- 实时监控命令执行
- 支持慢查询分析
- ELK Stack
- Filebeat收集Redis日志
- Kibana可视化分析日志模式
- Prometheus + Grafana
-
部署工具链:
- Docker
- 官方镜像:
redis:alpine - 持久化配置:
docker run -v /data/redis:/data redis - 集群部署示例:
docker-compose编排6节点集群
- 官方镜像:
- Kubernetes
- StatefulSet部署Redis集群
- 使用ConfigMap管理配置
- 通过Service暴露访问
- Ansible
- 自动化安装Redis
- 配置模板管理redis.conf
- 支持集群初始化脚本
- Docker
-
其他实用工具:
- redis-cli
- 内置命令行工具
- 支持批量操作:
--pipe模式 - 性能测试:
redis-benchmark
- Redis Desktop Manager
- 图形化键值查看器
- 支持数据导入导出
- Twemproxy
- Redis代理中间件
- 实现分片和负载均衡
- redis-cli
Redis分布式锁解析
1386

被折叠的 条评论
为什么被折叠?



