浅谈分布式锁

分布式锁是一个在分布式环境中重要的原语,它表明不同进程间采用互斥的方式操作共享资源 。本文将谈谈分布式相关的内容 。
1
分布式锁概述
从进程锁到分布式锁
在单进程环境中,为了防止多线程同时对共享资源进行读写操作,我们通常使用内核或者类库实现线程间的互斥 。当扩展到分布式系统下,我们需要提供相同功能的分布式锁服务,不同的主机通过该服务获取一把锁,而获取该锁的机器就可以排他性的访问共享资源 。

浅谈分布式锁


在单机环境下,操作系统知道自己进程的状态,当进程挂掉的时候该进程会释放自己持有的锁资源,但在分布式环境下,存在宕机、网络分区、时延等各种异常状态,因此需要给分布式锁提供新的特性:可用性 。
分布式锁的系统分类
基于锁资源的安全性,可以将分布式锁分为以下两部分:
  1. 基于异步复制的分布式系统,例如:redis、mysql
  2. 基于paxos协议的分布式一致性系统,例如:etcd、zookeeper
基于异步复制的分布式系统,存在丢锁的风险,不够安全,通常使用TTL机制承担细粒度的锁服务,该系统接入简单,适用于对事件很敏感,期望设置一个较短的有效时间,执行短期任务,丢锁对业务影响相对可控的服务 。
基于paxos协议的分布式系统,通过一致性协议保证数据的多副本,数据的安全性高,通常使用lease机制承担粗粒度的锁服务,适用于对安全性很敏感,希望长期持有锁,不希望发生丢锁现象的服务 。
2
基于 redis 的分布式锁
通常可以使用setnx(set if not exists)实现排他的获取锁操作,但是由于分布式系统中进程可能随时宕机,因此获取锁时,需要添加TTL保证锁不会因为进程挂掉之后变成死锁状态,但是此时又出现了第二个问题,那就是setnx和expire操作必须是原子操作,因为setnx之后,如果进程挂掉,expire有可能没有机会执行,这同样会导致死锁 。正确的操作如下:
【浅谈分布式锁】SET lock_name value NX EX lock_time
  1. EX second:设置 key 的过期时间,单位是秒
  2. NX:当 key 不存在的时候进行设置,等同于 SETNX 操作
释放锁时,只需要调用DEL命令删除锁即可:
DEL lock_name需要注意的是,这里有可能出现错误删除锁的问题 。场景如下:
  1. 进程A加锁成功,锁超时时间20秒 。由于进程A业务逻辑执行过长,20秒之后,锁过期自动释放 。
  2. 此时进程B接着加锁,加锁成功后,执行业务逻辑 。这期间,进程A结束执行,使用DEL释放锁 。
这样就导致进程A错误的释放进程B的锁 。因此,为了不被错误的释放锁,我们在加锁的时候需要设置UUID 。例如:
SET lock_name uuid NX EX lock_time释放锁时,需要先获取锁的UUID,如果是自己分配的,则释放锁 。但是这里的比较和释放操作必须是原子操作,否则会出现获获取锁比对的时候正确,此时正好TTL过期,另一个进程加锁成功,这样的话还是会出现错误释放锁的操作 。可以使用Lua脚本实现判断与删除的原子操作 。
3
基于 etcd/zookeeper 的分布式锁
排他锁(exclusive locks)
排他锁(写锁、独占锁)表现如下:
进程p1对数据d1加上排他锁,那么在整个加锁期间,只允许进程p1对数据d1进行读取和更新操作,其他任何进程都不能再对整个数据进行任何类型的操作 。直到p1释放排他锁 。
通过在zk/etcd上的数据节点来表示一个锁

推荐阅读