Redis 分布式锁的实现原理

Redis 分布式锁的实现原理

Redis 分布式锁通常利用 Redis 提供的原子操作来实现。最常用的方式是使用 SET 命令的某些选项,如 NX(Not eXists,意为"仅当键不存在时")和 EX(Expire,意为"设置键的过期时间")。以下是 Redis 实现分布式锁的基本步骤:

  1. 客户端 A 想要获得一个锁,它会生成一个随机的唯一值作为锁的值(通常是一个随机的 UUID),然后用 SET 命令尝试设置一个 key,例如 "lock_key"。客户端将使用 NX 选项确保只有在 key 不存在时才进行设置操作,并使用 EX 选项设置一个过期时间,以避免死锁。
sql 复制代码
SET lock_key random_value NX EX 10

这个命令尝试将 "lock_key" 的值设置为 "random_value",仅当 "lock_key" 不存在时才设置,并且设置 "lock_key" 10秒后过期。

  1. 如果 SET 操作成功,客户端 A 获得了锁,并且可以执行它的操作。在这个期间,任何其他客户端尝试获取同一个锁将会失败。

  2. 客户端 A 在执行完它的操作后,会删除这个锁。这里有一个需要注意的地方,为了安全地释放锁,客户端 A 需要先确认锁内的值是它最初设置的值(即 "random_value"),然后再删除。这需要使用 Lua 脚本来原子地执行这两个操作。

lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then

    return redis.call("del", KEYS[1])
else

    return 0

end

这个 Lua 脚本确保只有设置了锁的客户端可以释放这个锁。

  1. 如果客户端 A 在它的锁过期之前未能释放锁(比如因为客户端崩溃或操作耗时太长等原因),锁将自动过期,从而允许其他客户端可以获得锁。这样保证了即使发生异常情况,锁资源也不会被永久占用。

要注意的是,上述方法依赖于时间的一致性,因此在分布式系统中可能会有风险,特别是在时间不同步或网络延迟的情境下。因此,有一些更复杂的算法被设计出来提高分布式锁的安全性及可靠性,比如 Redlock 算法。

Redlock 算法

Redlock 这种方法会用到多个 Redis 实例来降低单点故障的风险。客户端尝试在多个实例上依次获取锁,并且只有在大多数实例上成功获取锁时,才认为整个锁定操作成功。不过 Redlock 算法的有效性和安全性在社区中存在争议,工程应用时需要进行严格的评估。

Redis 官方推荐的 Redisson 客户端库

此外,Redis 官方推荐使用 Redisson 这个 Java 客户端库,它提供了成熟的分布式锁功能,包括重试机制、锁的自动续期等。

总结

利用 Redis 提供的基本原子操作可以实现简单的分布式锁,但对于需要更高稳定性和安全性的场景,可能需要更复杂的算法或库支持。在分布式系统中实现锁机制总是需要细心设计和严格测试的。

java 复制代码
package org.middleware.redis.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author 周康(周康)
 * @date 2024/3/11 16:14
 */
@Slf4j
@Component
public class RedisLock {
    private static final String KEY_PRE = "{WF_RLock}_%s";

    //占用锁的最大时间 单位 秒
    private static final int LOCK_HOLDING_MAX_SECONDS = 4;

    // 线程休息时间 单位 毫秒
    private static final int LOCK_RETRY_INTERVAL = 50;

    private static final String UNLOCK_LUA_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "    return redis.call('del', KEYS[1]) " +
                    "else " +
                    "    return 0 " +
                    "end";

    /**
     * 锁定库存的脚本,是保证setnx和 expire操作的原子
     */
    //@formatter:off
    private static final String LOCK_LUA_SCRIPT =
            "local flag = redis.call('setnx',KEYS[1],0); " +
                    "if(flag == 1) then " +
                    "local maxtime = tonumber(ARGV[1]); " +
                    "redis.call('expire',KEYS[1],maxtime) " +
                    "end ; " +
                    "return tonumber(flag) == 1;";
    //@formatter:on

    private static final String LOCK_LUA_VALUE_SCRIPT =
            "local flag = redis.call('setnx',KEYS[1],KEYS[2]); " +
                    "if(flag == 1) then " +
                    "local maxtime = tonumber(ARGV[1]); " +
                    "redis.call('expire',KEYS[1],maxtime) " +
                    "end ; " +
                    "return tonumber(flag) == 1;";

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisLock(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 尝试获取锁,会在等待时长里每隔50ms轮询再次尝试获取锁
     *
     * @param lockKey          lockKey
     * @param maxWaitingMillis 最大等待时间 毫秒, <=0,则不进行尝试
     */
    public boolean tryLock(String lockKey, int maxWaitingMillis) {

        if (maxWaitingMillis <= 0) {
            if (this.lock(lockKey)) {
                return true;
            } else {
                log.debug("获取锁失败,lockKey:{}", lockKey);
                return false;
            }
        }

        long curTime = System.currentTimeMillis();
        while ((System.currentTimeMillis() - curTime) < TimeUnit.MILLISECONDS.toMillis(maxWaitingMillis)) {
            try {
                if (this.lock(lockKey)) {
                    return true;
                }

                TimeUnit.MILLISECONDS.sleep(LOCK_RETRY_INTERVAL);
            } catch (InterruptedException e) {
                log.debug("获取锁失败,lockKey:{}", lockKey, e);
            }
        }

        return false;
    }

    /**
     * 尝试获取锁,会在等待时长里每隔50ms轮询再次尝试获取锁
     *
     * @param lockKey          lockKey
     * @param maxWaitingMillis 最大等待时间 毫秒, <=0,则不进行尝试
     * @param value            所锁定的值
     */
    public boolean tryLock(String lockKey, int maxWaitingMillis, String value) {

        if (maxWaitingMillis <= 0) {
            if (this.lock(lockKey, value)) {
                return true;
            } else {
                log.debug("获取锁失败,lockKey:{}", lockKey);
                return false;
            }
        }

        long curTime = System.currentTimeMillis();
        while ((System.currentTimeMillis() - curTime) < TimeUnit.MILLISECONDS.toMillis(maxWaitingMillis)) {
            try {
                if (this.lock(lockKey, value)) {
                    return true;
                }

                TimeUnit.MILLISECONDS.sleep(LOCK_RETRY_INTERVAL);
            } catch (InterruptedException e) {
                log.debug("获取锁失败,lockKey:{}", lockKey, e);
            }
        }

        return false;
    }

    /**
     * 获取锁,获取成功,返回true,否则返回false
     */
    public boolean lock(String lockKey) {
        Boolean locked = redisTemplate.execute(
                new DefaultRedisScript<>(LOCK_LUA_SCRIPT, Boolean.class),
                Collections.singletonList(String.format(KEY_PRE, lockKey)),
                LOCK_HOLDING_MAX_SECONDS);
        return locked != null && locked;
    }

    /**
     * 获取锁,并锁定值 ,获取成功,返回true,否则返回false
     */
    public boolean lock(String lockKey, String value) {
        Boolean locked = redisTemplate.execute(
                new DefaultRedisScript<>(LOCK_LUA_VALUE_SCRIPT, Boolean.class),
                Arrays.asList(String.format(KEY_PRE, lockKey), value),
                LOCK_HOLDING_MAX_SECONDS);
        return locked != null && locked;
    }

    public void releaseLock(String lockKey) {
        redisTemplate.delete(String.format(KEY_PRE, lockKey));
    }

    /**
     * 进行释放锁,当锁定的值与所需要释放的值相等时才进行释放
     */
    public void releaseLock(String lockKey, String value) {
        redisTemplate.execute(
                new DefaultRedisScript<>(UNLOCK_LUA_SCRIPT, Boolean.class),
                Collections.singletonList(String.format(KEY_PRE, lockKey)),
                value);
    }
}
相关推荐
Hello.Reader32 分钟前
Redis热点数据管理全解析:从MySQL同步到高效缓存的完整解决方案
redis·mysql·缓存
C++忠实粉丝2 小时前
Redis 介绍和安装
数据库·redis·缓存
ClouGence3 小时前
Redis 到 Redis 数据迁移同步
数据库·redis·缓存
苏三说技术3 小时前
Redis 性能优化的18招
数据库·redis·性能优化
Tttian6224 小时前
基于Pycharm与数据库的新闻管理系统(2)Redis
数据库·redis·pycharm
言之。4 小时前
redis延迟队列
redis
hanbarger5 小时前
nosql,Redis,minio,elasticsearch
数据库·redis·nosql
弗罗里达老大爷6 小时前
Redis
数据库·redis·缓存
DT辰白20 小时前
基于Redis的网关鉴权方案与性能优化
数据库·redis·缓存
木子七21 小时前
Redis-十大数据类型
redis