如何用Redis实现分布式锁并避免死锁?

wen PHP项目 43

本文目录导读:

如何用Redis实现分布式锁并避免死锁?

  1. 基础实现(SET NX + 过期时间)
  2. 完整避免死锁的策略
  3. 最佳实践模板
  4. 注意事项与常见陷阱
  5. 性能优化建议

使用Redis实现分布式锁的核心是通过原子操作过期时间来避免死锁,以下是完整的技术方案和最佳实践。

基础实现(SET NX + 过期时间)

加锁命令

SET lock_key random_value NX PX 30000
  • NX:键不存在时才能设置成功
  • PX 30000:设置30秒自动过期,防止死锁
  • random_value:唯一随机值,用于安全释放锁

Java代码示例

import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisDistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private Jedis jedis;
    private String lockKey;
    private String requestId;
    private int expireTime = 30000; // 30秒过期
    public boolean tryLock(String lockKey) {
        requestId = UUID.randomUUID().toString();
        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
        return LOCK_SUCCESS.equals(result);
    }
    public boolean unlock(String lockKey) {
        // Lua脚本保证原子性
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        Object result = jedis.eval(script, 
            Collections.singletonList(lockKey), 
            Collections.singletonList(requestId));
        return RELEASE_SUCCESS.equals(result);
    }
}

完整避免死锁的策略

设置合理的过期时间

// 根据业务执行时间动态调整
int estimatedTime = 5000; // 预估执行时间
int expireTime = estimatedTime * 3; // 设置3倍缓冲
jedis.set(key, value, "NX", "PX", expireTime);

自动续期(看门狗模式)

public class LockWithWatchdog {
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private volatile boolean running = true;
    public boolean tryLock(String key, int expireTime) {
        if (acquireLock(key, expireTime)) {
            startWatchdog(key, expireTime);
            return true;
        }
        return false;
    }
    private void startWatchdog(String key, int expireTime) {
        scheduler.scheduleAtFixedRate(() -> {
            if (running) {
                // 续期操作
                jedis.expire(key, expireTime / 1000);
            }
        }, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS);
    }
    public void unlock(String key) {
        running = false;
        scheduler.shutdown();
        // 释放锁的Lua脚本
    }
}

Redlock算法(高可用场景)

public class Redlock {
    private List<Jedis> redisNodes; // 多个独立的Redis节点
    public boolean tryLock(String key, int ttl) {
        int n = redisNodes.size();
        int quorum = n / 2 + 1;
        int startTime = System.currentTimeMillis();
        int lockCount = 0;
        for (Jedis node : redisNodes) {
            String result = node.set(key, value, "NX", "PX", ttl);
            if ("OK".equals(result)) {
                lockCount++;
            }
        }
        int elapsedTime = System.currentTimeMillis() - startTime;
        if (lockCount >= quorum && elapsedTime < ttl) {
            // 获取锁成功
            return true;
        } else {
            // 释放部分获取的锁
            for (Jedis node : redisNodes) {
                unlock(node, key, value);
            }
            return false;
        }
    }
}

最佳实践模板

完整的生产级实现

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class RedisLockTemplate {
    private Jedis jedis;
    public <T> T executeWithLock(String lockKey, 
                                  int expireTimeMs,
                                  int waitTimeoutMs,
                                  Supplier<T> action) {
        String requestId = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();
        try {
            // 自旋获取锁
            while (System.currentTimeMillis() - startTime < waitTimeoutMs) {
                String result = jedis.set(lockKey, requestId, 
                    SetParams.setParams().nx().px(expireTimeMs));
                if ("OK".equals(result)) {
                    try {
                        // 执行业务逻辑
                        return action.get();
                    } finally {
                        // 安全释放锁
                        releaseLock(lockKey, requestId);
                    }
                }
                // 避免忙等,稍作休眠
                Thread.sleep(100);
            }
            throw new RuntimeException("获取锁超时");
        } catch (Exception e) {
            throw new RuntimeException("执行失败", e);
        }
    }
    private boolean releaseLock(String key, String requestId) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        Object result = jedis.eval(script, 
            Collections.singletonList(key), 
            Collections.singletonList(requestId));
        return Long.valueOf(1).equals(result);
    }
}
// 使用示例
RedisLockTemplate lockTemplate = new RedisLockTemplate();
Integer result = lockTemplate.executeWithLock(
    "order:123", 
    30000,  // 锁过期时间
    5000,   // 等待超时时间
    () -> {
        // 业务逻辑
        return processOrder(123);
    }
);

注意事项与常见陷阱

❌ 常见错误

  1. 不用原子操作:先检查再删除,非原子操作可能导致误删
  2. 过期时间设置不合理:太短业务没完成,太长影响并发
  3. 使用相同的value:无法区分不同客户端,可能释放别人的锁

✅ 正确做法

  • 使用Lua脚本保证原子性
  • 生成唯一随机值(UUID)
  • 设置合理的过期时间 + 续期机制
  • 考虑时钟漂移问题(Redlock中特别重要)

性能优化建议

场景 推荐方案
低并发、简单需求 SET NX + 过期时间
高并发、需要自动续期 看门狗模式
分布式、高可靠性 Redlock算法
已使用Redisson 直接使用其内置Lock

最简可靠性方案

// 加锁
SET key uuid NX PX 30000
// 释放锁(Lua脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这个方案通过过期时间防止死锁,通过Lua脚本保证原子释放,是生产环境最常用的实现方式,对于高可用场景,建议使用Redlock或直接采用Redisson等成熟框架。

抱歉,评论功能暂时关闭!