面试题:Redis(八)

news2024/10/22 21:49:39

1. 面试题

9720ce2478b64fbe830c437b0be5adc7.png

2. 锁的特性

单机版同一个jvm虚拟机内,synchronized或者Lock接口

分布式多个不同jvm虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享

一个靠谱分布式锁所需的条件 

ea93396274d9455293fff183aa07c340.png

3. 手写分布式锁

3.1 独占性(线程安全)

执行业务代码前进行加锁操作,保证同一时刻仅有一个线程能进行业务操作 

/**
     * V1.0 原始版本
     * 不足:可能出现超卖现象,锁失效(线程安全问题)
     * 在单机环境下,可以使用synchronized或Lock来实现。
     * 但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),
     * 所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
     * 不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
     * @return
     */
    public String sale() {
        String message = "";
        String key = "lgyLock";
        lock.lock();
        try {
            String result = stringRedisTemplate.opsForValue().get("inventory");
            Integer inventory = result == null ? 0 : Integer.valueOf(result);
            if (inventory > 0) {
                System.out.println("库存数量:" + inventory);
                int num = inventory - 1;
                stringRedisTemplate.opsForValue().set("inventory", String.valueOf(num));
                message = "销售成功,剩余" + num + "件" + ",服务端口号:" + port;
            } else {
                message = "库存不足";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return message;
    }

3.2 高可用(分布式锁)

因为有多个订单模块(nginx轮询调用不同的订单模块进行业务操作),而v1.0的版本只是本地jvm锁,无法解决分布式场景,此时可引入第三方组件进行加锁释放锁判断。从而可用解决跨进程+跨服务问题、超卖问题、缓存击穿等等

/**
     * V2.0 改进版本:解决不同线程中锁失效问题
     * 不足:在上锁和解锁的过程中会运行其他业务代码,若该订单模块出现故障(宕机)则会发生死锁现象,该key值一直存在,其他线程无法获取到该锁
     * @return
     */
    public String sale() {
        String message = "";
        String key = "lgyLock";
        String uuidValue = key + UUID.randomUUID().toString().replace("-", "") + Thread.currentThread().getId();

        // 分布式锁:
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
            try {
                // 暂停20毫秒,该锁被去哦他线程获取时会将该线程休眠20毫秒后在尝试获取
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            String result = stringRedisTemplate.opsForValue().get("inventory");
            Integer inventory = result == null ? 0 : Integer.valueOf(result);
            if (inventory > 0) {
                System.out.println("库存数量:" + inventory);
                int num = inventory - 1;
                stringRedisTemplate.opsForValue().set("inventory", String.valueOf(num));
                message = "销售成功,剩余" + num + "件" + ",服务端口号:" + port;
            } else {
                message = "库存不足";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            stringRedisTemplate.delete(key);
        }
        return message;
    }

3.3 防死锁(宕机)

如部署的微服务Java程序挂了(宕机)从而导致锁一直没有释放,其他正常运行的微服务一直无法获得锁从而引起死锁问题

 /**
     * V3.0 改进版本:预防可能发生死锁现象
     * 不足: 若业务未完成锁到期了会提前释放锁,此时其他线程会进行加锁,在加锁后当前线程完成业务进行释放锁,导致误删其他线程的锁
     * @return
     */
    public String sale() {
        String message = "";
        String key = "lgyLock";
        String uuidValue = key + UUID.randomUUID().toString().replace("-", "") + Thread.currentThread().getId();

        // 分布式锁:锁设置自动过期时间
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
            try {
                // 暂停20毫秒,锁被其他线程获取后会将当前线程休眠20毫秒后在尝试获取
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            String result = stringRedisTemplate.opsForValue().get("inventory");
            Integer inventory = result == null ? 0 : Integer.valueOf(result);
            if (inventory > 0) {
                System.out.println("库存数量:" + inventory);
                int num = inventory - 1;
                stringRedisTemplate.opsForValue().set("inventory", String.valueOf(num));
                message = "销售成功,剩余" + num + "件" + ",服务端口号:" + port;
            } else {
                message = "库存不足";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            stringRedisTemplate.delete(key);
        }
        return message;
    }

3.4 不乱抢(误删锁)

如线程1业务还未完成(正在进行),提前释放了锁 (锁过期),线程2进入执行业务代码(还在执行),线程1此时完成并进行释放锁,但线程1加的锁已经释放,此时线程1去释放线程2的锁,出现乱抢问题)

0a2301c892414287b7d4c1192b7d6ad5.png

/**
     * V4.0 改进版本:解决其他线程误删锁问题
     * @return
     */
    public String sale() {
        String message = "";
        String key = "lgyLock";
        String uuidValue = key + UUID.randomUUID().toString().replace("-", "") + Thread.currentThread().getId();

        // 分布式锁:锁设置自动过期时间
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
            try {
                // 暂停20毫秒,锁被其他线程获取后会将当前线程休眠20毫秒后在尝试获取
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            String result = stringRedisTemplate.opsForValue().get("inventory");
            Integer inventory = result == null ? 0 : Integer.valueOf(result);
            if (inventory > 0) {
                System.out.println("库存数量:" + inventory);
                int num = inventory - 1;
                stringRedisTemplate.opsForValue().set("inventory", String.valueOf(num));
                message = "销售成功,剩余" + num + "件" + ",服务端口号:" + port;
            } else {
                message = "库存不足";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放锁:比较要释放的锁和当前线程的锁是否一致,若一致则释放
            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)) {
                stringRedisTemplate.delete(key);
            }
        }
        return message;
    }

3.5 原子性(Lua保证原子性)

finall的判断操作和del操作不是原子性的

0ec5247b71c646ab90ea8e7110abae72.png 593d5c2a0a5d4950953d8f2e58403483.png

42694739a674476e8b894ea22cd156b6.png

/**
     * V5.0 改进版本:保证执行命令的原子性,使用lua脚本实现
     *  不足:不能兼顾可重复性
     * @return
     */
    public String sale() {
        String message = "";
        String key = "lgyLock";
        String uuidValue = key + UUID.randomUUID().toString().replace("-", "") + Thread.currentThread().getId();

        // 分布式锁:锁设置自动过期时间
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
            try {
                // 暂停20毫秒,锁被其他线程获取后会将当前线程休眠20毫秒后在尝试获取
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            String result = stringRedisTemplate.opsForValue().get("inventory");
            Integer inventory = result == null ? 0 : Integer.valueOf(result);
            if (inventory > 0) {
                System.out.println("库存数量:" + inventory);
                int num = inventory - 1;
                stringRedisTemplate.opsForValue().set("inventory", String.valueOf(num));
                message = "销售成功,剩余" + num + "件" + ",服务端口号:" + port;
            } else {
                message = "库存不足";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放锁:比较要释放的锁和当前线程的锁是否一致,若一致则释放
            String luaScript =
                    "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
                            "return redis.call('del',KEYS[1]) " +
                            "else " +
                            "return 0 " +
                            "end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
        }
        return message;

3.6 重入性(可重入锁)

可重入锁又名递归锁

 

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

 

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了那......

所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

 3.6.1 隐式锁

synchronized关键字使用的锁默认是可重入锁

 

隐式锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。

简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

 

 

与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的

public class ReEntryLockDemo
{
    public synchronized void m1()
    {
        System.out.println("-----m1");
        m2();
    }
    public synchronized void m2()
    {
        System.out.println("-----m2");
        m3();
    }
    public synchronized void m3()
    {
        System.out.println("-----m3");
    }

    public static void main(String[] args)
    {
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();

        reEntryLockDemo.m1();
    }
}

3.6.2 重入实现机制

 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

 

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

 

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

 

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

3.6.3 显示锁

lock使用的是显示锁,加锁几次必须释放锁几次 

public class ReEntryLockDemo
{
    static Lock lock = new ReentrantLock();

    public static void main(String[] args)
    {
        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("----外层调用lock");
                lock.lock();
                try
                {
                    System.out.println("----内层调用lock");
                }finally {
                    // 这里故意注释,实现加锁次数和释放次数不一样
                    // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                    lock.unlock(); // 正常情况,加锁几次就要解锁几次
                }
            }finally {
                lock.unlock();
            }
        },"a").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("b thread----外层调用lock");
            }finally {
                lock.unlock();
            }
        },"b").start();

    }
}

3.7 自动续期

自动续期功能可确保锁的过期时间大于业务执行时间,避免业务还未执行完成锁过期的问题

4. RedLock

368c68739ba94504891918ecc7b4ccae.png

4.1 问题引入 

手写分布式锁的不足

74faadd2196f42198ab86ef4ac973c79.png 

4.2 解决方案

Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。

锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。

c54193e0608c484eb22cb9bc176db857.png

该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。

假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,

为了取到锁客户端执行以下操作:

1
获取当前时间,以毫秒为单位;
2
依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁;
3
客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
4
如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5
如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;

条件2:客户端获取锁的总耗时没有超过锁的有效时间。

ab52fff1855b46dbaf29fab889a1c7cd.png 

这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于 防止了 单节点故障造成整个服务停止运行的情况且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法。

Redisson 分布式锁支持 MultiLock 机制可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。

 

最低保证分布式锁的有效性及安全性的要求如下:

1.互斥;任何时刻只能有一个client获取锁

2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁

3.容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁

 

网上讲的基于故障转移实现的redis主从无法真正实现Redlock:

因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,这是clientB尝试获取锁,并且能够成功获取锁,导致互斥失效;

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2221086.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VScode运行C语言终端输出中文乱码问题解决方案

VScode运行C语言输出中文乱码问题解决方案 由于 VSCode 的终端是对系统的 cmd 命令行工具的调用&#xff0c;而 cmd 的默认编码为 GBK。当我们在 VSCode 中以 UTF-8 编码进行代码编写且代码里含有中文字符时&#xff0c;在终端运行代码便会出现中文乱码现象。要解决此问题&…

传统企业应该如何突破管理瓶颈?

传统企业应该如何突破管理瓶颈&#xff1f; 【导读】 作为传统企业&#xff0c;有很多传承的传统机制&#xff0c;然而在市场机制下&#xff0c;越来越能够深刻感受到外部市场变化快的特点&#xff0c;在逐步适应以市场为导向的环境下&#xff0c;传统企业自身如何做好管理工…

C/C++每日一练:实现一个环形队列

队列&#xff08;queue&#xff09; 队列是一种先进先出&#xff08;FIFO&#xff0c;First In First Out&#xff09; 的数据结构&#xff0c;类似于排队的场景。最先进入队列的元素最先被处理&#xff0c;而后加入的元素则排在队列的末尾。 常见的队列操作&#xff1a; 入队…

【linux 多进程并发】0301 Linux创建后台服务进程,daemon进程,自己的进程可以被一号进程接管啦

0301 Linux创建后台进程 ​专栏内容&#xff1a; postgresql使用入门基础手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 一、概述…

Matlab实现鲸鱼优化算法(WOA)求解路径规划问题

目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1内容介绍 鲸鱼优化算法&#xff08;WOA&#xff09;是一种受自然界座头鲸捕食行为启发的优化算法&#xff0c;它通过模拟座头鲸的环绕猎物、螺旋游动和搜索猎物三种主要行为来探索和优化问题的解。WOA因其强大的全局搜索能…

RabbitMQ最新版本4.0.2在Windows下的安装及使用

RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;提供可靠的消息传递和队列服务。它支持多种消息协议&#xff0c;包括 AMQP、STOMP、MQTT 等。本文将详细介绍如何在 Windows 系统上安装和使用最新版本的 RabbitMQ 4.0.2。 前言 RabbitMQ 是用 Erlang 语言开发的 AMQP&…

攻坚金融关键业务系统,OceanBase亮相2024金融科技大会

10月15-16日&#xff0c;第六届中新数字金融应用博览会与2024金融科技大会&#xff08;简称“金博会”&#xff09;在苏州工业园区联合举办。此次大会融合了国家级重要金融科技资源——“中国金融科技大会”&#xff0c;围绕“赋能金融高质量发展&#xff0c;金融科技创新前行”…

如何实现金蝶商品数据集成到电商系统的SKU

如何实现金蝶商品数据集成到电商SKU系统 金蝶商品数据集成到电商SKU的技术实现 在现代企业的数据管理中&#xff0c;系统间的数据对接与集成是提升业务效率和准确性的关键环节。本文将分享一个实际案例&#xff1a;如何通过轻易云数据集成平台&#xff0c;将金蝶云星辰V2中的商…

Gin框架操作指南06:POST绑定(下)

官方文档地址&#xff08;中文&#xff09;&#xff1a;https://gin-gonic.com/zh-cn/docs/ 注&#xff1a;本教程采用工作区机制&#xff0c;所以一个项目下载了Gin框架&#xff0c;其余项目就无需重复下载&#xff0c;想了解的读者可阅读第一节&#xff1a;Gin操作指南&#…

idea删除git历史提交记录

前言&#xff1a;此文章是我在实际工作中有效解决问题的方法&#xff0c;做记录的同时也供大家参考&#xff01; 一、 首先&#xff0c;通过idea的终端或系统的cmd控制台&#xff0c;进入到你的项目文件根目录&#xff0c;idea终端默认就是项目根目录。 二、确保你当前处于要删…

浙大恩特CRM Quotegask_editAction SQL注入漏洞复现

0x01 产品描述&#xff1a; 浙大恩特CRM是由浙江大学恩智浙大科技有限公司推出的客户关系管理&#xff08;CRM&#xff09;系统。该系统旨在帮助企业高效管理客户关系&#xff0c;提升销售业绩&#xff0c;促进市场营销和客户服务的优化。 0x02 漏洞描述&#xff1a; 浙大恩特…

​​Spring6梳理19——基于注解管理Bean之@Autowired注入

以上笔记来源&#xff1a; 尚硅谷Spring零基础入门到进阶&#xff0c;一套搞定spring6全套视频教程&#xff08;源码级讲解&#xff09;https://www.bilibili.com/video/BV1kR4y1b7Qc 目录 19.1 Autowired注入 ①场景一&#xff1a;属性注入 19.1.1创建UserDao接口 19.1.2…

如何将数据从 AWS S3 导入到 Elastic Cloud - 第 2 部分:Elastic Agent

作者&#xff1a;来自 Elastic Hemendra Singh Lodhi 了解将数据从 AWS S3 提取到 Elastic Cloud 的不同选项。 这是多部分博客系列的第二部分&#xff0c;探讨了将数据从 AWS S3 提取到 Elastic Cloud 的不同选项。 在本博客中&#xff0c;我们将了解如何使用 Elastic Agent…

OQE-OPTICAL AND QUANTUM ELECTRONICS

文章目录 一、征稿简介二、重要信息三、服务简述四、投稿须知五、联系咨询 一、征稿简介 二、重要信息 期刊官网&#xff1a;https://ais.cn/u/3eEJNv 三、服务简述 四、投稿须知 1.在线投稿&#xff1a;由艾思科蓝支持在线投稿&#xff0c;请将文章全文投稿至艾思科蓝投稿系…

【Linux】————动静态库

作者主页&#xff1a; 作者主页 本篇博客专栏&#xff1a;Linux 创作时间 &#xff1a;2024年10月22日 一&#xff0e;库的定义 什么是库&#xff0c;在windows平台和linux平台下都大量存在着库。 本质上来说库是一种可执行代码的二进制形式&#xff0c;可以被操作系统载…

虚拟机网络设置为桥接模式

1、打开VMware Workstation Pro&#xff0c;点击“虚拟机—设置”&#xff0c;进入虚拟机设置页面 2、点击“网络适配器”&#xff0c;网络连接选择桥接模式 3、点击“编辑—虚拟网络编辑器”&#xff0c;进入虚拟网络编辑器页面 4、选择桥接模式&#xff0c;并选择要桥接到的…

Flux.using 使用说明书

using public static <T,D> Flux<T> using(Callable<? extends D> resourceSupplier,Function<? super D,? extends Publisher<? extends T>> sourceSupplier,Consumer<? super D> resourceCleanup)Uses a resource, generated by a…

创建人物状态栏

接下来&#xff0c;我们来尝试制作一下我们的UI&#xff0c;我们会学习unity基本的UI系统 ************************************************************************************************************** 我们要先安装一个好用的插件到我们的unity当中&#xff0c;帮助…

Mac电脑:资源库Library里找不到WebServer问题的解决

今天看到一本书里写到Windows电脑自带IIS Web服务器&#xff0c;好奇了一下下&#xff0c;mac电脑自带的又是什么服务器呢&#xff1f;经查询&#xff0c;原来是Apache服务器&#xff0c;这个名字我很熟悉。只是如何设置呢&#xff1f;我从来没用过&#xff0c;于是试验了一番。…

[Linux进程概念]命令行参数|环境变量

目录 一、命令行参数 1.什么是命令行参数 2.为什么要有命令行参数 &#xff08;1&#xff09;书写的代码段 &#xff08;2&#xff09;实际的代码段 3.Linux中的命令行参数 二、环境变量 1.什么是环境变量&#xff1f; 2.获取环境变量 &#xff08;1&#xff09;指令…