【分布式锁】我们为什么需要分布式锁?

一、背景

大多数互联网系统都是分布式部署的,分布式部署确实能带来性能和效率上的提升,但为此,我们就需要多解决一个分布式环境下,数据一致性的问题。
当某个资源在多系统之间,具有共享性的时候,为了保证大家访问这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端处理,不能并发的执行,否者就会出现同一时刻有人写有人读,大家访问到的数据就不一致了。

二、 我们为什么需要分布式锁?

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

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

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

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

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

  • 排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取
  • 避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)
  • 高可用:获取或释放锁的机制必须高可用且性能佳

三、分布式锁的实现方式有哪些?

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

  • 基于数据库实现
  • 基于Redis实现
  • 基于ZooKeeper实现 无论哪种方式

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

1 基于数据库实现

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

  • 基于数据库的乐观锁
  • 基于数据库的悲观锁

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

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

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

如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

image.png

如图,假设同一个账户,用户A和用户B都要去进行取款操作,账户的原始余额是2000,用户A要去取1500,用户B要去取1000,如果没有锁机制的话,在并发的情况下,可能会出现余额同时被扣1500和1000,导致最终余额的不正确甚至是负数。但如果这里用到乐观锁机制,当两个用户去数据库中读取余额的时候,除了读取到2000余额以外,还读取了当前的版本号version=1,等用户A或用户B去修改数据库余额的时候,无论谁先操作,都会将版本号加1,即version=2,那么另外一个用户去更新的时候就发现版本号不对,已经变成2了,不是当初读出来时候的1,那么本次更新失败,就得重新去读取最新的数据库余额。

通过上面这个例子可以看出来,使用「乐观锁」机制,必须得满足:

  1. 锁服务要有递增的版本号version
  2. 每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

2.基于Redis实现

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

SET user_key user_value NX PX 100

redis从2.6.12版本开始,SET命令才支持这些参数: NX:只在在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value PX millisecond:设置键的过期时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效

上述代码示例是指, 当redis中不存在user_key这个键的时候,才会去设置一个user_key键,并且给这个键的值设置为 user_value,且这个键的存活时间为100ms

为什么这个命令可以帮我们实现锁机制呢?
因为这个命令是只有在某个key不存在的时候,才会执行成功。
那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。
当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。

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

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

3. .基于ZooKeeper实现

其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。

当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。

如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。

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

image.png

如图,locker是一个持久节点,node_1/node_2/…/node_n 就是上面说的临时节点,由客户端client去创建的。 client_1/client_2/…/clien_n 都是想去获取锁的客户端。

以client_1为例,它想去获取分布式锁,则需要跑到locker下面去创建临时节点(假如是node_1)创建完毕后,看一下自己的节点序号是否是locker下面最小的,如果是,则获取了锁。

如果不是,则去找到比自己小的那个节点(假如是node_2),找到后,就监听node_2,直到node_2被删除,那么就开始再次判断自己的node_1是不是序列中最小的,如果是,则获取锁,如果还不是,则继续找一下一个节点。

image.png

3. .基于etcd实现

etcd v3 的 lock 则是利用 lease (ttl)、 Revision (版本)和Watch prefix 来实现的。

  1. 往自定义的 etcd 目录写一个 key, 并配置 key 的 lease ttl 超时
  2. 然后获取该目录下的所有 key,判断当前最小 revision 的key 是否是由自身创建的,如果是则拿到锁.
  3. 拿不到,则监听 watch 比自身 revison 小一点的 key
  4. 当监听的 key 发生事件时,则再次判断当前 key revison 是否最最小,,重新走第二个步骤
image.png

k8s 的kube-scheduler 和 kube-manager-controller 的 leader election 依赖于etcd,抢锁选主的逻辑是周期轮询实现的, 相比社区中标准的分布式锁来说,不仅增加了由于无效轮询带来的性能开销,也不能解决公平性,谁抢到了锁谁就是主 leader。
这种leader election机制 虽然有这些缺点, 但由于 k8s 里需要高可用组件就那么几个,调度器和控制器组件开多个副本加起来也没多少个实例,,开销可以不用担心。
这些组件都是跟无状态的 apiserver 对接的,apiserver 作为无状态的服务可以横向扩展,后端的 etcd 对付这些默认 2s 的锁请求也什么问题。另外, 选主的逻辑不在乎公平性,谁先谁后无所谓, 总结起来这类选举的场景, 轮询没啥问题, 实现起来也简单。

kafka基于zookeeper选controller,是悲观锁还是乐观锁?

kafka基于zookeeper选controller使用的是乐观锁。在zookeeper中,每个节点都有一个版本号(version),当多个客户端同时对一个节点进行修改时,只有版本号最大的客户端能够成功修改节点的值。
因此,kafka使用zookeeper的版本号机制来实现乐观锁,确保只有一个broker成为controller。

在Kafka中,确实是通过在Zookeeper上创建/controller节点来选举Controller节点。但是,选举过程中使用的是乐观锁机制。
当某个Broker节点想要成为Controller节点时,它会尝试在Zookeeper上创建一个/controller节点,并将自己的Broker ID写入该节点中。
如果创建成功,则该Broker节点成为了Controller节点;否则,它会读取该节点的内容,获取当前的Controller Broker ID,然后在Zookeeper上更新/controller节点的内容,将其中的Broker ID更新为自己的Broker ID,同时增加版本号。
如果更新成功,则该Broker节点成为了Controller节点;否则,它会重新尝试更新/controller节点,直到更新成功或超过最大尝试次数。
这种机制可以避免多个Broker节点同时创建/controller节点,从而确保只有一个Broker节点成为Controller节点。
同时,使用乐观锁机制也可以减少对Zookeeper的负载,因为只有在更新/controller节点时才需要向Zookeeper发起写请求。

当Kafka集群中的Controller节点挂掉后,Kafka需要选举一个新的Controller节点来代替它,以确保集群的正常运行。
Kafka选举新的Controller节点的过程如下:

  1. 每个Broker节点都会监听Zookeeper中/controller节点的变化,当发现Controller节点挂掉后,它会尝试参与选举。

  2. 每个Broker节点会读取Zookeeper中/brokers/ids节点的内容,获取当前集群中所有的Broker ID。

  3. 每个Broker节点会将这些Broker ID排序,并选择最小的Broker ID作为新的Controller节点。

  4. 每个Broker节点都会尝试在Zookeeper上更新/controller节点的内容,将其中的Broker ID更新为自己选择的新Controller节点。如果更新成功,则该Broker节点成为了新的Controller节点;否则,它会重新尝试更新/controller节点,直到更新成功或超过最大尝试次数。

需要注意的是,当Controller节点挂掉后,Kafka集群中可能会出现多个Broker节点同时尝试成为新的Controller节点的情况。
在这种情况下,只有最小的Broker ID的节点才会成功成为新的Controller节点。

悲观锁和乐观锁的判断标准是什么?

悲观锁和乐观锁的判断标准主要是对并发操作的处理方式不同。

悲观锁:认为在并发情况下,数据很可能会被其他线程修改,因此在每次操作数据时都会先加锁,以保证数据的一致性。悲观锁的判断标准是在进行数据操作前先加锁,如果加锁失败则认为数据被其他线程占用,需要等待其他线程释放锁。

乐观锁:认为在并发情况下,数据不太可能被其他线程修改,因此在每次操作数据时都不会加锁,而是在更新数据时检查数据版本号,如果版本号一致则更新成功,否则认为数据已被其他线程修改,更新失败。乐观锁的判断标准是在进行数据操作时先检查数据版本号,如果版本号一致则更新数据,否则认为数据已被其他线程占用,需要进行相应的处理。

基于redis的分布式锁,是悲观锁还是乐观锁?

Redis分布式锁可以使用乐观锁和悲观锁两种机制实现。

基于Redis的分布式锁一般使用的是乐观锁机制。

乐观锁是一种乐观思想的锁,它认为并发冲突的概率很小,所以在操作时不会对共享资源加锁,而是在更新时判断资源是否被其他线程修改过。
在基于Redis的分布式锁中,可以使用Redis的SETNX命令来实现乐观锁。

具体实现方式如下:

  1. 在Redis中创建一个键值对,键为锁的名称,值为锁的持有者标识(如客户端ID)。

  2. 使用SETNX命令尝试设置该键值对,如果设置成功,则说明该锁未被占用,当前客户端获得了锁。

  3. 如果SETNX命令设置失败,说明该锁已被其他客户端占用,当前客户端无法获得锁。可以等待一段时间后再次尝试获取锁,或者直接返回获取锁失败的结果。

  4. 当客户端释放锁时,需要使用DEL命令将该键值对从Redis中删除,以释放锁。

需要注意的是,乐观锁机制的缺点是可能会出现ABA问题。
当某个线程读取共享资源时,共享资源的值为A,然后另一个线程将共享资源的值修改为B,再将其修改回A,此时第一个线程再次读取共享资源时,仍然认为它没有被修改过。
基于Redis的分布式锁可以通过加入版本号等机制来解决ABA问题。

使用悲观锁机制时,通过在Redis中使用SET命令来设置锁,并使用EX选项设置锁的过期时间,以避免死锁。
在获得锁之后,可以使用GET命令来获取锁的值,并在释放锁时使用DEL命令将锁从Redis中删除。

不同的实现方式适用于不同的场景。
乐观锁机制适用于并发量较小的场景,可以避免对Redis的频繁访问,从而提高性能;
而悲观锁机制适用于并发量较大的场景,可以避免多个客户端同时获得锁,从而保证数据的一致性。

基于 etcd的 k8s组件 kube-scheduler 和 kube-manager-controller 的 leader election ,使用的是乐观锁还是悲观锁?

基于 etcd 的 k8s 组件 kube-scheduler 和 kube-manager-controller 的 leader election 使用的是乐观锁。
在 etcd 中,每个节点都有一个版本号,当需要修改一个节点的值时,会先获取该节点的版本号,然后将新值与版本号一起提交给 etcd,如果版本号与 etcd 中当前版本号一致,则修改成功,否则会返回错误。
这种方式就是乐观锁。

悲观锁的应用场景举例

悲观锁通常用于多线程环境下的共享资源访问控制,例如数据库中的行锁、表锁等。

下面以数据库行锁为例:

假设有两个线程 A 和 B 同时对数据库中的某行进行修改,如果不加锁,可能会导致数据不一致的问题。这时可以采用悲观锁的方式,即当线程 A 读取该行数据时,就对该行加锁,直到线程 A 完成修改后才释放锁,期间其他线程无法修改该行数据。线程 B 在读取该行数据时,发现已被加锁,就会等待线程 A 完成修改并释放锁后才能继续执行。

悲观锁的优点在于确保了数据的一致性,缺点在于需要频繁加锁、释放锁,会影响并发性能。因此,在高并发环境下,一般会采用乐观锁等更轻量级的锁机制来提高并发性能。

还有一些其他的悲观锁的应用举例:

  1. 文件锁:在多个进程同时访问同一个文件时,可以使用悲观锁来控制文件的访问,避免出现数据不一致的问题。

  2. 网络编程中的锁:在多个线程同时访问网络资源时,可以使用悲观锁来控制资源的访问,避免出现数据不一致的问题。

  3. 操作系统中的锁:在操作系统内核中,也可以使用悲观锁来控制共享资源的访问,例如内核中的进程锁、文件系统锁等。

总之,悲观锁适用于需要保证数据一致性的场景,但会影响并发性能,需要根据具体情况选择合适的锁机制。

四、参考

带你玩转分布式锁
https://burningmyself.gitee.io/micro/fbs-lock

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容