【面试】考官问我如何实现分布式锁?

关键知识点:

1,安全性,加锁和释放锁必须是同一个客户端(解铃还须系铃人,只能释放自己的锁不能误删别人的锁)
2,互斥性,同一时刻只能有一个线程获得锁。
3,可重入性,当一个线程获取锁后,还可以再次获取这个锁,避免死锁发生。
4,容错性,当Redis宕机,客户端仍然可以释放锁,所以做锁的过期。
5,高性能,要做到高并发、低延迟。
6,高可用,当小部分节点挂掉后,仍然能够对外提供服务。

【Redis】分布式锁&Redisson

面试官问我:有没有参与过秒杀系统的设计?

回答:参与过秒杀系统,并独立负责过秒杀系统的架构设计。

案例:

在电商系统设计秒杀的时候,怎么防止商品超卖?比如活动中只有10台笔记本,最终卖出100台,肯定不行,平台要亏钱。 

Java自带Synchronized、ReentrantLock锁只能用在单机系统中的锁,但是秒杀系统请求量较大,一般使用分布式集群,这时候就需要用到分布式锁。

分布式锁的作用

保证数据的正确性: 比如:秒杀的时候防止商品超卖,表单重复提交,接口幂等性。

避免重复处理数据: 比如:调度任务在多台机器重复执行,缓存过期所有请求都去加载数据库。

分布式锁的特性

互斥:同一时刻只能有一个线程获得锁。
高性能:要做到高并发、低延迟。
可重入:当一个线程获取锁后,还可以再次获取这个锁,避免死锁发生。
高可用:当小部分节点挂掉后,仍然能够对外提供服务。
支持阻塞和非阻塞:Synchronized是阻塞的,ReentrantLock.tryLock()就是非阻塞的
支持公平锁和非公平锁:Synchronized是非公平锁,ReentrantLock(boolean fair)可以创建公平锁

分布式锁实现方式

关系型数据库(例如:MySQL)
分布式数据库(例如:Redis)
分布式协调服务框架(例如:zookeeper)

基于数据库实现:通常基于主键,或者唯一索引来实现分布式锁,但是性能比较差,一般不建议使用

基于Redis :可以使用setnx来加锁 ,但是需要设置锁的自动删除来防止死锁,所以要结合expire使用.为了保证setnx和expire两个命令的原子性,可以使用set命令组合。

另外释放锁在finallly中调用del删除锁,而删除锁前需要判断该锁是否是当前线程加的锁以免误删除锁,需要通过get获取锁然后进行判断,但是需要保证get判断或和del删除锁的原子性,可以使用LUA脚本实现。

基于zookeeper : 使用临时顺序节点实现,线程进来都去创建临时顺序节点,第一个节点的创建线程获取到锁,后面的节点监听自己的上一个节点的删除事件,如果第一个节点被删除,释放锁第二个节点就成为第一个节点,获取到锁。

方法一:MySQL实现分布式锁

使用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='分布式锁';

关键点:

 UNIQUE KEY `uk_resource_name` (`resource_name`)

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

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秒,这样就能保证业务逻辑处理完成之前锁不会过期。

方法二:使用Redis实现分布式锁

可以使用setnx来加锁 ,但是需要设置锁的自动删除来防止死锁,所以要结合expire使用.为了保证setnx和expire两个命令的原子性,可以使用set命令组合。

setnx命令,只会在key不存在时,将将键的key设置为value。若已经存在则不做操作。

例如:果三个服务同时抢锁,服务A抢先一步执行setnx(lock_stock,1)加上锁,那么当服务B在执行setnx(lock_stock,1)加锁的时候就会失败,服务C也一样,服务A抢到锁执行完业务逻辑后就会释放锁,可以使用del(lock_stock)删除锁,其他服务就可以执行setnx(lock_stock,1)加锁了。

最简单的获取锁方式:

// 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'))

方式三:zookeeper怎么实现分布式锁

zookeeper采用树形节点,类似Linux目录文件结构,同一目录下的节点名称不能重复。

节点有分为四种类型:

  1. 持久节点(PERSISTENT)所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。
  2. 持久顺序节点(PERSISTENT_SEQUENTIAL)这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。
  3. 临时节点(EPHEMERAL)和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。
  4. 临时顺序节点(EPHEMERAL_SEQUENTIAL)在临时几点的基础上增加了顺序,可以用来实现分布式锁

zookeeper还有个监听-通知机制,客户端可以在资源节点上创建watch事件。当节点发生变化,会通知客户端,客户端可以根据变化做相应的业务处理。

我们可以利用临时顺序节点的特性创建分布式锁,分以下三步:

  1. 在资源/resource1目录下创建临时顺序节点node
  2. 获取/resource1目录下的所有节点,如果当前节点序号最小,代表加锁成功
  3. 如果不是,就是watch监听序号最小的节点

实现逻辑很简单,我们来分析一下zookeeper实现分布式锁的优点:

  1. 由于创建的临时节点,断开连接后自动删除,所以无需设置锁超时时间,也就不用考虑不释放和锁续期
  2. 由于节点上存储的创建人信息,锁也就支持可重入
  3. 由于可以监听节点,也就实现了可阻塞

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

常生果

喜欢我,请支持我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值