Redis实现分布式锁

1. 什么是分布式锁

alt

分布式锁是控制分布式系统同时访问资源的一种方式,在单机或者单进程下面,多线程并发的时候,可以加一个锁比如 synchronized 或者 Reentrantlock 类来控制资源访问。在分布式系统简单加锁就不适用了。前面两个都是在一个 JVM 或者线程有效的,分布式情况下,是跨多个 JVM 的,所以这种锁就会失效。

常见的分布式锁可以用 Redis 去实现。

https://www.cnblogs.com/wangyingshuo/p/14510524.html

分布式锁的特性

互斥性:只有一个竞争者才能持有锁,这一点要尽可能保证。

对称性:同一个锁,加锁和解锁只能时同一个竞争者,不能把其他竞争者的锁给释放 了,UUID 作为 value,保证自己的锁只能自己释放。

可靠性:比如正在拿锁的服务挂掉了,要有一些容灾的处理

通过 Redis 的命令

通过 Redis 的 setnx 命令来设置一个分布式锁。

setnx  key value

alt

通过 setnx key value 如果 key 不存在那么会返回 1,如果 key 存在那么就返回 0.

执行完成逻辑之后之后再 delete 这个锁即可。

要为 delete 失败进行兜底。如果执行逻辑过程中服务挂了,那么锁一直就会存在,其他线程就只能在外面等待。

加一个过期的时间

如果 delete 的服务挂掉了,那么锁一直就得不到释放。需要加一个过期的时间,expire,但是 setnx 和 expire 并不具备原子性,如果 setnx 获取锁之后,服务挂掉了,那么也不行。

redis 提供了 set key value nx ex seconds。 nx 表示具有 setnx 的特定,ex 表示增加了过期时间,最后一个参数就是过期时间的值。

业务执行时间太长了

客户端 A 还在处理业务当中,时间太长锁过期了。 被 B 使用 del 命令删除了,如果马上有其他客户端来获取锁,就会和 A 一起共享数据了。

给这个锁的 value 设置值得时候, UUID+线程 ID。保证每一个客户端的唯一标识不一样,当删除锁的时候,需要判断检查这个 value 和客户端标识是否一样。 这个也会有问题,删除锁就变成了 读取变量、判断变量、删除变量多个操作,就要保证原子性。这个地方就需要整合 Redis 的 LUA 脚本,整合 LUA 脚本。

使用 Redis 的 LUA 脚本来执行,删除锁不是原子性的操作。

alt

锁过期的时候业务还没有完成

有的业务可能因为慢 sql 或者调用第三方执行时间太长了,导致锁已经过去了,这个时候 b 拿到锁,a 执行完成之后再去释放锁,就会出现问题。

可以使用看门狗机制来实现这个问题,每隔一段时间看一下锁,如果还持有锁,那么就给锁进行延期。

Redission

可以使用 Redission 来实现分布式锁。

alt

总结

alt

实现代码

使用原生的命令+lua 脚本实现分布式锁

import redis.clients.jedis.Jedis;

public class DistributedLock {

    private Jedis jedis;

    public DistributedLock(Jedis jedis) {
        this.jedis = jedis;
    }

    // 获取锁
    public boolean tryLock(String lockKey, String lockValue, int expireTime) {
        String luaScript = "if redis.call('exists', KEYS[1]) == 0 then " +
                           "redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX'); " +
                           "return 1; " +
                           "else return 0; end";
        Object result = jedis.eval(luaScript, 1, lockKey, lockValue, String.valueOf(expireTime));
        return "1".equals(result.toString());
    }

    // 释放锁
    public boolean releaseLock(String lockKey, String lockValue) {
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                           "redis.call('del', KEYS[1]); return 1; else return 0; end";
        Object result = jedis.eval(luaScript, 1, lockKey, lockValue);
        return "1".equals(result.toString());
    }
}

启动看门狗机制,在创建锁的时候启动一个新的线程,大概是设置过期时间的一半,然后去看一下这个锁也没有过期,没有过期就延迟时间。

// 启动看门狗机制
    public void startWatchdog(String lockValue, int expireTime) {
        new Thread(() -> {
            while (true) {
                try {
                    // 每隔 expireTime / 2 秒延长锁的过期时间
                    Thread.sleep(expireTime / 2 * 1000);
                    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                                       "redis.call('expire', KEYS[1], ARGV[2]); " +
                                       "return 1; else return 0; end";
                    jedis.eval(luaScript, 1, LOCK_KEY, lockValue, String.valueOf(expireTime));
                } catch (InterruptedException e) {
                    break;
                }
            }
        }).start();
    }

Redis 的看门狗机制通过定期延长锁的过期时间,避免了因任务执行时间较长而导致锁提前释放的问题。它是一个非常有用的技术,尤其在分布式系统中,可以帮助确保任务在完成前锁不会被误释放,防止死锁的发生。

使用 RedisSession

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.Redisson;

public class RedissonDistributedLock {

    private RedissonClient redissonClient;

    public RedissonDistributedLock() {
        // 创建 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        this.redissonClient = Redisson.create(config);
    }

    // 获取锁
    public boolean tryLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁,最多等待 10 秒,上锁后 30 秒自动释放
            return lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            return false;
        }
    }

    // 释放锁
    public void releaseLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    public static void main(String[] args) {
        RedissonDistributedLock lock = new RedissonDistributedLock();
        String lockKey = "myLock";

        if (lock.tryLock(lockKey)) {
            System.out.println("Lock acquired");
            // 执行任务
            lock.releaseLock(lockKey);
            System.out.println("Lock released");
        } else {
            System.out.println("Unable to acquire lock");
        }
    }
}

【用大白话的方式,带你彻底搞懂Redis分布式锁!】https://www.bilibili.com/video/BV1nk4y1u781?vd_source=5e7bb87290f27a331ea3de93e582c5ad

面试问题

你提到了 lua,lua 能保证原子性吗?

分布式锁实现的要点是什么?

加锁、解锁、自己的锁自己删,不能删别人的锁、防止死锁。

#Redis实现分布式锁#
牛牛的面试专栏 文章被收录于专栏

牛牛的面试专栏,希望自己在25年可以拿到一份大厂的SP Offer 你的点赞和收藏都是我持续更新的动力

全部评论

相关推荐

评论
2
13
分享

创作者周榜

更多
牛客网
牛客企业服务