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);
    }
}
相关推荐
BergerLee12 小时前
对不经常变动的数据集合添加Redis缓存
数据库·redis·缓存
huapiaoy12 小时前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
【D'accumulation】13 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
Cikiss13 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
一休哥助手14 小时前
Redis 五种数据类型及底层数据结构详解
数据结构·数据库·redis
盒马盒马15 小时前
Redis:zset类型
数据库·redis
Jay_fearless17 小时前
Redis SpringBoot项目学习
spring boot·redis
Wang's Blog17 小时前
Redis: 集群环境搭建,集群状态检查,分析主从日志,查看集群信息
数据库·redis
wclass-zhengge1 天前
Redis篇(最佳实践)(持续更新迭代)
redis·缓存·bootstrap
Dylanioucn1 天前
【分布式微服务云原生】探索Redis:数据结构的艺术与科学
数据结构·redis·分布式·缓存·中间件