作者:微信小助手
发布时间:2019-04-20T07:35:04
▍阅读索引
0. 名词定义
1. 问题引入
2. 分布式环境的特点
3. 锁
4. 分布式锁
5. 分布式锁实现方案
5.1. 朴素Redis实现方案、朴素Redis方案小结
5.2. ZooKeeper实现方案、ZooKeeper方案小结
5.3. Redisson实现方案、Redisson方案小结
6. 总结
7. 结束语
8. Reference
▍0. 名词定义
分布式锁:顾名思义,是指在分布式环境下的锁,重点在锁。所以我们先从锁开始讲起。
▍1. 问题引入
举个例子:
某服务记录数据X,当前值为100。A请求需要将X增加200;同时,B请求需要将X减100。在理想的情况下,A先读取到X=100,然后X增加200,最后写入X=300。B请求接着读取到X=300,减少100,最后写入X=200。然而在真实情况下,如果不做任何处理,则可能会出现:A和B同时读取到X=100;A写入之前B读取到X;B比A先写入等等情况。
上面这个例子相信大家都非常熟悉。出现不符合预期的结果本质上是对临界资源没有做好互斥操作。互斥性问题通俗来讲,就是对共享资源的抢占问题。对于共享资源争抢的正确性,锁是最常用的方式,其他的如CAS(compare and swap)等,这里不展开。
▍2. 分布式环境的特点
我们的绝大部分服务都处于分布式环境中。那么,分布式系统有哪些特点呢?大致如下:
* 可扩展性:可通过横向水平扩展提高系统的性能和吞吐量。
* 高可靠性:高容错,即使系统中一台或几台故障,系统仍可提供服务。
* 高并发性:各机器并行独立处理和计算。
* 廉价高效:多台小型机而非单台高性能机。
▍3.锁
我们先来看下非分布式情况下的锁方案(多线程和多进程的情况),然后再演进到分布式锁。
▍多线程下的锁机制:
各种语言有不同的实现方式,比较成熟。比如,go语言中的sync.RWMutex(读写锁)、sync.Mutex(互斥锁);JAVA中的ReentrantLock、synchronized;在php中没有找到原生的支持锁的方式,只能通过外部来间接实现,比如文件锁,借助外部存储的锁等。
▍多进程下的锁机制:
对于临界资源的访问已经超出了单个进程的控制范围。在多进程的情况下,主要是利用操作系统层面的进程间通信原理来解决临界资源的抢占问题。比较常见的一种方法便是使用信号量(Semaphores)。
▍对信号量的操作,主要是P操作(wait)和V操作(signal):
* P操作 ( wait ) :
先检查信号量的大小,若值大于零,则将信号量减1,同时进程获得共享资源的访问权限,继续执行;若小于或者等于零,则该进程被阻塞后,进入等待队列。
* V操作 ( signal ) :
该操作将信号量的值加1,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒。
可看出,多进程锁方案跟多线程的锁方案实现思路大同小异。
我们将互斥的级别拉高,分布式环境下不同节点不同进程或线程之间的互斥,就是分布式锁的挑战之一。后面再细讲。
另外,在传统的基于数据库的架构中,对于数据的抢占问题也可以通过数据库事务(ACID)来保证。在分布式环境中,出于对性能以及一致性敏感度的要求,使得分布式锁成为了一种比较常见而高效的解决方案。
▍从上面对于多线程和多进程锁的概括,可以总结出锁的抽象条件:
1)“需要有存储锁的空间,并且锁的空间是可以访问到的”:
对于多线程就是内存(进程中不同的线程都可以读写),多进程中通过共享内存的方式,也是提供一块地方,供不同进程读写。主要目的是保证不同的进线程改动对于其他进线程可见,进而满足互斥性需求。
2)“锁需要被唯一标识”:
不同的共享资源,必然需要用不同的锁进行保护,因此相应的锁必须有唯一的标识。在多线程环境中,锁可以是一个对象,那么对这个对象的引用便是这个唯一标识。多进程下,比如有名信号量,便是用硬盘中的文件名作为唯一标识。
3)“锁要有至少两种状态”:
有锁,没锁。存在,不存在等等。很好理解。
满足上述三个条件就可以实现基础的分布式锁了。但是随着技术的演进,