如果说分布式互斥算法讲了如何协调多个进程获取权限和根据权限有序访问共享资源,即获得访问权限的进程可以访问共享资源,其他进程必须 等待拥有该权限的进程释放权限。这个权限的设置或者产生的原理就是分布式锁。

在单机多线程环境中,遇到多个线程访问同一个共享资源(在单机情况下,这种一般称作“临界资源”,分布式情况下则叫“共享资源”)的 情况。为了维护数据的一致性,需要某种机制来保证只有满足某个条件的线程才能访问 资源,不满足条件的线程只能等待,在下一轮竞争中重新满足条件时才能访问资源。

锁是实现多线程同时访问同一共享资源,保证同一时刻只有一个线程可访问共享 资源所做的一种标记。分布式锁是指分布式环境下,系统部署在多个机器中,实现多进程分布 式互斥的一种锁。为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、 Memcache、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时 刻只有一个进程可访问共享资源,确保数据的一致性。

实现分布式锁有3 种主流方法,即:  基于关系型数据库实现分布式锁; 基于缓存实现分布式锁; 基于 ZooKeeper 实现分布式锁。

 

一.基于关系型数据库

创建一张锁表,当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。 数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保 证只有一个操作可以成功,操作成功的那个线程就获得了访问共享资源的锁,可以进行操 作。 因为数据库需要落到硬盘上,频繁读 取数据库会导致 IO 开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。

主要有两个缺点:

  • 单点故障问题。一旦数据库不可用,会导致整个系统崩溃。
  • 死锁问题。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。一旦已获得锁的进程挂掉或者解锁操作失败,会导致锁记录一直存在数据库中,其他进程无法获得锁。

 

二.基于缓存

所谓基于缓存,也就是说把数据存放在计算机内存中,不需要写入磁盘,减少了 IO 读写。

Redis 通常可以使用 setnx(key, value) 函数来实现分布式锁。key 和 value 就是基于缓存的分布式锁的两个属性,其中 key 表示锁 id,value = currentTime + timeOut,表示当前时间 + 超时时间。也就是说,某个进程获得 key 这把锁后,如果在 value 的时间内未释放锁,系统就会主动释放锁。

 

setnx 函数的返回值有 0 和 1:

  • 返回 1,说明该服务器获得锁,setnx 将 key 对应的 value 设置为当前时间 + 锁的有效时间。
  • 返回 0,说明其他服务器已经获得了锁,进程不能进入临界区。该服务器可以不断尝试 setnx 操作,以获得锁。

Redis 通过队列来维持进程访问共享资源的先后顺序。Redis 锁主要基于 setnx 函数实现分布式锁,当进程通过 setnx<key,value> 函数返回 1 时,表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁,或者等到时间超时才能获得锁。

对于基于数据库实现分布式锁的方案来说,基于缓存实现的分布式锁的优势表现在以下几个方面:

  • 性能更好。数据被存放在内存,而不是磁盘,避免了频繁的 IO 操作。
  • 很多缓存可以跨集群部署,避免了单点故障问题。
  • 很多缓存服务都提供了可以用来实现分布式锁的方法,比如 Redis 的 setnx 方法等。
  • 可以直接设置超时时间来控制锁的释放,因为这些缓存服务器一般支持自动删除过期数据。

这个方案的不足是,通过超时时间来控制锁的失效时间,并不是十分靠谱,因为一个进程执行时间可能比较长,或受系统进程做内存回收等影响,导致时间超时,从而不正确地释放了锁。无法合理控制超时时间

 

三.基于ZooKeeper

ZooKeeper 基于树形数据存储结构实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题。ZooKeeper 的树形数据存储结构主要由 4 种节点构成:

  • 持久节点。这是默认的节点类型,一直存在于 ZooKeeper 中。
  • 持久顺序节点。也就是说,在创建节点时,ZooKeeper 根据节点创建的时间顺序对节点进行编号。
  • 临时节点。与持久节点不同,当客户端与 ZooKeeper 断开连接后,该进程创建的临时节点就会被删除。
  • 临时顺序节点,就是按时间顺序编号的临时节点。

ZooKeeper 基于临时顺序节点实现了分布锁。

  1. 在与该方法对应的持久节点的目录下,为每个进程创建一个临时顺序节点。
  2. 每个进程获取所有临时节点列表,对比自己的编号是否最小,若最小,则获得锁。
  3. 若本进程对应的临时节点编号不是最小的,则继续判断:
    • 若本进程为读请求,则向比自己序号小的最后一个写请求节点注册 watch 监听,当监听到该节点释放锁后,则获取锁;
    • 若本进程为写请求,则向比自己序号小的最后一个读请求节点注册 watch 监听,当监听到该节点释放锁后,获取锁。

 

 

为了确保分布式锁的可用性,应保证以下几点:

  • 互斥性,即在分布式系统环境下,分布式锁应该能保证一个资源或一个方法在同一时间只能被一个机器的一个线程或进程操作。
  • 具备锁失效机制,防止死锁。即使有一个进程在持有锁的期间因为崩溃而没有主动解锁,也能保证后续其他进程可以获得锁。
  • 可重入性,即进程未释放锁时,可以多次访问临界资源。
  • 有高可用的获取锁和释放锁的功能,且性能要好。

 

posted on 2020-05-24 22:38  Moonshoterr  阅读(170)  评论(0编辑  收藏  举报