1. 分布式锁的核心诉求与Redis的天然优势
在分布式系统中,多个服务实例对共享资源进行并发访问时,传统的单机锁机制会立即失效。想象一下电商系统中的库存扣减场景:当100台手机库存同时收到1000个下单请求时,如果没有跨进程的协调机制,必然会出现超卖问题。这就是分布式锁要解决的核心问题——在分布式环境下实现排他性资源访问。
Redis之所以成为分布式锁的首选方案,源于其三个不可替代的特性:
- 单线程执行模型:所有命令串行处理,天然避免并发问题
- 高性能:10万+ QPS的吞吐量满足高并发场景
- 丰富的数据结构:String类型的SETNX命令正是分布式锁的基石
关键认知:分布式锁的本质是在Redis中抢占一个唯一的key。谁成功创建了这个key,谁就获得了锁。这与单机系统中通过内存地址竞争互斥量有异曲同工之妙。
2. SETNX命令的深度解析与版本演进
2.1 原始版本的分布式锁实现
在Redis 2.6.12之前,实现分布式锁需要组合使用两个命令:
SETNX lock_key unique_value # 尝试获取锁 EXPIRE lock_key 30 # 设置过期时间这种实现存在致命缺陷——如果SETNX成功后Redis宕机,EXPIRE未执行,将导致死锁。我在2018年就曾因此导致线上订单服务瘫痪2小时,教训深刻。
2.2 Redis 2.6.12的原子性突破
新版本引入了扩展语法:
SET lock_key unique_value NX EX 30这个命令将SETNX和EXPIRE合并为原子操作,彻底解决了死锁风险。但很多老系统仍在使用Lua脚本方案,主要因为:
- 历史代码惯性
- 企业自研Redis集群可能基于旧版本
- 某些云服务商自定义命令不支持新语法
3. Lua脚本实现原子操作的工程实践
3.1 加锁脚本的防踩坑实现
以下是经过生产验证的Lua脚本(适配RESP3协议):
-- KEYS[1]: 锁key -- ARGV[1]: 唯一标识(建议使用UUID) -- ARGV[2]: 过期时间(秒) if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) return 1 else -- 这里返回0而不是false,兼容所有Redis版本 return 0 end关键细节:
- 唯一标识必须使用UUID等随机值,防止误删其他客户端的锁
- 过期时间建议设置为业务处理时间的3-5倍
- 返回值用数字而非布尔值,避免RESP2/RESP3协议差异
3.2 解锁脚本的安全实现
-- KEYS[1]: 锁key -- ARGV[1]: 预期唯一标识 if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end血泪教训:
- 一定要先GET再DEL,否则可能删除其他客户端的锁
- 返回不同的错误码(如0表示锁不存在,2表示标识不匹配)便于问题排查
- 在Java中调用时要用Long类型接收返回值
4. Java客户端的完整实现方案
4.1 Spring Data Redis实现
public class RedisDistributedLock { private final StringRedisTemplate redisTemplate; private final String lockKey; private final String lockValue; private final long expireTime; public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException { long start = System.currentTimeMillis(); do { Boolean success = redisTemplate.execute( new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) { return connection.set( lockKey.getBytes(), lockValue.getBytes(), Expiration.from(expireTime, TimeUnit.SECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT ); } } ); if (Boolean.TRUE.equals(success)) { return true; } Thread.sleep(100); // 适度休眠避免CPU空转 } while (System.currentTimeMillis() - start < unit.toMillis(waitTime)); return false; } public void unlock() { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; redisTemplate.execute( new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(lockKey), lockValue ); } }4.2 Redisson框架的最佳实践
对于企业级应用,建议直接使用Redisson:
RLock lock = redisson.getLock("orderLock"); try { // 尝试加锁,最多等待100秒,锁有效期30秒 if (lock.tryLock(100, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { lock.unlock(); }Redisson的优势在于:
- 内置看门狗机制自动续期
- 提供公平锁、联锁等高级特性
- 完善的监控指标
5. 生产环境中的典型问题与解决方案
5.1 锁过期但业务未完成
现象:线程A未执行完锁已过期,线程B获取锁后,线程A完成操作误删线程B的锁
解决方案:
- 合理评估超时时间(建议设置自动续期)
- 实现锁版本号机制
- 使用Redisson等成熟框架
5.2 Redis集群脑裂问题
现象:主从切换期间出现多个主节点,导致锁状态不一致
解决方案:
- 使用RedLock算法(需至少5个主节点)
- 业务层添加补偿机制
- 监控集群健康状态
5.3 锁重入需求
场景:同一线程需要多次获取同一个锁
实现方案:
public class ReentrantRedisLock { private ThreadLocal<Map<String, Integer>> lockCount = ThreadLocal.withInitial(HashMap::new); public boolean tryLock(String key) { Map<String, Integer> counts = lockCount.get(); Integer count = counts.get(key); if (count != null) { counts.put(key, count + 1); return true; } // 实际获取锁逻辑... } }6. 性能优化与监控指标
6.1 关键监控指标
| 指标名称 | 警戒值 | 监控手段 |
|---|---|---|
| 锁等待时间 | >500ms | Prometheus + Grafana |
| 锁持有时间 | >10s | 分布式链路追踪 |
| 锁竞争失败率 | >20% | 日志统计 |
| Redis节点负载 | >70% | Redis监控工具 |
6.2 优化建议
- 锁粒度控制:将大锁拆分为细粒度锁(如订单锁拆为订单ID锁)
- 非阻塞获取:使用tryLock而非lock,避免线程堆积
- 本地缓存:对热点数据采用二级缓存策略
我在某电商平台优化秒杀系统时,通过将全局商品锁改为按SKU分片锁,使QPS从800提升到12000,同时将锁超时时间从5s优化到300ms,系统稳定性显著提升。
7. 多方案对比与选型建议
7.1 主流方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis单节点 | 实现简单、性能高 | 存在单点故障 | 非关键业务、测试环境 |
| Redis集群 | 高可用 | 脑裂问题 | 一般生产系统 |
| RedLock | 强一致性 | 性能损耗大 | 金融交易等关键业务 |
| Zookeeper | 强一致性 | 性能差(写QPS<1000) | 配置管理 |
| 数据库乐观锁 | 无需额外组件 | 高并发下性能急剧下降 | 低频操作 |
7.2 选型决策树
是否需要强一致性? ├─ 是 → 考虑Zookeeper或RedLock └─ 否 → Redis集群是否可用? ├─ 是 → 使用Redis集群+看门狗 └─ 否 → 评估是否可接受短暂不一致在具体实施时,建议先在预发布环境进行长达24小时的稳定性压测,重点关注网络分区场景下的表现。我曾见过某系统在测试环境运行良好,但上线后因跨机房网络抖动导致大量锁异常,最终不得不回滚方案。