什么是分布式锁

分布式锁是在分布式系统中用于控制多个进程/服务对共享资源进行互斥访问的一种机制。在单机系统中,我们可以使用语言提供的锁机制(如Java的synchronized或ReentrantLock),但在分布式环境下,这些本地锁无法跨进程/机器工作,因此需要分布式锁。

Redis实现分布式锁的基本原理

Redis实现分布式锁主要依赖其单线程执行命令的特性,以及SETNX(SET if Not eXists)命令:

  1. SETNX命令:当key不存在时设置值,返回1;key已存在则不做任何操作,返回0

  2. DEL命令:释放锁时删除key

基本流程:

加锁:SETNX lock_key unique_value
解锁:DEL lock_key
Redis分布式锁的正确实现方式
基本实现方案
# 加锁
SET resource_name my_random_value NX PX 30000

# 解锁(使用Lua脚本保证原子性)
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

关键点:

  1. 使用NX参数实现SETNX的效果

  2. 使用PX设置过期时间,防止客户端崩溃导致锁无法释放

  3. 解锁时使用Lua脚本保证原子性,并验证value值,防止误删其他客户端的锁

Redlock算法

Redis作者提出的更安全的分布式锁算法,适用于Redis集群环境:

  1. 获取当前时间(毫秒)

  2. 依次尝试从N个独立的Redis实例获取锁(使用相同的key和随机value)

  3. 计算获取锁花费的时间(当前时间减去步骤1的时间),当且仅当从大多数(N/2+1)实例获取锁成功,并且总耗时小于锁的过期时间,才认为加锁成功

  4. 如果加锁成功,锁的有效时间 = 初始有效时间 - 获取锁花费的时间

  5. 如果加锁失败,向所有Redis实例发起释放锁请求

Redis分布式锁的最佳实践

  1. 设置合理的过期时间:不宜过长(影响系统可用性)或过短(可能导致任务未完成锁已释放)

  2. 使用唯一标识作为value:通常使用UUID或客户端ID+线程ID,确保只有锁的持有者才能释放锁

  3. 避免长时间持有锁:业务逻辑应尽量快速执行,减少锁的持有时间

  4. 考虑锁的可重入性:如果需要可重入锁,需要在客户端实现计数逻辑

  5. 实现锁续约机制:对于执行时间不确定的任务,可以定期延长锁的过期时间(看门狗机制)

Redis分布式锁的优缺点

优点

  • 实现相对简单

  • 性能高

  • 社区支持完善,有多种客户端实现

缺点

  • 单Redis实例有单点故障风险

  • 主从切换可能导致锁失效(Redlock可以缓解)

  • 需要处理网络分区等复杂场景

与其他分布式锁方案的对比

  1. Zookeeper分布式锁

    • 基于临时顺序节点实现

    • 更严格的强一致性保证

    • 性能通常低于Redis

  2. 数据库分布式锁

    • 基于唯一约束或乐观锁实现

    • 性能较差,不推荐高并发场景使用

常见问题及解决方案

  1. 锁过期但任务未完成

    • 实现锁续约机制

    • 合理设置超时时间

  2. 客户端长时间阻塞导致锁失效

    • 使用带超时的获取锁操作

    • 实现锁获取失败的回退策略

  3. Redis主从切换导致锁丢失

    • 使用Redlock算法

    • 考虑使用Redis集群模式

Redis分布式锁在大多数场景下是一个简单高效的解决方案,但在对一致性要求极高的场景下,可能需要考虑Zookeeper等替代方案。

具体实例:

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisUtils {

    private static RedissonClient REDISSON_CLIENT;

    public RedisUtils(@Autowired RedissonClient redissonClient) {
        REDISSON_CLIENT = redissonClient;
    }

    /**
     * 尝试获取锁
     *
     * @param key 锁key
     * @param waitTime  最多等待时间,0:表示不等待,只尝试加锁一次
     * @param leaseTime 上锁后自动释放锁时间,-1:表示不自动释放锁
     * @return 获取锁成功返回true 失败返回false
     */
    public static boolean tryLock(String key, long waitTime, long leaseTime) {
        RLock lock = REDISSON_CLIENT.getLock(key);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            return false;
        }
    }

    public static void unlock(String key) {
        RLock lock = REDISSON_CLIENT.getLock(key);
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
        } else {
            log.warn("redis锁已超时释放");
        }
    }

}

使用

            String lockKey = CommonConstant.INIT_QW_USER_LOCK_KEY;
            // 只加一次锁,获取不到证明已经有一个任务获取到了
            if (RedisUtils.tryLock(lockKey, 0, 60 * 25)) {
                try {
                    log.info("分布式锁加锁成功, lockKey = {}", lockKey);
                    qwUserService.sync(null);
                } finally {
                    RedisUtils.unlock(lockKey);
                    log.info("分布式锁释放成功, lockKey = {}", lockKey);
                }
            } else {
                log.info("未获取到分布式锁");
            }
        

Logo

更多推荐