0%

分布式锁

现实生产环境中为什么需要分布式锁?

在现在的公司的生产环境中,一个项目的部署最起码需要两台服务器,你不可以在你产品上线部署的时候,不让用户使用你们公司的产品,而且分布式部署,通过Nginx的负载均衡,也可以减轻单台服务器的压力,带来性能和效率上的提升。那么问题来了,在分布式部署产品的时候,就会产生一个问题,如何保证数据的一致性。当某个资源在多个系统之间,存在着共享的时候,为了保证大家访问这个资源数据是一致性的,就必须保证统一时刻只能有只有一个客户端出来,不能并发执行。单机部署,我们需要考虑并发问题,我们往往采用线程间加锁的机制。但是在分布式系统时代,为了实现我们之前的问题,就必须考虑分布式锁了。

分布式锁指的是在分布式部署环境下,通过锁机制来让多客户端互斥的对共享资源镜像访问。

目前主流的分布式锁的技术:

  • 基于数据库实现
  • 基于Redis实现(本篇主要讲的内容)
  • 基于Memcached实现
  • 基于 ZooKeeper实现

基于数据库实现

基于数据库实现分布式锁,主要是依靠数据库的乐观锁或者悲观锁来实现。这里可以看下我之前的博客—MySQL数据库锁机制

基于乐观锁

在数据库表里面引入一个版本号(version),通过这个字段来控制数据是否更新。

基于悲观锁

悲观锁也叫排他锁,在 MySQL 中主要是通过 for update 来实现加锁。

基于Redis实现

使用Redis来实现分布式锁,主要依靠Redis自身的原子操作。通过Redis在缓存中占个“坑位”,告诉后来者,这个“坑”已经被我占用了,你要使用,请稍后。

在之前的版本中,使用 setnx key value,如果这个key在Redis中不存在,这个命令会往Redis中插入一条数据,返回数据 1,如果存在就会返回 0。在使用后,使用Redis的del删除这条数据。
不过存在问题的是如果在两个命令之间出现异常,那么就不会删除数据,一种方法是使用try{}catch{}finally{},删除的命令放到finally里面。另外一种是加一个过期时间,不过加过期时间的话,就有存在一个问题,过期时间大小的控制,如果时间小,这个现场还没结束,这条数据就过期了怎么办?

在Redis 2.6.12 版本后,字符串命令中加入了一些参数,set key value [EX seconds] [PX milliseconds] [NX|XX],这才是它所提供全部的命令。不过从这个命令中可以看出,在后来版本的Redis中,基本上 set 命令取代了 setnx

  • EX/PX: EX指的是秒数;PX指的是毫秒
  • NX/XX:NX表示当key不存在是,创建保存;XX表示当key存在是,保存value

Redis分布式锁解决方案

注意点:

  • set命令要用set key value px milliseconds nx;
  • value要具有唯一性;
  • 释放锁时要验证value值,不能误解锁;

这类锁在也是比较容易实现的,但是这种分布式锁最大的缺点就是加锁是只能作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某种原因发生了主从切换,那么就会出现锁丢失的情况:

  • 在Redis的master节点拿到锁
  • 但是这个锁的key还没有同步到slave节点
  • master故障,发生故障转移,slave节点升级为master节点
  • 导致锁丢失

RedLock

Redis 作者基于上面的问题,提出了 Redlock算法

在算法的分布式版本中,我们假设我们有N个Redis主机。这些节点完全独立,因此我们不使用复制或任何其他隐式协调系统。我们已经描述了如何在单个实例中安全地获取和释放锁。我们理所当然地认为算法将使用此方法在单个实例中获取和释放锁。在我们的示例中,我们设置N = 5,这是一个合理的值,因此我们需要在不同的计算机或虚拟机上运行5个Redis主服务器,以确保它们以大多数独立的方式失败。

为了获取锁,客户端执行以下操作:

  • 以毫秒为单位获取当前时间。

  • 它尝试按顺序获取所有N个实例中的锁,在所有实例中使用相同的键名和随机值。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

  • 客户端通过从当前时间中减去开始获取锁的时间戳(步骤1中记录的时间)来计算获取锁定使用所需的时间。当且仅当客户端能够在大多数实例中获取锁定时(N/2+1,这里至少3个)并且获取锁定所经过的总时间小于锁定有效时间,认为锁定被获取。

  • 如果获得了锁,key的有效时间被认为是初始有效时间减去经过的时间,如步骤3中计算的。

  • 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

Redlock算法的实现

redisson 已经对Redlock算法封装了。在 POM 中引入 redisson。

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.9.1</version>
</dependency>

具体关于 Redission 实现和源码解析

基于Memcached实现

这种方式和基于Redis类似,利用 Memcached 的 add 命令。此命令是原子性操作,只有在key不存在的情况下,才能add成功,也就意味着线程得到了锁。

基于ZooKeeper

这部分主要是看网上的资料了解的。
利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper设计的初衷,就是为了实现分布式锁服务的。

原理就是:当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。

客官,赏一杯coffee嘛~~~~