要实现分布式场景下的资源互斥,Redis是很多开发者第一时间想到的工具,但很多人不知道,看似简单的Redis分布式锁,从单节点崩溃到多节点的RedLock方案,在高并发下可能悄无声息就失效了,今天就把这些坑讲透,从原理到示例,再到实际避坑。

一、Redis分布式锁的基础实现:单节点模式

1.1 单节点锁的核心逻辑和示例

单节点锁的核心是在Redis里占一个专属“坑”,只有成功占坑的线程才能执行业务,用完再释放坑,防止其他线程占用。这里用Java+Jedis作为技术栈,示例代码完整,注释清晰:

import redis.clients.jedis.Jedis;
import java.util.UUID;

public class SingleRedisLock {
    // 锁的key,对应要互斥的资源,比如秒杀商品ID
    private static final String LOCK_KEY = "seckill_goods_1001";
    // 锁过期时间,单位毫秒,防止业务异常导致死锁
    private static final int LOCK_EXPIRE = 3000;
    // 线程本地变量,存每个线程的唯一锁标识,释放锁时必须对应
    private static final ThreadLocal<String> CURRENT_LOCK = new ThreadLocal<>();

    // 尝试加锁方法,返回是否成功
    public boolean tryLock(Jedis jedis) {
        // 生成每个线程独有的随机值,避免误删其他线程的锁
        String lockValue = UUID.randomUUID().toString();
        // Redis SET命令参数:NX=不存在key才设置,PX=设置过期时间,一步完成加锁
        String result = jedis.set(LOCK_KEY, lockValue, "NX", "PX", LOCK_EXPIRE);
        if ("OK".equals(result)) {
            CURRENT_LOCK.set(lockValue);
            return true;
        }
        return false;
    }

    // 释放锁方法,必须保证原子操作,防止中间被其他线程打断
    public boolean releaseLock(Jedis jedis) {
        String lockValue = CURRENT_LOCK.get();
        if (lockValue == null) return false;
        // Lua脚本:先判断锁是不是当前线程的,再删除,避免误删
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object delResult = jedis.eval(script, 1, LOCK_KEY, lockValue);
        CURRENT_LOCK.remove();
        return (Long) delResult == 1;
    }

    public static void main(String[] args) throws InterruptedException {
        Jedis jedis = new Jedis("localhost", 6379);
        SingleRedisLock lock = new SingleRedisLock();
        // 模拟线程A执行业务
        new Thread(() -> {
            if (lock.tryLock(jedis)) {
                try {
                    System.out.println("线程A拿到锁,开始执行业务(扣库存)...");
                    Thread.sleep(2500); // 模拟业务执行时间,小于锁过期3秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.releaseLock(jedis);
                    System.out.println("线程A释放锁");
                }
            } else {
                System.out.println("线程A加锁失败");
            }
        }).start();
    }
}

1.2 单节点锁的明显漏洞:节点崩溃和主从切换

单节点锁的最大问题是单点故障:如果Redis主节点挂了,主从切换到从节点需要时间,这段时间里从节点没有同步锁数据,新的请求可以正常加锁,导致多个线程同时执行核心业务。比如电商秒杀场景,1000件商品,主Redis崩溃后,主从切换耗时10秒,1000个请求全部加锁,最终库存会变成负数,直接损失惨重。

二、RedLock登场:多节点的容灾方案

2.1 RedLock的原理和实现

为了解决单节点的单点问题,Redis官方提出了RedLock方案,核心逻辑是用至少3个独立的Redis节点,在超过半数节点(比如3个节点需要2个成功)上加锁,才算最终拿到锁。这样单个节点挂了,剩余节点仍能保证互斥,提升容灾性。RedLock的加锁逻辑是:每个节点加锁时设置统一的过期时间,所有节点加锁的总耗时不能超过锁的过期时间,否则视为加锁失败。

2.2 RedLock的隐形坑:时钟漂移和高并发失效

RedLock看似解决了单点问题,但设计上有个致命缺陷:它依赖节点的时钟同步。实际场景中,节点时钟不可能完全一致,会存在时钟漂移(比如两个节点时钟差500毫秒),再加上网络延迟,高并发下很容易出现锁过期重叠,导致多个实例同时持有锁。比如两个节点A和B,时钟差400毫秒,锁过期时间设为2秒,节点A加锁成功后,锁过期时间是加锁时间+2秒;节点B加锁时,因为时钟慢,看到的时间比节点A晚400毫秒,此时节点A的锁已经过期,节点B的锁还没到自己的过期时间,两个节点都认为自己持有锁,同时执行扣库存业务,最终超卖。

三、高并发场景下的真实失效示例:秒杀业务

3.1 场景还原

某电商平台做限时秒杀,一款手机只有1000件库存,用RedLock方案,3个Redis节点,锁过期时间设为2秒。平台部署了2个服务实例S1和S2,S1的时钟比S2快500毫秒。秒杀开始后,1000个并发请求中,500个请求落到S1,500个落到S2。S1的请求先在3个Redis节点加锁,耗时100毫秒,锁过期时间为加锁时间+2秒;S2的请求因为网络延迟,加锁时S1的锁已经过了节点B的时间(时钟慢500毫秒),S2的请求成功在剩下的2个节点加锁,此时S1和S2都持有锁,同时扣库存,最终库存变成998,超卖2件,直接损失超百万。

3.2 失效的根源设计缺陷

RedLock的假设是“节点时钟同步、网络延迟可控”,但实际分布式环境中,这两个条件都无法满足。高并发下,加锁请求的网络延迟差异会放大,时钟漂移会导致锁过期时间重叠,最终多个实例同时持有锁,互斥保护失效,这种问题是隐形的,线上只有出了超卖、重复支付等问题才会被发现。

四、分布式锁的应用场景、优缺点和避坑要点

4.1 应用场景

单节点Redis锁适合并发量小(每秒几百次请求)、节点稳定、对容灾要求不高的场景,比如内部管理系统的资源互斥、个人博客的评论限流;RedLock适合并发量极高(每秒几万次请求)、容灾要求高的核心场景,比如电商秒杀、订单支付、库存扣减,但必须做好时钟同步。

4.2 技术优缺点

单节点锁的优点是实现简单、代码量少、性能高;缺点是单点故障,主从切换时锁失效,锁容易丢失。RedLock的优点是容灾性好,单个节点挂了不影响整体;缺点是实现复杂,对时钟和网络敏感,高并发下容易出现锁失效,性能比单节点稍低,开发和运维成本高。

4.3 注意事项

  1. 锁的过期时间要根据业务执行时间设置,既不能太长浪费资源,也不能太短导致业务没执行完锁就释放;
  2. 释放锁必须用Lua脚本,保证判断锁和删除锁是原子操作,防止误删其他线程的锁;
  3. RedLock节点必须用NTP同步时钟,减少时钟漂移,最好用时间差控制在100毫秒以内;
  4. 高并发核心业务尽量避免用RedLock,改用ZooKeeper的临时节点锁,虽然性能低但更可靠;
  5. 锁的key要对应唯一资源,比如用“业务类型+资源ID”,避免混淆不同资源的锁。

五、总结

Redis分布式锁是解决分布式互斥的常用工具,但不同方案都有设计缺陷:单节点锁的单点问题,RedLock的时钟依赖问题,在高并发场景下都可能导致互斥保护失效,而且这种失效是隐形的,很难通过日志提前发现。开发者要根据业务场景选择合适的锁方案,同时一定要做好锁的校验和释放,避免线上出现库存超卖、重复支付等核心问题,给业务带来损失。