在微服务、高并发环境中,我们常常需要对共享资源进行加锁控制。分布式锁正是为了解决多个节点竞争同一资源的问题而生。本文将详细介绍 Redis 实现分布式锁的原理、注意事项及 Java 实战代码。
一、为什么需要分布式锁?
在单体应用中,我们可以使用 synchronized
或 ReentrantLock
解决并发问题,但在分布式系统中,不同服务实例部署在多台服务器上,JVM 层面的锁无法跨节点同步,这时候就需要一个跨进程、跨机器的锁机制,即:分布式锁。
二、Redis 为什么适合作为分布式锁?
Redis 本身是单线程 + 内存型数据库,具备以下特点:
操作快(RT < 1ms)
命令原子性强
支持过期时间
可部署高可用结构(哨兵、集群)
所以,用 Redis 来实现分布式锁成为主流做法之一。
三、加锁的核心命令
使用 SET key value NX PX
命令:
SET lock:order:123 abc123 NX PX 30000
NX
:仅当 key 不存在时设置(确保加锁原子性)PX
:设置过期时间(防止死锁)abc123
:一般是 UUID,标识当前线程持有锁
返回值:
"OK"
表示加锁成功nil
表示锁已被他人持有
四、释放锁为何必须用 Lua 脚本?
❌ 错误做法(非原子):
if (jedis.get(key).equals(value)) {
jedis.del(key);
}
在高并发环境中,可能在 get 和 del 之间锁就过期了,导致误删了其他线程/服务设置的锁!
✅ 正确做法(Lua 脚本保证原子):
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
使用 Redis 的 EVAL
命令将 Lua 脚本执行为一个原子操作。
五、Java 实战代码
加锁实现:
String lockKey = "lock:order:123";
String uuid = UUID.randomUUID().toString();
String result = jedis.set(lockKey, uuid, SetParams.setParams().nx().px(30000));
if ("OK".equals(result)) {
// 加锁成功
}
解锁实现(Lua 脚本):
String lua =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(uuid));
六、存在的问题与优化建议
问题 | 建议解决方案 |
---|---|
锁过期导致误删他人锁 | 使用 UUID + Lua 脚本安全释放 |
锁未过期业务超时(死锁) | 使用合理的 PX 设置过期时间 |
锁意外失效(宕机、GC) | 使用 Redisson 支持自动续期 |
不可重入 | 自实现线程绑定锁或使用 Redisson |
主从延迟导致锁丢失(Redis 单点) | 推荐 Redis Sentinel 或 Cluster 部署 |
七、推荐:使用 Redisson 简化实现
Redisson 是基于 Redis 的 Java 客户端,支持:
可重入锁
自动续期
公平锁、读写锁
分布式 CountDownLatch
只需一行代码即可加锁:
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
八、总结
Redis 分布式锁是一种高效、实用的并发控制工具,在保证性能的前提下,为微服务系统提供了可靠的锁机制。生产环境中推荐使用 Lua 脚本 + UUID + 过期时间组合,同时考虑使用 Redisson 简化开发与容错处理。