一、分布式锁的核心需求
在分布式系统中,当多个进程或服务需要访问共享资源时,分布式锁是保证数据一致性的关键。一个可靠的分布式锁需要满足以下特性:
-
互斥性:同一时刻只有一个客户端持有锁
-
不会死锁:即使持有锁的客户端崩溃,锁也能被释放
-
容错性:只要大部分Redis节点存活,锁就能正常工作
-
一致性:锁的状态在分布式环境中保持一致
二、Redis 分布式锁实现原理
1. 基础实现:SET NX EX 命令
Redis 的 SET 命令提供了原子性操作,这是实现分布式锁的基础:
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisLock {
private final Jedis jedis;
private final String lockKey;
private final int ttl;
private final String lockValue;
public RedisLock(Jedis jedis, String lockKey, int ttl) {
this.jedis = jedis;
this.lockKey = "lock:" + lockKey;
this.ttl = ttl;
this.lockValue = UUID.randomUUID().toString();
}
public boolean acquire() {
// SET key value NX EX ttl
// NX:只有key不存在时才设置
// EX:设置过期时间(秒)
String result = jedis.set(lockKey, lockValue, "NX", "EX", ttl);
return "OK".equals(result);
}
public boolean release() {
// 使用 Lua 脚本保证原子性释放
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = (Long) jedis.eval(script, 1, lockKey, lockValue);
return result == 1;
}
}
关键要点:
-
使用
NX参数确保只有第一个请求能获取锁 -
使用
EX参数设置自动过期时间,防止死锁 -
使用 UUID 作为锁的值,确保只有持有者能释放锁
-
释放锁必须使用 Lua 脚本,避免误删其他客户端的锁
2. 为什么不用 EXIST + SET
很多人可能会这样实现:
// 错误示例!存在竞态条件
if (!jedis.exists(lockKey)) {
jedis.set(lockKey, value);
return true;
}
return false;
问题 :exists 和 set 之间存在时间窗口,多个客户端可能同时通过检查,导致锁被多个客户端持有。
三、生产环境进阶方案
1. Redlock 算法
当 Redis 部署为集群时,单节点锁可能存在单点故障问题。Redlock 算法通过多个节点来提高可靠性:
import redis.clients.jedis.Jedis;
import java.util.List;
import java.util.UUID;
public class Redlock {
private final List<Jedis> nodes;
private final String lockKey;
private final int ttl;
private final int quorum;
public Redlock(List<Jedis> nodes, String lockKey, int ttl) {
this.nodes = nodes;
this.lockKey = "lock:" + lockKey;
this.ttl = ttl;
this.quorum = (nodes.size() / 2) + 1;
}
public LockResult acquire() {
String lockValue = UUID.randomUUID().toString();
int acquiredCount = 0;
long startTime = System.currentTimeMillis();
for (Jedis node : nodes) {
try {
String result = node.set(lockKey, lockValue, "NX", "EX", ttl);
if ("OK".equals(result)) {
acquiredCount++;
}
} catch (Exception e) {
// 节点不可用,跳过
}
}
// 超过半数节点成功获取锁,且在有效时间内
long elapsed = System.currentTimeMillis() - startTime;
if (acquiredCount >= quorum && elapsed < ttl * 1000) {
return new LockResult(lockValue, acquiredCount);
} else {
// 释放已获取的锁
release(lockValue);
return new LockResult(null, 0);
}
}
public void release(String lockValue) {
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
for (Jedis node : nodes) {
try {
node.eval(script, 1, lockKey, lockValue);
} catch (Exception e) {
// 节点不可用,跳过
}
}
}
public static class LockResult {
public final String lockValue;
public final int acquiredCount;
public LockResult(String lockValue, int acquiredCount) {
this.lockValue = lockValue;
this.acquiredCount = acquiredCount;
}
}
}
适用场景:
-
需要极高可靠性的关键业务
-
Redis 集群部署环境
-
对锁的安全性要求高于性能的场景
2. 带续约机制的分布式锁
当业务执行时间可能超过锁的过期时间时,需要定期续约:
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class RenewingLock {
private final Jedis jedis;
private final String lockKey;
private final int ttl;
private final int renewInterval;
private final String lockValue;
private final ScheduledExecutorService scheduler;
private final AtomicBoolean stopped = new AtomicBoolean(false);
public RenewingLock(Jedis jedis, String lockKey, int ttl, int renewInterval) {
this.jedis = jedis;
this.lockKey = "lock:" + lockKey;
this.ttl = ttl;
this.renewInterval = renewInterval;
this.lockValue = UUID.randomUUID().toString();
this.scheduler = Executors.newSingleThreadScheduledExecutor();
}
private void renew() {
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
scheduler.scheduleAtFixedRate(() -> {
if (!stopped.get()) {
try {
jedis.eval(script, 1, lockKey, lockValue, String.valueOf(ttl));
} catch (Exception e) {
// 续约失败,可能锁已过期或节点不可用
}
}
}, 0, renewInterval, TimeUnit.SECONDS);
}
public boolean acquire() {
String result = jedis.set(lockKey, lockValue, "NX", "EX", ttl);
if ("OK".equals(result)) {
// 启动续约任务
stopped.set(false);
renew();
return true;
}
return false;
}
public boolean release() {
stopped.set(true);
scheduler.shutdown();
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = (Long) jedis.eval(script, 1, lockKey, lockValue);
return result == 1;
}
}
适用场景:
-
业务执行时间不确定的场景
-
需要长时间持有锁的操作
-
防止锁被意外释放
四、实战应用场景
1. 库存扣减场景
public Map<String, Object> deductStock(Jedis jedis, String productId, int quantity) {
RedisLock lock = new RedisLock(jedis, "stock:" + productId, 10);
if (!lock.acquire()) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "系统繁忙,请稍后重试");
return result;
}
try {
// 获取当前库存
String stockStr = jedis.get("stock:" + productId);
if (stockStr == null || Integer.parseInt(stockStr) < quantity) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "库存不足");
return result;
}
// 扣减库存
jedis.decrBy("stock:" + productId, quantity);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "扣减成功");
return result;
} finally {
lock.release();
}
}
2. 订单重复提交防护
public Map<String, Object> createOrder(Jedis jedis, String userId, OrderData orderData) {
// 用用户ID和业务标识作为锁key
String lockKey = "order:" + userId + ":" + orderData.getOrderNo();
RedisLock lock = new RedisLock(jedis, lockKey, 60);
if (!lock.acquire()) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "订单已提交");
return result;
}
try {
// 检查是否已存在订单
if (checkOrderExists(orderData.getOrderNo())) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "订单已存在");
return result;
}
// 创建订单
Order order = saveOrder(orderData);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("data", order);
return result;
} finally {
lock.release();
}
}
3. 异步任务幂等性保障
public Map<String, Object> processTask(Jedis jedis, String taskId) {
RedisLock lock = new RedisLock(jedis, "task:" + taskId, 300);
if (!lock.acquire()) {
// 任务正在处理中或已完成
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "任务处理中");
return result;
}
try {
// 检查任务状态
String status = jedis.get("task:" + taskId + ":status");
if ("completed".equals(status)) {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "任务已完成");
return result;
}
// 执行任务
executeTask(taskId);
// 标记任务完成
jedis.set("task:" + taskId + ":status", "completed");
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "任务处理完成");
return result;
} finally {
lock.release();
}
}
五、常见问题与解决方案
1. 锁过期导致数据不一致
问题:业务执行时间超过锁的 TTL,锁被自动释放,其他客户端获取锁后修改数据。
解决方案:
-
预估业务执行时间,设置合理的 TTL
-
使用续约机制延长锁的有效期
-
在业务操作前再次验证锁的持有者
2. 锁释放时的误删问题
问题:客户端 A 的锁过期后,客户端 B 获取了锁,此时 A 执行完业务,误删了 B 的锁。
解决方案:
-
使用 UUID 作为锁的值
-
释放锁时使用 Lua 脚本验证锁的持有者
-
避免直接使用 DEL 命令
3. 集群环境下的一致性问题
问题:Redis 主从复制存在延迟,主节点宕机后,从节点可能未同步锁信息。
解决方案:
-
使用 Redlock 算法
-
对数据一致性要求高的场景,使用强一致性存储
4. 网络分区导致的脑裂
问题:网络分区导致不同节点对锁的状态产生分歧。
解决方案:
-
设置合理的 quorum 数量
-
使用 Redlock 的时钟偏移检测机制
-
设置较短的锁过期时间
六、性能优化建议
1. 合理设置锁的粒度
-
避免使用过大的锁粒度(如整个业务模块)
-
尽量使用细粒度锁(如按商品ID、用户ID)
-
减少锁的持有时间
2. 优化锁的获取策略
public RedisLock acquireWithRetry(Jedis jedis, String lockKey, int maxRetries, long delayMs) {
for (int i = 0; i < maxRetries; i++) {
RedisLock lock = new RedisLock(jedis, lockKey, 30);
if (lock.acquire()) {
return lock;
}
try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
return null;
}
3. 使用连接池复用连接
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
// 创建连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(20);
poolConfig.setMinIdle(5);
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);
// 获取连接
try (Jedis jedis = jedisPool.getResource()) {
// 执行操作
jedis.set("key", "value");
}
4. 监控锁的使用情况
public void monitorLockUsage(Jedis jedis) {
// 获取所有锁的数量
Set<String> lockKeys = jedis.keys("lock:*");
System.out.println("当前持有锁数量: " + lockKeys.size());
// 检查过期时间
for (String key : lockKeys) {
Long ttl = jedis.ttl(key);
System.out.println(key + ": TTL=" + ttl + "秒");
}
}
七、总结
Redis 分布式锁是分布式系统中不可或缺的工具,正确使用能有效保证数据一致性。核心要点:
-
原子性:使用 SET NX EX 命令保证锁获取的原子性
-
安全性:使用 UUID 和 Lua 脚本防止误删锁
-
可靠性:设置合理的过期时间,避免死锁
-
进阶方案:根据业务需求选择 Redlock 或续约机制
在实际应用中,需要根据业务场景选择合适的锁策略,平衡一致性、可用性和性能之间的关系。