Redis实现分布式锁的原始方式详解:从入门到实践
一、为什么需要分布式锁?
在微服务架构中,当多个服务实例需要同时访问共享资源(如库存扣减、订单创建)时,传统的单机锁机制无法满足需求。分布式锁通过协调不同节点对资源的访问顺序,确保在高并发场景下的数据一致性。想象一下双十一抢购场景:如果没有锁机制,可能会导致超卖现象,而分布式锁就是解决这类问题的关键。
二、Redis实现分布式锁核心原理
2.1 最简实现方案
// 尝试获取锁
String uuid = UUID.randomUUID().toString();
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent("product_lock", uuid, 30, TimeUnit.SECONDS);if(lockAcquired) {try {// 执行业务逻辑} finally {// 释放锁redisTemplate.delete("product_lock");}
}
这就是一个典型错误实现!继续往下看为什么
三、满足分布式锁的四大条件
3.1 互斥性(Mutex)
要求:同一时刻只能有一个客户端持有锁
实现方案:
使用Redis的SETNX
命令(SET if Not eXists)。当key不存在时设置值,存在时不做操作:
# Redis命令原型
SET lock_key unique_value NX PX 30000
NX表示仅当key不存在时设置,PX设置过期时间(单位毫秒)
3.2 避免死锁(Deadlock Free)
要求:即使客户端崩溃,锁也能自动释放
实现方案:
为锁设置合理的过期时间。注意:业务代码执行时间必须小于锁过期时间!
3.3 解铃还须系铃人
要求:只能由加锁者解锁
实现方案:
使用唯一标识(如UUID)作为value,释放时验证身份:
// 错误示例:直接删除可能误删其他客户端的锁
redisTemplate.delete("lock"); // 正确做法:Lua脚本保证原子性验证和删除
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +" return redis.call('del', KEYS[1]) " +"else " +" return 0 " +"end";
3.4 原子性操作
要求:加锁、设置过期时间必须原子完成
实现方案:
使用Redis的原子命令组合:
// Spring Data Redis实现
redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, // 过期时间TimeUnit.SECONDS
);
四、完整实现代码剖析
4.1 加锁实现
public boolean tryLock(String lockKey, String clientId, long expireSeconds) {return redisTemplate.execute((RedisCallback<Boolean>) connection -> {// 原子化执行SETNX+EXPIRERedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();byte[] keyBytes = redisTemplate.getKeySerializer().serialize(lockKey);byte[] valueBytes = redisTemplate.getValueSerializer().serialize(clientId);Expiration expiration = Expiration.seconds(expireSeconds);return connection.set(keyBytes, valueBytes, expiration, setOption);});
}
4.2 释放锁实现
public boolean releaseLock(String lockKey, String clientId) {String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +" return redis.call('del', KEYS[1]) " +"else " +" return 0 " +"end";DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(luaScript);redisScript.setResultType(Long.class);Long result = redisTemplate.execute(redisScript,Collections.singletonList(lockKey),clientId);return result != null && result == 1;
}
4.3 重试机制
public void doWithLock(String lockKey, Runnable task) {String clientId = UUID.randomUUID().toString();int retryCount = 0;while(retryCount < MAX_RETRY) {if(tryLock(lockKey, clientId, 30)) {try {task.run();return;} finally {releaseLock(lockKey, clientId);}}try {// 指数退避算法避免活锁Thread.sleep((long)Math.pow(2, retryCount) * 100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}retryCount++;}throw new LockAcquisitionException("Failed to acquire lock after retries");
}
五、生产环境注意事项
问题类型 | 产生原因 | 解决方案 |
---|---|---|
锁过期提前释放 | 业务执行时间超过锁超时时间 | 设置合理的超时时间,使用续约机制 |
锁误删 | 未验证客户端身份 | 必须使用UUID校验身份 |
集群脑裂 | 主从切换导致锁状态不一致 | 使用RedLock算法 |
客户端阻塞 | 长时间GC导致锁失效 | 添加JVM监控,优化GC参数 |
六、锁的优化方向
- 可重入锁:记录重入次数
- 公平锁:使用Redis队列实现排队机制
- 自动续约:后台线程定期延长锁有效期
- 高可用:采用Redis Cluster或RedLock方案
七、总结与思考
通过Redis实现分布式锁需要严格遵循四个基本原则。虽然本文展示了基础实现方案,但在实际生产环境中,建议使用经过验证的框架(如Redisson),它们已经处理了续约、重试、集群容错等复杂问题。记住:分布式系统的可靠性永远不能完全依赖单一中间件,必须结合业务场景设计兜底方案。