1. 非原子操作问题

在实现Redis分布式锁时,开发者首先可能会想到使用setNx命令:

java

if (jedis.setnx(lockKey, val) == 1) {
   jedis.expire(lockKey, timeout);
}

这段代码虽然能够完成加锁,但存在一个严重问题:加锁与设置超时时间是两个独立操作,不具备原子性。若在加锁成功后、设置超时时间前发生异常,将导致锁无法自动释放,在高并发场景下可能引发Redis内存溢出。

解决方案:使用Redis的set命令,支持多参数原子操作:

java

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
    return true;
}
return false;
  • lockKey:锁标识

  • requestId:唯一请求ID

  • NX:仅当键不存在时执行设置

  • PX:设置毫秒级超时时间

  • expireTime:具体过期时长

通过一条命令原子性地完成加锁与超时设置,彻底解决非原子操作问题。


2. 锁释放遗漏问题

尽管set命令解决了原子加锁问题,但若加锁后未及时释放,仍会引发资源占用。合理的分布式锁流程应为:

  1. 手动加锁

  2. 执行业务逻辑

  3. 手动释放锁

  4. 若释放失败,依赖超时机制自动释放

基础实现代码:

java

try {
    String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    if ("OK".equals(result)) {
        // 业务逻辑
        return true;
    }
    return false;
} finally {
    unlock(lockKey); // 确保锁释放
}

关键点:

  • 在finally块中释放锁,确保异常场景下锁仍能被释放

  • 若释放时发生系统重启或网络中断,通过预设超时时间保证最终释放


3. 误释放他人锁问题

在并发场景下,若线程A因业务执行超时导致锁自动释放,此时线程B获取锁后,线程A完成业务并释放锁,将导致线程B的锁被意外释放。

解决方案:基于requestId验证锁归属:

java

if (jedis.get(lockKey).equals(requestId)) {
    jedis.del(lockKey);
    return true;
}
return false;

或使用Lua脚本保证原子性验证与释放:

java

String script = 
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "   return redis.call('del', KEYS[1]) " +
    "else " +
    "   return 0 " +
    "end";
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

4. 大量请求失败问题

在高并发场景下,大量线程竞争同一把锁会导致绝大多数请求失败。例如秒杀系统中,直接加锁会形成均匀秒杀,不符合业务预期。

解决方案:引入自旋锁机制

java

try {
    Long start = System.currentTimeMillis();
    while(true) {
        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
        if ("OK".equals(result)) {
            // 业务逻辑
            return true;
        }
        
        long elapsed = System.currentTimeMillis() - start;
        if (elapsed >= timeout) return false;
        
        try {
            Thread.sleep(50); // 控制重试频率
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
} finally {
    unlock(lockKey, requestId);
}

通过有限时间的重试,平衡锁竞争与系统性能。


5. 锁重入问题

在递归调用或嵌套方法中,同一线程需要多次获取同一把锁时,基本的Redis分布式锁不支持重入,导致死锁。

解决方案:使用Redisson实现的可重入锁

java

private int expireTime = 1000;

public void run(String lockKey) {
    RLock lock = redisson.getLock(lockKey);
    recursiveMethod(lock, 1);
}

public void recursiveMethod(RLock lock, int level) {
    try {
        lock.lock(5, TimeUnit.SECONDS);
        if (level <= 10) {
            recursiveMethod(lock, level + 1);
        }
    } finally {
        lock.unlock();
    }
}

Redisson通过Lua脚本实现重入计数:

lua

-- 加锁脚本
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
return redis.call('pttl', KEYS[1]);

6. 锁竞争性能问题
6.1 读写锁分离

对于读多写少的场景,使用读写锁提升性能:

java

// 读锁
RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock readLock = readWriteLock.readLock();
readLock.lock();
try {
    // 读操作
} finally {
    readLock.unlock();
}

// 写锁
RLock writeLock = readWriteLock.writeLock();
writeLock.lock();
try {
    // 写操作
} finally {
    writeLock.unlock();
}

特性总结:

  • 读读共享

  • 读写互斥

  • 写写互斥

6.2 锁分段

借鉴ConcurrentHashMap思想,将锁粒度细化:

java

// 基于用户ID的路由分段
int segment = Math.abs(userId.hashCode() % 100);
String segmentKey = "stock_segment_" + segment;

将原来单锁竞争转化为多锁并行,显著提升系统吞吐量。


7. 锁超时与自动续期问题

当业务执行时间超过锁超时时间时,会导致锁提前释放,临界资源失去保护。

解决方案:实现看门狗自动续期机制

java

// Redisson看门狗原理
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 1; 
end;
return 0;

建议设置总超时时间上限(如30秒),避免无限续期。


8. 主从架构下的锁失效问题

在Redis主从架构中,若主节点加锁后未同步至从节点即发生故障,将导致锁信息丢失。

解决方案:Redlock算法

java

RedissonClient client1 = // 实例1
RedissonClient client2 = // 实例2  
RedissonClient client3 = // 实例3

RedissonRedLock lock = new RedissonRedLock(
    client1.getLock("lock1"),
    client2.getLock("lock2"), 
    client3.getLock("lock3")
);
lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

核心原则:在N个独立实例中,至少获得N/2+1个锁才算成功。


分布式锁选择建议

在分布式系统CAP理论框架下:

  • CP型锁(如ZooKeeper):强一致性优先,适合金融、交易等场景

  • AP型锁(如Redis):高可用性优先,适合大多数互联网业务场景

建议根据业务特征进行技术选型,在保证系统可用性的前提下,通过最终一致性方案解决数据一致性问题。

更多推荐