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 你的点赞和收藏都是我持续更新的动力

全部评论

相关推荐

09-03 11:14
门头沟学院 Java
滴滴一面: 1.自我介绍2.蕾丝图像的AIGC使用了大模型还是什么?3.扩散模型和大模型的方案有什么区别吗,为了解决什么问题4.linkedlist、hashmap、concurrentHashmap三者的底层实现原理5.concurrentHashmap对于hashmap的升级是为了解决什么问题6.linkedlist主要用在什么样的问题场景(没答上来)7.一个完整的类加载流程(没答上来),加载了一个类首先放在哪个区(还是没答上来)(方法区|元空间存放虚拟机加载的类信息、常量、静态变量等数据),于是问每个区放什么东西8.触发一次gc的时候底层发生了什么才会触发gc9.mysql的innodb的索引结构10.B+树是用什么机制来控制树的高度(没答好)(B+树主要通过 ​​多路平衡搜索、节点分裂与合并​​ 等机制来控制树的高度,使其在面对大规模数据时仍能保持低矮和平衡,从而确保高效的数据操作性能。)11.数据库的锁的类型12. select * where a>3 and a<10 这么一个语句会触发什么锁 (答看a是不是主键or索引,分情况)13.间隙锁怎么理解14. mysql的事务怎么理解(没答全)15.redis缓冲你主要用到了他的什么能力16.说说你的多级缓存是怎么维护的(redis+caffeine)17.更新数据的时候采用先更新数据库再删除缓冲会有什么问题,延迟双删会带来什么问题18.mq有了解底层的基本实现吗(底层源码看,只是使用和了解概念),主要包括哪几部分?一个生产端配置多个消费端,通过什么来维护这个消费关系19.RPC这块的架构有了解吗(不熟),然后就没问了,开始写算法。20.算法:类似N皇后,给一个NxN的棋盘,给k个棋子,要求同一行和列上不能有两个及以上的棋子,且棋盘上可能会有空白区域,空白区域这个位置就不能放棋子。要求出有多少种放法。(没写出来,最后只答了个思路,走远了,寄)
查看20道真题和解析
点赞 评论 收藏
分享
08-29 20:59
已编辑
门头沟学院 Java
1.自我介绍2.项目拷打穿插八股:a.你在哪些功能中引入了新的组件?b.ES相比于MySQL好在哪?c.什么场景下用MySQL查找,什么场景下用ES?d.大数据量用ES就一定更好吗?e.你知道在分布式部署下ES可能出现哪些问题吗?f.讲讲RocketMQ在你的项目中的使用逻辑?g.你认为在你的理解中RocketMQ最重要的特性是什么?(顺序性,不丢失,不重复,幂等性,可用性)h.那你知道RocketMQ是怎么实现这些特性的吗?i.Kafka是怎么实现集群高可用,在Leader宕机情况下不会有消息丢失的?j.你平时是怎么学习技术的?k.你这边建立了二级缓存,那请问你是怎么保证二级缓存中数据一致性的?l.那你采用事务可能会带来一些什么问题?m.除了Caffeine还了解哪些本地缓存结构吗?n.知道Caffeine的内部结构吗?3.来讲讲Java中的Error是怎么出现的?会带来什么后果?4.你提到了OOM,请问什么情况下会出现OOM呢?5.那异常呢?Java中有哪些异常类型?6.你认为编译时异常和运行时异常该怎么去处理?什么时候要注意处理这些异常?7.来讲讲锁,对Synchronized了解吗?8.Synchronized是可重入锁吗?是公平锁吗?9.还了解哪些锁呢?10.假如你自己设计了一个并发包,我们可以丢弃Synchronized只用ReentrantLock吗?手撕:实现一个单链表首尾交叉相连,要求必须在原链表上操作例子:1-2-3-4-5-6-7输出:1-7-2-6-3-5-4反问1.改进建议?(对于大二来说挺不错的)[听怕了这句话,逢说必挂]2.几轮?(2-3轮)希望七夕我可以和offer长长久久,牛友也和另一半长长久久🥰
查看23道真题和解析
点赞 评论 收藏
分享
评论
2
13
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务