关于java:面试官竟然问我怎么实现分布式锁幸亏我总结了全套八股文

26次阅读

共计 3995 个字符,预计需要花费 10 分钟才能阅读完成。

一个挺着啤酒肚,身穿格子衫,发际线重大后移的中年男子,手拿着保温杯,胳膊夹着 MacBook 向你走来,看样子是架构师级别。

面试开始, 直入正题。

面试官: 你有没有参加过秒杀零碎的设计?

我: 没有,我平时都是开发后盾管理系统、OA 办公零碎、外部管理系统,素来没有开发过秒杀零碎。

面试官: 嗯 …,小伙子很实诚。明天就先到这里吧,前面有音讯会被动分割你。

前面还可能有音讯吗?你们啥时候被动分割过我?
实话实说的被拒,八股文背的溜反而被录取。
好吧,等我看看一灯怎么总结的秒杀零碎的八股文。

我: 参加过秒杀零碎,并独立负责过秒杀零碎的架构设计(【狗头】是的,都是我设计的)。

面试官: 这样才对,这样我能力接着往下问。你在设计秒杀零碎的时候,怎么避免商品超卖?比方流动中只有一台 iPhone,最终卖出 100 台,必定不行,平台要亏钱。

我: 必定要加锁,不过因为秒杀零碎申请量较大,个别应用分布式集群。而 Java 自带 Synchronized、ReentrantLock 锁只能用在单机零碎中,这时候就须要用到分布式锁。

面试官: 你提到分布式锁,分布式锁都有哪些作用?

八股文这就开始了。

我:我感觉分布式锁次要有两个作用:

保证数据的正确性:
比方:秒杀的时候避免商品超卖,表单反复提交,接口幂等性。

防止数据反复解决:
比方:调度工作在多台机器反复执行,缓存过期所有申请都去加载数据库。

总结八股文,还得是一灯。

面试官: 小伙子总结的挺全,你晓得设计一个分布式锁,要具备哪些个性?

我: 我感觉分布式锁要具备以下这些个性:

互斥:同一时刻只能有一个线程取得锁。
可重入:当一个线程获取锁后,还能够再次获取这个锁,防止死锁产生。
高可用:当小局部节点挂掉后,依然可能对外提供服务。
高性能:要做到高并发、低提早。
反对阻塞和非阻塞:Synchronized 是阻塞的,ReentrantLock.tryLock()就是非阻塞的
反对偏心锁和非偏心锁:Synchronized 是非偏心锁,ReentrantLock(boolean fair)能够创立偏心锁

面试官: 小伙子,有点货色。你是怎么设计一个分布式锁?

我: 有几种罕用的工具都能够实现分布式锁。
比方:关系型数据库(例如:MySQL)、分布式数据库(例如:Redis)、分布式协调服务框架(例如:zookeeper)

应用 MySQL 实现分布式锁比较简单,建一张表:

CREATE TABLE `distributed_lock` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
  `resource_name` varchar(200) NOT NULL DEFAULT ''COMMENT' 资源名称(惟一索引)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_resource_name` (`resource_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁';

获取锁的时候,就插入一条记录。插入胜利就代表获取到锁,插入失败就代表获取锁失败。

INSERT INTO distributed_lock (`resource_name`) VALUES ('资源 1');

开释锁的时候,就删除这条记录。

DELETE FROM distributed_lock WHERE resource_name = '资源 1';

实现比较简单,不过还不能用于理论生产中,有几个问题没有解决:

  1. 这把锁不反对阻塞,insert 失败立刻就返回了。当然能够用 while 循环直到插入胜利,不过自旋也会占用 CPU。
  2. 这把锁不是可重入的,曾经获取到锁的线程再次插入也会失败,咱们能够减少两列,一列记录获取到锁的节点和线程,另一列记录加锁次数。获取锁,次数加一,开释锁,次数减一,次数为零就删除这把锁。
  3. 这把锁没有过期工夫,如果业务解决失败或者机器宕机,导致没有开释锁,锁就会始终存在,其余线程也无奈获取到锁。咱们能够减少一列锁过期工夫,再启动一个异步工作扫描过期工夫大于以后工夫的锁就删除。

就是这么麻烦,咱们看一下优化之后的锁变成什么样了:

CREATE TABLE `distributed_lock` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
  `resource_name` varchar(200) NOT NULL DEFAULT ''COMMENT' 资源名称(惟一索引)',
  `owner` varchar(200) NOT NULL DEFAULT ''COMMENT' 锁持有者(机器码 + 线程名称)',
  `lock_count` int NOT NULL DEFAULT '0' COMMENT '加锁次数',
  `expire_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '锁过期工夫',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_resource_name` (`resource_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁';

这下应该完满了吧?不行,还有个问题:

业务逻辑没解决完,锁过期了怎么办?

如果咱们设置锁过期工夫是 6 秒,失常状况下业务逻辑能够在 6 秒内解决实现,然而当 JVM 产生 FullGC 或者调用第三方服务呈现网络提早,业务逻辑还没解决完,锁曾经过期,被删掉,而后被其余线程获取到锁,岂不是要出问题?

这就引入了另一个知识点“锁续期”:

获取锁的同时,启动一个异步工作,每当业务执行到三分之一工夫,也就是 6 秒中的第 2 秒的时候,就主动缩短锁过期工夫,持续缩短到 6 秒,这样就能保障业务逻辑解决实现之前锁不会过期。

面试官: 小伙子,分布式锁算是让你玩明确了。我还想持续问,生产中个别很少用 MySQL 做分布式锁,因为 MySQL 并发性能跟不上。方才提到 Redis 也能够实现分布式锁,你晓得该怎么实现吗?

我当然晓得,八股文就要背全套。

我: 应用 Redis 实现分布式锁,跟应用 MySQL 相似,也须要解决实现过程中遇到的各种问题,不过解决方案稍有不同。

最简略的获取锁形式:

// 1. 获取锁
redis.setnx('resource_name1', 'owner1')
// 2. 开释锁
redis.del('resource_name1')

当“resource_name1”不存在时,set 胜利,也就是获取锁胜利。

不过还须要加上过期工夫,避免没有开释锁。

// 1. 获取锁
redis.setnx('resource_name1', 'owner1')
// 2. 减少锁过期工夫
redis.exprire('resource_name1', 6, TimeUnit.SECONDS)

又引入新问题了,两条命令不是原子的,可能获取锁之后还没来得及设置过期工夫就宕机了,这该怎么办?

好办,在 Redis 2.6.12 之后,提供一条复合命令:

redis.set('resource_name1', 'owner1',"NX" "EX", 6)

还有一个问题,开释锁的时候,并没有判断锁的持有者,有可能把其余线程持有的锁给开释了,这可不行,能够这样做:

// 开释锁
if ('owner1'.equals(redis.get('resource_name1'))){redis.del('resource_name1')
}

这样行不行呢?还不行,因为 get 和 del 两条命令不是原子操作,须要引入 Lua 脚本把两条命令打包成一条发给 Redis 执行:

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redis.eval(script, Collections.singletonList('resource_name1'), Collections.singletonList('owner1'))

这样总行了吧?还不行,还有个“锁续期”的问题没有解决。

更简略了,Redis 客户端 Redisson 曾经帮咱们实现续期的性能,叫“WatchDog”(看门狗),在咱们调用 lock 主动唤醒“看门狗”。

面试官: 小伙子,你可真行啊。你再讲一下应用 zookeeper 怎么实现分布式锁?

我: zookeeper 采纳树形节点,相似 Linux 目录文件构造,同一目录下的节点名称不能反复。

节点有分为四种类型:

长久节点: 一旦创立,永恒存储在服务器上,除非手动删除。
长期节点: 生命周期与客户端绑定,客户端断开连接,节点就被主动删除。
长久程序节点: 个性同长久节点,只是在节点名称前面追加自增有序数字。
长期程序节点: 个性同长期节点,只是在节点名称前面追加自增有序数字。

zookeeper 还有个监听 - 告诉机制,客户端能够在资源节点上创立 watch 事件。当节点发生变化,会告诉客户端,客户端能够依据变动做相应的业务解决。

咱们能够利用 长期程序节点 的个性创立分布式锁,分以下三步:

  1. 在资源 /resource1 目录下创立长期程序节点 node
  2. 获取 /resource1 目录下的所有节点,如果以后节点序号最小,代表加锁胜利
  3. 如果不是,就是 watch 监听序号最小的节点

实现逻辑很简略,咱们来剖析一下 zookeeper 实现分布式锁的长处:

  1. 因为创立的长期节点,断开连接后主动删除,所以无需设置锁超时工夫,也就不必思考不开释和锁续期
  2. 因为节点上存储的创建人信息,锁也就反对可重入
  3. 因为能够监听节点,也就实现了可阻塞

面试官: 小伙子,降级加薪的机会就是留给你这样的人。薪资 double,今天就来下班吧。

总结:

对于分布式锁的所有知识点,尽管很多,但都曾经总结在这张图上了,欢送点赞珍藏转发评论。

正文完
 0