分布式锁的实现

1.前言

大多数互联网系统是分布式部署的,分布式部署解决了高并发高可用的问题,但是由此带来了数据一致性问题。

当某个资源在多系统之间,被共享操作的时候,为了保证这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端操作,不能并发的执行,否者就会出现同一时刻有客户端写,别的客户端在读,两者访问到的数据就不一致了。

2.我们为什么需要分布式锁

在单机时代,虽然不需要分布式锁,但也面临过类似的问题,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。

但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,应用程序会有多份,并且部署在不同的机器上,这些资源已经不是在同一进程的不同线程间共享,而是属于多进程之间共享的资源。

因此,为了解决这个问题,我们就必须引入「分布式锁」。

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

分布式锁要满足哪些要求呢?

排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法获取

避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)

高可用:获取或释放锁的机制必须高可用且性能佳

而且最好是可重入锁。

3.分布式锁的实现方式有哪些

目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:

基于数据库实现

基于Redis实现

基于ZooKeeper实现

无论哪种方式,其实都不完美,依旧要根据咱们业务的实际场景来选择。

4.基于数据库实现

基于数据库来做分布式锁的话,通常有两种做法:

基于数据库的乐观锁

基于数据库的悲观锁

我们先来看一下如何基于「乐观锁」来实现:

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。

当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

「悲观锁」的实现:

悲观锁利用数据库的行锁来进行锁定指定行,

通车用"SELECT * FROM TABLE_NAME WHERE id=id_value FOR UPDATE" 来获取数据。

如果能获取到数据,则加锁成功, 如果获取失败,说明锁已经被别的程序占用了,自己则获取锁失败。

5.基于Zookeeper来实现

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

当释放锁的时候,只需将这个临时节点删除即可。

Zookeeper本身很容易部署成集群模式,因此通过集群部署Zookeeper可以解决分布式锁的单点故障问题。

另外Apache Curator项目是一个集成好了的Zookeeper Client,里面就包含有分布式锁的实现,直接拿来用也行。

GitHub源码:

原生Zookeeper实现分布式锁:

https://github.com/liushaoming/distarch/tree/master/lock/distarch-lock-zookeeper

Curator实现分布式锁:

https://github.com/liushaoming/distarch/tree/master/lock/distarch-lock-curator

6.基于Redis实现

基于Redis实现的锁机制,主要是依赖redis自身的原子操作,例如:

setnx user_key user_value px millisecond

上述代码示例是指

当redis中不存在user_key这个键的时候,才会去设置一个user_key键,并且给这个键的值设置为 user_value,且这个键的存活时间为100ms

为什么这个命令可以帮我们实现锁机制呢?

因为这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。

当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。

解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。

另外,针对redis集群模式的分布式锁,可以采用redis的Redlock机制。

Redis实现分布式的弊端:

当客户端因为异常断开了跟Redis的链接的时候,怎么自动释放锁呢?这里通过redis set expire lock_timeout来。 当超时时间到了后,自动key过期了,就自动删掉了。锁也就被释放了。

lock_timeout的存在也使得失去了锁的意义,即存在并发的现象。一旦出现锁的租约时间,就意味着获取到锁的客户端必须在租约之内执行完毕业务逻辑,一旦业务逻辑执行时间过长,租约到期,就会引发并发问题。所以有lock timeout的可靠性并不是那么的高。

7.总结

总的来说,用zookeeper来实现分布式锁是性能高,无单点故障,可靠性最高的方式,也是博主极力推荐的方式.

GitHub源码:

原生Zookeeper实现分布式锁:

https://github.com/liushaoming/distarch/tree/master/lock/distarch-lock-zookeeper

Curator实现分布式锁:

https://github.com/liushaoming/distarch/tree/master/lock/distarch-lock-curator

分布式系统架构父项目

https://github.com/liushaoming/distarch

精品Github项目列表

https://github.com/liushaoming?tab=repositories

如果对你有帮助,欢迎在github上star。

文章旁边,顺手点个“好看”呗!^_^

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190214G0TE0U00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券