高并发系统中分布式锁解决方案

1 什么是分布式锁?

对于单机多线程,在 Java 中,我们通常使用 ReetrantLock 这类 JDK 自带的 本地锁 来控制本地多个线程对本地共享资源的访问。对于分布式系统,我们通常使用 分布式锁 来控制多个服务对共享资源的访问。

一个最基本的分布式锁需要满足:

  • 互斥 :任意一个时刻,锁只能被一个线程持有;
  • 高可用 :锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。

通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点。

2 分布式锁的应用场景

业务:10个人来抢2部iphone手机

业务实现:

3 高并发场景下秒杀超卖Bug复现

现有两种bug:

  1. 10个请求同时访问一台服务器。
  2. 10个请求同时访问两台服务器。

4 高并发场景下JVM锁现场压测实战

通过设置JVM锁,可以解决bug1,但是无法解决bug2,因为jvm锁只局限于处理该请求的代码运行环境,故两台服务器之间还是会发生并发事故。因此需要分布式锁来解决这个问题。

5 高并发场景下分布式锁思路分析

6 高并发秒杀场景下mysql分布式锁实战

  • 在数据库中创建一个抢占锁表,假如只有两个字段,id,value
  • 当多个用户抢商品时,每一个试图抢单的进程都会忘数据库表中增加一行记录,记录id就是本订单id
  • 设计抢占锁表的id主键不可重复,那么谁在数据库插入成功了,就是抢占锁成功
  • 其他因为主键约束插入失败的,视为抢占锁失败
  • 抢锁成功的,执行完业务后调用释放锁即删除哪行记录;

7 高并发场景下redis分布式锁实战

7.1 简单的redis分布式锁

不论是实现锁还是分布式锁,核心都在于互斥。在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0

释放锁的话,直接通过 DEL 命令删除对应的 key 即可。

> DEL lockKey
(integer) 1

为了误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,现在思考这样一个问题,如果在业务代码的实现逻辑中,已经加锁成功,但是在删除锁之前代码出现了问题,就会导致该锁无法被释放。这种情况该如何解决呢?答案是给锁设置一个过期时间

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
  • lockKey :加锁的锁名;
  • uniqueValue :能够唯一标示锁的随机字符串;
  • NX :只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
  • EX :过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。如果不是一个原子操作,伪代码如下所示

1.setnx  //加锁代码

2.xxx     //业务代码1

3.expire //设置过期时间

4.xxx    //业务代码2

如果在执行的2步骤的代码的时候出现了问题,则依然不会设置过期时间,从而导致锁无法释放,因此一定要保证设置指定 key 的值和过期时间是一个原子操作!!!但是即使成功设置了锁的过期时间,也会出现一些问题。如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。

你或许在想: 如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。

Redisson 中的分布式锁自带自动续期机制,它提供了一个专门用来监控锁的 Watch Dog( 看门狗),如果操作共享资源的还未完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

7.2 redis锁性能如何提升

假设现在有1000个人抢1号商品,1号商品有100个,其中一个人获得了锁,则剩余的999人都会等待锁的释放才会执行自己的业务逻辑。

7.2.1 分段锁

解决方案:将100个商品分成十组,也就是说可以同时加10个锁,每一组都会有一个请求在进行处理。效率提升10倍。分段锁的概念。

7.2.2 redis集群方案-解决redis单点故障

如果redis宕机了呢?

 

这个时候就得引入redis集群了。

但是涉及到redis集群,就会有新的问题出现,假设是主从集群,且主从数据并不是强一致性。当主节点宕机后,主节点的数据还未来得及同步到从节点,进行主从切换后,新的主节点并没有老的主节点的全部数据,这就会导致刚写入到老的主节点的锁在新的主节点并没有,其他服务来获取锁时还是会加锁成功。此时则会有2个服务都可以操作公共资源,此时的分布式锁则是不安全的。

redis的作者也想到这个问题,于是他发明了RedLock。

什么是RedLock?
要实现RedLock,需要至少5个实例(官方推荐),且每个实例都是master,不需要从库和哨兵。

实现流程

  1. 客户端先获取当前时间戳T1
  2. 客户端依次向5个master实例发起加锁命令,且每个请求都会设置超时时间(毫秒级,注意:不是锁的超时时间),如果某一个master实例由于网络等原因导致加锁失败,则立即想下一个master实例申请加锁。
  3. 当客户端加锁成功的请求大于等于3个时,且再次获取当前时间戳T2,当时间戳T2 - 时间戳T1 < 锁的过期时间。则客户端加锁成功,否则失败。
  4. 加锁成功,开始操作公共资源,进行后续业务操作。
  5. 加锁失败,向所有redis节点发送锁释放命令。

即当客户端在大多数redis实例上申请加锁成功后,且加锁总耗时小于锁过期时间,则认为加锁成功。 

 释放锁需要向全部节点发送锁释放命令。

第3步为啥要计算申请锁前后的总耗时与锁释放时间进行对比呢?

因为如果申请锁的总耗时已经超过了锁释放时间,那么可能前面申请redis的锁已经被释放掉了,保证不了大于等于3个实例都有锁存在了,锁也就没有意义了

这样的话分布式锁就真的没问题了嘛?

  1. 得5个redis实例,成本大大增加
  2. 可以通过上面的流程感受到,这个RedLock锁太重了
  3. 主从切换这种场景绝大多数的时候不会碰到,偶尔碰到的话,保证最终的兜底操作我觉得也没啥问题。
  4. 分布式系统中的NPC问题

分布式系统中的NPC问题

(可不是游戏里的NPC提问哦)

N:Network Delay,网络延迟

P:Process Pause,进程暂停(GC)

C:Clock Drift,时钟漂移

举个例子吧:

  1. 客户端 1 请求锁定节点 A、B、C、D、E
  2. 客户端 1 的拿到锁后,进入 GC(时间比较久)
  3. 所有 Redis 节点上的锁都过期了
  4. 客户端 2 获取到了 A、B、C、D、E 上的锁
  5. 客户端 1 GC 结束,认为成功获取锁
  6. 客户端 2 也认为获取到了锁,发生【冲突】

在第2步已经成功获取到锁后,由于GC时间超过锁过期时间,导致GC完成后其他客户端也能够获取到锁,此时2个客户端都会持有锁。就会有问题。

这个问题无论是redlock还是zookeeper都会有这种问题。不做业务上的兜底操作就没得解。

时钟漂移问题也只能是尽量避免吧。无法做到根本解决。

8 高并发场景下zookeeper分布式锁实现方案

8.1 什么是zookeeper(zk)?

zk是一个分布式协调服务,功能包括:配置维护、域名服务、分布式同步、组服务等。

zk的数据结构跟Unix文件系统类似。是一颗树形结构,这里不做详细介绍。

8.2 zookeeper节点介绍

zk的节点称之为znode节点,znode节点分两种类型:

  1. 临时节点(Ephemeral):当客户端与服务器断开连接后,临时znode节点就会被自动删除
  2. 持久节点(Persistent):当客户端与服务器断开连接后,持久znode节点不会被自动删除

znode节点还有一些特性:

  1. 节点有序:在一个父节点下创建子节点,zk提供了一个可选的有序性,创建子节点时会根据当前子节点数量给节点名添加序号。例:/root下创建/java,生成的节点名称则为java0001,/root/java0001。
  2. 临时节点:当会话结束或超时,自动删除节点
  3. 事件监听:当节点有创建,删除,数据修改,子节点变更的时候,zk会通知客户端的。

8.3 zookeeper分布式锁的实现

zookeeper就是通过临时节点和节点有序来实现分布式锁的。

  1. 每个获取锁的线程会在zk的某一个目录下创建一个临时有序的节点。
  2. 节点创建成功后,判断当前线程创建的节点的序号是否是最小的。
  3. 如果序号是最小的,那么获取锁成功。
  4. 如果序号不是最小的,则对他序号的前一个节点添加事件监听。如果前一个节点被删了(锁被释放了),那么就会唤醒当前节点,则成功获取到锁。

zookeepe和redisr两者的优缺
zookeeper
优点:

  1. 不用设置过期时间
  2. 事件监听机制,加锁失败后,可以等待锁释放

缺点:

  1. 性能不如redis
  2. 当网络不稳定时,可能会有多个节点同时获取锁问题。例:node1由于网络波动,导致zk将其删除,刚好node2获取到锁,那么此时node1和node2两者都会获取到锁。

Redis
优点:性能上比较好,天然的支持高并发

缺点:

  1. 获取锁失败后,得轮询的去获取锁
  2. 大多数情况下redis无法保证数据强一致性
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值