每日算法 — 使用java实现分布式锁:Redis红锁与多节点共识

概述

在分布式系统中,多个进程或服务可能需要协调对共享资源的访问。分布式锁是解决这一问题的经典方案。本文将用Java实现基于Redis的分布式锁,包括单节点锁、Redlock红锁算法以及锁的续约与自动释放机制,帮助读者理解分布式环境下的协调原语。

核心挑战

分布式锁需要满足四个基本条件:

  1. 互斥性:任意时刻只有一个客户端持有锁
  2. 防死锁:即使持有锁的客户端崩溃,锁也能最终释放
  3. 容错性:只要大多数Redis节点正常,锁就能正常工作
  4. 一致性:锁的获取和释放需要原子操作保证

Java实现

单节点Redis锁

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class RedisDistributedLock {
    private final JedisPool jedisPool;
    private final String lockKey;
    private final String lockValue;
    private final int expireSeconds;

    public RedisDistributedLock(JedisPool pool, String lockKey, int expireSeconds) {
        this.jedisPool = pool;
        this.lockKey = lockKey;
        this.expireSeconds = expireSeconds;
        // 使用UUID保证每个客户端的锁标识唯一
        this.lockValue = UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
    }

    /**
     * 尝试获取锁(非阻塞)
     * 使用 SET key value NX EX seconds 原子命令
     */
    public boolean tryLock() {
        try (Jedis jedis = jedisPool.getResource()) {
            SetParams params = SetParams.setParams()
                .nx()  // 仅当key不存在时才设置
                .ex(expireSeconds);  // 设置过期时间

            String result = jedis.set(lockKey, lockValue, params);
            return "OK".equals(result);
        }
    }

    /**
     * 阻塞式获取锁,带超时
     */
    public boolean lock(long waitTime, TimeUnit unit) throws InterruptedException {
        long deadline = System.nanoTime() + unit.toNanos(waitTime);
        while (System.nanoTime() < deadline) {
            if (tryLock()) {
                return true;
            }
            Thread.sleep(100); // 自旋间隔
        }
        return false;
    }

    /**
     * 释放锁:使用Lua脚本保证原子性
     * 仅当锁值匹配时才删除,防止误删其他客户端的锁
     */
    public boolean unlock() {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";

        try (Jedis jedis = jedisPool.getResource()) {
            Object result = jedis.eval(luaScript, 
                Collections.singletonList(lockKey),
                Collections.singletonList(lockValue));
            return Long.valueOf(1).equals(result);
        }
    }

    public String getLockValue() {
        return lockValue;
    }
}

看门狗自动续约

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class WatchdogRenewal {
    private final ScheduledExecutorService scheduler;
    private final RedisDistributedLock lock;
    private final int lockExpireSeconds;
    private final AtomicBoolean running = new AtomicBoolean(false);

    public WatchdogRenewal(RedisDistributedLock lock, int lockExpireSeconds) {
        this.lock = lock;
        this.lockExpireSeconds = lockExpireSeconds;
        this.scheduler = new ScheduledThreadPoolExecutor(1);
    }

    /**
     * 启动看门狗:每1/3过期时间续约一次
     */
    public void start() {
        if (running.compareAndSet(false, true)) {
            long renewInterval = lockExpireSeconds * 1000 / 3;
            scheduler.scheduleAtFixedRate(this::renew, 
                renewInterval, renewInterval, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * 续约逻辑:检查锁是否仍属于自己,然后延长过期时间
     */
    private void renew() {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('expire', KEYS[1], ARGV[2]) " +
            "else " +
            "    return 0 " +
            "end";

        try (Jedis jedis = lock.getJedisPool().getResource()) {
            Object result = jedis.eval(luaScript,
                Collections.singletonList(lock.getLockKey()),
                Arrays.asList(lock.getLockValue(), String.valueOf(lockExpireSeconds)));

            if (!Long.valueOf(1).equals(result)) {
                // 续约失败,锁可能已丢失
                stop();
            }
        }
    }

    public void stop() {
        if (running.compareAndSet(true, false)) {
            scheduler.shutdown();
        }
    }
}

Redlock红锁算法

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class RedLock {
    private final List<JedisPool> redisNodes;
    private final int quorum;  // 多数派阈值 = N/2 + 1
    private final int lockExpireMs;

    public RedLock(List<JedisPool> nodes, int lockExpireMs) {
        this.redisNodes = nodes;
        this.quorum = nodes.size() / 2 + 1;
        this.lockExpireMs = lockExpireMs;
    }

    /**
     * Redlock算法核心步骤:
     * 1. 获取当前时间戳
     * 2. 依次向所有N个节点申请锁,使用相同的key和随机value
     * 3. 计算获取锁的总耗时
     * 4. 若成功获取锁的节点数 >= 多数派,且总耗时 < 锁过期时间,则加锁成功
     * 5. 若失败,向所有节点发送释放锁请求
     */
    public LockResult tryLock(String resourceName) {
        String lockValue = generateUniqueValue();
        long startTime = System.currentTimeMillis();
        int successCount = 0;
        List<JedisPool> lockedNodes = new ArrayList<>();

        for (JedisPool node : redisNodes) {
            try (Jedis jedis = node.getResource()) {
                // 使用PX指定毫秒级过期时间
                String result = jedis.set(resourceName, lockValue, 
                    SetParams.setParams().nx().px(lockExpireMs));

                if ("OK".equals(result)) {
                    successCount++;
                    lockedNodes.add(node);
                }
            } catch (Exception ex) {
                // 单个节点故障不影响整体判断
            }

            // 随机延迟,避免所有客户端同时请求导致活锁
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(5));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        long elapsed = System.currentTimeMillis() - startTime;
        boolean locked = successCount >= quorum && elapsed < lockExpireMs;

        if (!locked) {
            // 加锁失败,释放已获取的锁
            unlockOnAllNodes(resourceName, lockValue);
            return new LockResult(false, null, null);
        }

        // 计算有效过期时间 = 原始过期时间 - 获取锁耗时 - 时钟漂移补偿(2ms)
        long validityTime = lockExpireMs - elapsed - 2;
        return new LockResult(true, lockValue, lockedNodes, validityTime);
    }

    /**
     * 在所有节点上释放锁
     */
    public void unlock(String resourceName, String lockValue, List<JedisPool> nodes) {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";

        for (JedisPool node : nodes) {
            try (Jedis jedis = node.getResource()) {
                jedis.eval(luaScript, 
                    Collections.singletonList(resourceName),
                    Collections.singletonList(lockValue));
            } catch (Exception ex) {
                // 忽略释放失败的节点
            }
        }
    }

    private void unlockOnAllNodes(String resourceName, String lockValue) {
        unlock(resourceName, lockValue, redisNodes);
    }

    private String generateUniqueValue() {
        return UUID.randomUUID().toString() + "-" + System.nanoTime();
    }

    public static class LockResult {
        public final boolean success;
        public final String lockValue;
        public final List<JedisPool> lockedNodes;
        public final long validityTime;

        public LockResult(boolean success, String lockValue, 
                         List<JedisPool> lockedNodes, long validityTime) {
            this.success = success;
            this.lockValue = lockValue;
            this.lockedNodes = lockedNodes;
            this.validityTime = validityTime;
        }

        public LockResult(boolean success, String lockValue, List<JedisPool> lockedNodes) {
            this(success, lockValue, lockedNodes, 0);
        }
    }
}

基于Redlock的分布式计数器

public class DistributedCounter {
    private final RedLock redLock;
    private final String counterKey;
    private final JedisPool masterPool;

    public DistributedCounter(RedLock redLock, String counterKey, JedisPool masterPool) {
        this.redLock = redLock;
        this.counterKey = counterKey;
        this.masterPool = masterPool;
    }

    /**
     * 原子递增:先获取Redlock,再执行Redis INCR
     */
    public long increment() {
        RedLock.LockResult lock = redLock.tryLock(counterKey + ":lock");
        if (!lock.success) {
            throw new RuntimeException("无法获取分布式锁");
        }

        try {
            // 在有效时间内完成操作
            long deadline = System.currentTimeMillis() + lock.validityTime;

            try (Jedis jedis = masterPool.getResource()) {
                long newValue = jedis.incr(counterKey);

                if (System.currentTimeMillis() > deadline) {
                    throw new RuntimeException("锁已过期,操作可能不安全");
                }

                return newValue;
            }
        } finally {
            redLock.unlock(counterKey + ":lock", lock.lockValue, lock.lockedNodes);
        }
    }
}

算法分析

容错能力

总节点数N 多数派阈值 可容忍故障
3 2 1
5 3 2
7 4 3

时间复杂度

操作 时间复杂度
单节点加锁 O(1)
Redlock加锁 O(N) N为Redis节点数
解锁 O(N)
看门狗续约 O(1) 每轮

实际应用建议

  1. 时钟同步:Redlock假设各节点时钟大致同步,最大漂移应远小于锁过期时间
  2. 锁续期:业务执行时间不确定时,务必使用看门狗机制
  3. 锁粒度:锁的粒度应尽可能小,减少竞争
  4. 重试策略:获取锁失败时采用指数退避,避免活锁
  5. 熔断机制:连续获取锁失败时应触发熔断,保护系统

总结

本文实现了从单节点Redis锁到Redlock红锁的完整分布式锁方案,涵盖了原子加锁、Lua脚本安全释放、看门狗自动续约等核心机制。分布式锁的本质是在无共享内存的环境中,通过外部协调服务实现互斥原语。理解其原理对于构建可靠的分布式系统至关重要。