分布式面试分布式锁实现及应用场景

目录
  • 引言
  • 1、面试官:
    • 你有遇到需要使用分布式锁的场景吗?
      • 事件A:
      • 事件B:
  • 2、面试官:
    • Redis分布式锁实现方法
    • 1、基于Redis的分布式锁
  • 3、面试官: 那解锁操作呢?
    • 使用del命令解锁
  • 3、面试官:
    • 基于ZooKeeper的分布式锁实现原理
  • 额外补充
    • 方法一:
    • 方法二:
  • 总结

引言

锁是开发过程中十分常见的工具,你一定不陌生,悲观锁,乐观锁,排它锁,公平锁,非公平锁等等,很多概念,如果你对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分布式锁实现方法

问题分析:目前分布式锁的实现方式主要有两种,1.基于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,表示失败,别的线程已经设置过了。

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'

方法二:

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

总结

针对分布式锁的两种实现方法,使用哪种需要取决于业务场景,如果系统接口的读写操作完全是基于内存操作的,那显然使用Redis更合适,Mysql表锁or行锁明显不合适。同样是基于内存的 Redis锁 和 ZK锁具体选用哪一种,要根据是否有具体环境和架构师对哪种技术更为了解,原则就是选你最了解的,目的是能解决问题。

以上就是分布式面试分布式锁实现及应用场景的详细内容,更多关于分布式锁实现及应用场景的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详细解读分布式锁原理及三种实现方式

    目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CAP理论告诉我们"任何一个分布式系统都无法同时满足一致性(Consistency).可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项."所以,很多系统在设计之初就要对这三者做出取舍.在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证"最终一致性",只要这个最终

  • Java分布式锁的概念与实现方式详解

    什么是分布式锁?在回答这个问题之前,我们先回答一下什么是锁. 普通的锁,即在单机多线程环境下,当多个线程需要访问同一个变量或代码片段时,被访问的变量或代码片段叫做临界区域,我们需要控制线程一个一个的顺序执行,否则会出现并发问题. 如何控制呢?就是设置一个各个线程都能看的见的标志.然后,每个线程想访问临界区域时,都要先查看标志,如果标志没有被占用,则说明目前没有线程在访问临界区域.如果标志被占用了,则说明目前有线程正在访问临界区域,则当前线程需要等待. 这个标志,就是锁. 在单机多线程的java程

  • 浅谈Java分布式架构下如何实现分布式锁

    01分布式锁运用场景 互联网秒杀,抢优惠卷,接口幂等性校验.咱们以互联网秒杀为例. @RestController @Slf4j publicclassIndexController{ @Autowired privateRedissonredission; @Autowired privateStringRedisTemplatestringRedisTemplate; @RequestMapping("/deduct_stock") publicStringdeductStock(

  • 详解如何在springcloud分布式系统中实现分布式锁

    目录 一.简介 二.redis命令介绍 三.实现思路 四.编码实现 五.注意点 六.参考资料 最近在看分布式锁的资料,看了 Josial L的<Redis in Action>的分布式锁的章节.实现思路是利用springcloud结合redis实现分布式锁. 注意:这篇文章有问题,请看这一篇https://www.jb51.net/article/228819.htm 一.简介 一般来说,对数据进行加锁时,程序先通过acquire获取锁来对数据进行排他访问,然后对数据进行一些列的操作,最后需要

  • 分布式锁三种实现方式及对比

    分布式锁三种实现方式: 1. 基于数据库实现分布式锁: 2. 基于缓存(Redis等)实现分布式锁: 3. 基于Zookeeper实现分布式锁: 一, 基于数据库实现分布式锁 1. 悲观锁 利用select - where - for update 排他锁 注意: 其他附加功能与实现一基本一致,这里需要注意的是"where name=lock ",name字段必须要走索引,否则会锁表.有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题. 2. 乐观锁 所谓乐观锁与前边

  • 分布式面试分布式锁实现及应用场景

    目录 引言 1.面试官: 你有遇到需要使用分布式锁的场景吗? 事件A: 事件B: 2.面试官: Redis分布式锁实现方法 1.基于Redis的分布式锁 3.面试官: 那解锁操作呢? 使用del命令解锁 3.面试官: 基于ZooKeeper的分布式锁实现原理 额外补充 方法一: 方法二: 总结 引言 锁是开发过程中十分常见的工具,你一定不陌生,悲观锁,乐观锁,排它锁,公平锁,非公平锁等等,很多概念,如果你对java里的锁还不了解,可以参考这一篇:不可不说的Java“锁”事,这一篇写的很全面了,但

  • java分布式面试接口如何保证幂等及概念理解

    目录 引言 1.幂等的概念 问题分析: 事后问题分析: 关于这个接口的幂等设计 深入分析: 2.工作中常见的幂等设计场景 3.幂等接口常见设计方案 总结 引言 稳定性设计第一篇:这一小节开始讲设计系统稳定性保证的相关设计,谁都不想自己负责的系统三天两头就出故障,也不想周六日跟女票葡萄美酒夜光杯的时候一个电话call去VPN办公,那么你就想办法让你的系统尽量稳定,我们的目标是让系统“无人值守”. 阿里新零售和阿里妈妈,美团,过去我面试这些公司都被问过接口幂等相关问题,接口幂等设计在分布式系统开发中

  • java分布式面试降级组件Hystrix的功能特性

    目录 引言 1.面试官:能简单介绍下Hystrix有哪些功能吗? 1.1.fail-fast(快速失败) 1.2.Fallback优雅降级机制 1.3.线程/信号量隔离机制 线程隔离: 信号量隔离: 2.面试官:刚刚说到线程隔离,那实际使用中是否打开超时线程中断开关? 3.面试官:那你是如何估计线程池大小的? 深入分析 Hystrix历史 Hystrix的主要功能特性 HystrixDemo 哪些情况下会触发fallback? 附录:Hystrix策略配置 Sentinel与Hystrixres

  • java分布式面试系统限流最佳实践

    目录 引言 1.面试官: 哪些场景系统使用了限流?为什么要使用限流? 2.面试官: 那你了解哪些常用限流算法? 1.计数器方法: 2.漏斗算法: 3.令牌桶算法: 3.面试官: 那具体这值该如何评估,说到现在我还是不知道限流到底要怎么设置,可以给我一点经验方法吗? 深入分析 使用线程池实现: 借助Guava实现: 总结 引言 前面讲了系统中的降级熔断设计和对 Hystrix 组件的功能了解,关于限流降级还有一个比较重要的知识点就是限流算法. 如果你面试的是电商相关公司,这一块就显得更加重要了,秒

  • java分布式面试CAP分别代表含义分析

    目录 引言 1.面试官,说到CAP定理,那能详细说说CAP分别代表什么吗? 2.面试官:听起来很简单,这只是概念,但是具体是什么意思呢? 举例深入分析 总结 引言 上一节讲面试中被问到分布式系统概念相关的,讲完了分布式系统的概念,优点缺点和 RPC 后,我以为这个问题就到此结束了,没想到成功给自己挖了个坑(微笑脸),关于 CAP,以前只是听说过,并没有详细点整理过,这一次问好好整理了下. CAP 问题已经成了计算机科学中一个研究领域,之前说到分布式系统有哪些优势时讲到三个提升: 1. 系统可用性

  • 分布式面试消息队列解决消息重复保证消息顺序

    目录 引言 1.面试官: 那你有考虑过消息重复问题怎么解决吗? 2.面试官: 在多集群消息架构中,如果消费端要求接收到的消息是有序的,怎么解决消息顺序消费问题? 3.面试官: 那如何做到topic不分区,能举例说明一下吗? 总结 引言 我在<项目中为什么要使用消息队列>中列举了两个使用消息队列的例子. (1)收银系统,确认收款成功,通过MQ通知给物流系统发货. (2)消费积分,用户每消费一笔给用户增加一定积分,京东豆,信用卡积分,2020年如果还没倒闭的电商平台中,可以100%的确定订单系统和

  • Redis分布式非公平锁的使用

    目录 前言 redis分布式锁第一版 redis分布式锁第二版 redis分布式锁第三版 redis分布式锁最终版 前言 看了很多博客,和资料,这里只针对redis做分布式锁做一下深入探讨,希望对你们有帮助.网上提供了很多分布式锁的操作,这里逐一举例然后评论优缺点及改进方案,希望这样子能让当家更好的理解redis分布式锁. redis分布式锁第一版 大家应该都知道Redis做分布式锁无非就是INCR命令或者是SetNx命令,这里我们采用setnx命令. 操作:setnx key 如果操作成功则代

  • 深入了解MySQL锁机制及应用场景

    目录 锁的概述 锁的分类 锁的应用场景 数据库事务管理 多线程程序开发 数据库的备份和恢复 对于在线游戏等高并发应用场景 锁的具体使用方法 锁的应用实例 总结 锁的概述 MySQL锁是操作MySQL数据库时常用的一种机制.MySQL锁可以保证多个用户在同时执行读写操作时,能够互相协同.避免数据出现不一致或者读写冲突等问题.本篇文章将详细介绍MySQL锁的基本知识和具体应用.MySQL锁是多用户数据库系统中的一种典型的并发控制机制,可让多个同时操作完成相应的操作.当多个用户同时访问同一系列表时,很

  • Netty分布式编码器及写数据事件处理使用场景

    目录 概述 编码器 第一节: writeAndFlush的事件传播 我们看一个最简单的使用的场景 我们跟到writeAndFlush方法中 我们跟到invokeWriteAndFlush中 我们再看invokeFlush0方法 概述 上一小章我们介绍了解码器, 这一章我们介绍编码器 其实编码器和解码器比较类似, 编码器也是一个handler, 并且属于outbounfHandle, 就是将准备发出去的数据进行拦截, 拦截之后进行相应的处理之后再次进发送处理, 如果理解了解码器, 那么编码器的相关

  • redisson实现分布式锁原理

    Redisson分布式锁 之前的基于注解的锁有一种锁是基本redis的分布式锁,锁的实现我是基于redisson组件提供的RLock,这篇来看看redisson是如何实现锁的. 不同版本实现锁的机制并不相同 引用的redisson最近发布的版本3.2.3,不同的版本可能实现锁的机制并不相同,早期版本好像是采用简单的setnx,getset等常规命令来配置完成,而后期由于redis支持了脚本Lua变更了实现原理. <dependency> <groupId>org.redisson&

随机推荐