0

0

Redis分布式锁怎么实现及应用场景是什么

PHPz

PHPz

发布时间:2023-05-30 17:55:51

|

1748人浏览过

|

来源于亿速云

转载

    引言

    锁是开发过程中十分常见的工具,你一定不陌生,悲观锁,乐观锁,排它锁,公平锁,非公平锁等等,很多概念,如果你对java里的锁还不了解,可以参考这一篇:不可不说的java“锁”事,这一篇写的很全面了,但是对于初学者,知道这些锁的概念,由于缺乏实际工作经验,可能并不了解锁的实际使用场景,java中可以通过volatile、synchronized、reentrantlock 三个关键字来实现线程的安全,这部分知识在第一轮基础面试里一定会问(要熟练掌握哦)。

    在分布式系统中Java这些锁技术是无法同时锁住两台机器上的代码,所以要通过分布式锁来实现,熟练使用分布式锁也是大厂开发必会的技能。

    1、面试官:

    你有遇到需要使用分布式锁的场景吗?

    问题分析:这个问题主要作为引子,先要了解什么场景下需要使用分布式锁,分布式锁要解决什么问题,在此前提下有助于你更好的理解分布式锁的实现原理。

    使用分布式锁的场景一般需要满足以下场景:

    • 系统是一个分布式系统,java的锁已经锁不住了。

    • 操作共享资源,比如库里唯一的用户数据。

    • 同步访问,即多个进程同时操作共享资源。

    答:说一个我在项目中使用分布式锁场景的例子:

    消费积分在很多系统里都有,信用卡,电商网站,通过积分换礼品等,这里“消费积分”这个操作是需要使用锁的典型场景。

    事件A:

     以积分兑换礼品为例来讲,完整的积分消费过程简单分成3步:

    A1:用户选中商品,发起兑换提交订单。

    A2:系统读取用户剩余积分:判断用户当前积分是否充足。

    A3:扣掉用户积分。

    事件B: 

    系统给用户发放积分也简单分成3步:

    B1:计算用户当天应得积分

    B2:读取用户原有积分

    B3:在原有积分上增加本次应得积分

    那么问题来了,如果用户消费积分和用户累加积分同时发生(同时用户积分进行操作)会怎样?

    假设:用户在消费积分的同时恰好离线任务在计算积分给用户发放积分(如根据用户当天的消费额),这两件事同时进行,下面的逻辑有点绕,耐心理解。

    用户U有1000积分(记录用户积分的数据可以理解为共享资源),本次兑换要消耗掉999积分。

    不加锁的情况:事件A程序在执行到第2步读积分时,A:2操作读到的结果是1000分,判断剩余积分够本次兑换,紧接着要执行第3步A:3操作扣积分(1000 - 999 = 1),正常结果应该是用户还是1分。但是这个时候事件B也在执行,本次要给用户U发放100积分,两个线程同时进行(同步访问),不加锁的情况,就会有下面这种可能,A:2 -> B:2 -> A:3 -> B:3 ,在A:3尚未完成前(扣积分,1000 - 999),用户U总积分被事件B的线程读取了,最后用户U的总积分变成了1100分,还白白兑换了一个999积分的礼物,这显然不符合预期结果。

    有人说怎么可能这么巧同时操作用户积分,cpu那么快,只要用户足够多,并发量足够大,墨菲定律迟早生效,出现上述bug只是时间问题,还有可能被黑产行业卡住这个bug疯狂薅羊毛,这个时候作为开发人员要解决这个隐患就必须了解锁的使用。

    (写代码是一项严谨的事儿!)

    Java本身提供了两种内置的锁的实现,一种是由JVM实现的synchronized 和 JDK 提供的 Lock,以及很多原子操作类都是线程安全的,当你的应用是单机或者说单进程应用时,可以使用这两种锁来实现锁。

    但是当下互联网公司的系统几乎都是分布式的,这个时候Java自带的 synchronized 或 Lock 已经无法满足分布式环境下锁的要求了,因为代码会部署在多台机器上,为了解决这个问题,分布式锁应运而生,分布式锁的特点是多进程,多个物理机器上无法共享内存,常见的解决办法是基于内存层的干涉,落地方案就是基于Redis的分布式锁 or ZooKeeper分布式锁。

    (我分析的不能更详细了,面试官再不满意?)

    2、面试官:

    Redis分布式锁实现方法

    目前有两种主要的实现方式来解决分布式锁问题:一种是基于Redis Cluster模式,另一种则是……。2.基于Zookeeper 集群模式。

    优先掌握这两种,应付面试基本没问题了。

    答:

    1、基于Redis的分布式锁

    方法一:使用setnx命令加锁

    public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
    		// 第一步:加锁
        Long result = jedis.setnx(lockKey, requestId);
        if (result == 1) {
            // 第二步:设置过期时间
            jedis.expire(lockKey, expireTime);
        }
    }

    代码解释:

    setnx命令,意思就是 set if not exist,如果lockKey不存在,把key存入Redis,保存成功后如果result返回1,表示设置成功,如果非1,表示失败,别的线程已经设置过了。

    多个微信小程序源码合集
    多个微信小程序源码合集

    微信小程序是一种轻量级的应用开发平台,由腾讯公司推出,主要应用于移动端,旨在提供便捷的用户体验,无需下载安装即可在微信内使用。本压缩包包含了丰富的源码资源,涵盖了多个领域的应用场景,下面将逐一介绍其中涉及的知识点。1. 图片展示:这部分源码可能涉及了微信小程序中的``组件的使用,用于显示图片,以及`wx.getSystemInfo`接口获取屏幕尺寸,实现图片的适配和响应式布局。可能还包括了图片懒加

    下载

    expire(),设置过期时间,防止死锁,假设,如果一个锁set后,一直不删掉,那这个锁相当于一直存在,产生死锁。

    (讲到这里,我还要和面试官强调一个“但是”)

    思考,我上面的方法哪里与缺陷?继续给面试官解释…

    加锁总共分两步,第一步jedis.setnx,第二步jedis.expire设置过期时间,setnx与expire不是一个原子操作,如果程序执行完第一步后异常了,第二步jedis.expire(lockKey, expireTime)没有得到执行,相当于这个锁没有过期时间,有产生死锁的可能。正对这个问题如何改进?

    改进:

    public class RedisLockDemo {
        private static final String SET_IF_NOT_EXIST = "NX";
        private static final String SET_WITH_EXPIRE_TIME = "PX";
        /**
         * 获取分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @param expireTime 超期时间
         * @return 是否获取成功
         */
        public static boolean getLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    				// 两步合二为一,一行代码加锁并设置 + 过期时间。
            if (1 == jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)) {
                return true;//加锁成功
            }
            return false;//加锁失败
        }
    }

    代码解释:

    将加锁和设置过期时间合二为一,一行代码搞定,原子操作。

    (没等面试官开口追问,面试官很满意了)

    3、面试官: 那解锁操作呢?

    答:

    释放锁就是删除key

    使用del命令解锁

    public static void unLock(Jedis jedis, String lockKey, String requestId) {
        // 第一步: 使用 requestId 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 第二步: 若在此时,这把锁突然不是这个客户端的,则会误解锁
            jedis.del(lockKey);
        }
    }

    代码解释: 通过 requestId 判断加锁与解锁是不是同一个客户端和 jedis.del(lockKey) 两步不是原子操作,理论上会出现在执行完第一步if判断操作后锁其实已经过期,并且被其它线程获取,这是时候在执行jedis.del(lockKey)操作,相当于把别人的锁释放了,这是不合理的。当然,这是非常极端的情况,如果unLock方法里第一步和第二步没有其它业务操作,把上面的代码扔到线上,可能也不会真的出现问题,原因第一是业务并发量不高,根本不会暴露这个缺陷,那么问题还不大。

    但是写代码是严谨的工作,能完美则必须完美。针对上述代码中的问题,提出改进。

    代码改进:

    public class RedisTool {
        private static final Long RELEASE_SUCCESS = 1L;
        /**
         * 释放分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @return 是否释放成功
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    }

    代码解释:

    使用 jedis 客户端的 eval 方法和只需一行 script 脚本,即可解决方法一中涉及的原子性问题。

    3、面试官:

    基于 ZooKeeper 的分布式锁实现原理

    答:还是积分消费与积分累加的例子:事件A和事件B同时需要进行对积分的修改操作,两台机器同时进行,正确的业务逻辑上让一台机器先执行完后另外一个机器再执行,要么事件A先执行,要么事件B先执行,这样才能保证不会出现A:2 -> B:2 -> A:3 -> B:3这种积分越花越多的情况(想到这种bug一旦上线,老板要生气了,我可能要哭了)。

    怎么办?使用 zookeeper 分布式锁。

    一个机器接收到了请求之后,先获取 zookeeper 上的一把分布式锁(zk会创建一个 znode),执行操作;然后另外一个机器也尝试去创建那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等待,等第一个机器执行完了方可拿到锁。

    使用 ZooKeeper 的顺序节点特性,假如我们在/lock/目录下创建3个节点,ZK集群会按照发起创建的顺序来创建节点,节点分为/lock/0000000001、/lock/0000000002、/lock/0000000003,最后一位数是依次递增的,节点名由zk来完成。

    ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。

    分布式锁的基本逻辑是使用ZK中节点的存在与否作为锁状态,以此实现分布式锁

    • 客户端调用create()方法创建名为“/dlm-locks/lockname/lock-”的临时顺序节点。

    • 客户端调用getChildren(“lockname”)方法来获取所有已经创建的子节点。

    • 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,就是看自己创建的序列号是否排第一,如果是第一,那么就认为这个客户端获得了锁,在它前面没有别的客户端拿到锁。

    • 如果创建的节点不是所有节点中最小的,那么就要监视比自己创建节点的序列号小的最大的节点,然后进入等待状态。在监视的子节点发生变更后,再获取子节点并判断是否获得锁。

    尽管释放锁的过程相对简单,其实就是删除创建的子节点,但仍需考虑删除节点失败等异常情况。

    额外补充

    分布式锁还可以从数据库下手解决问题

    方法一:

    利用 Mysql 的锁表,创建一张表,设置一个 UNIQUE KEY 这个 KEY 就是要锁的 KEY,所以同一个 KEY 在mysql表里只能插入一次了,这样对锁的竞争就交给了数据库,处理同一个 KEY 数据库保证了只有一个节点能插入成功,其他节点都会插入失败。

    这样 lock 和 unlock 的思路就很简单了,伪代码:

    def lock :
        exec sql: insert into locked—table (xxx) values (xxx)
        if result == true :
            return true
        else :
            return false
    def unlock :
        exec sql: delete from lockedOrder where order_id='order_id'

    方法二:

    使用流水号+时间戳做幂等操作,可以看作是一个不会释放的锁。

    相关专题

    更多
    java
    java

    Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

    837

    2023.06.15

    java正则表达式语法
    java正则表达式语法

    java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

    741

    2023.07.05

    java自学难吗
    java自学难吗

    Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

    736

    2023.07.31

    java配置jdk环境变量
    java配置jdk环境变量

    Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

    397

    2023.08.01

    java保留两位小数
    java保留两位小数

    Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

    399

    2023.08.02

    java基本数据类型
    java基本数据类型

    java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

    446

    2023.08.02

    java有什么用
    java有什么用

    java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

    430

    2023.08.02

    java在线网站
    java在线网站

    Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

    16926

    2023.08.03

    高德地图升级方法汇总
    高德地图升级方法汇总

    本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

    72

    2026.01.16

    热门下载

    更多
    网站特效
    /
    网站源码
    /
    网站素材
    /
    前端模板

    精品课程

    更多
    相关推荐
    /
    热门推荐
    /
    最新课程
    进程与SOCKET
    进程与SOCKET

    共6课时 | 0.3万人学习

    Redis+MySQL数据库面试教程
    Redis+MySQL数据库面试教程

    共72课时 | 6.4万人学习

    关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
    php中文网:公益在线php培训,帮助PHP学习者快速成长!
    关注服务号 技术交流群
    PHP中文网订阅号
    每天精选资源文章推送

    Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号