分布式锁和我们平常讲到的锁原理基本一样, 目的就是确保在多个线程并发时, 只有一个线程在同一刻操作这个业务或者说方法, 变量.
在一个进程中, 也就是一个 JVM 或者说应用中, 我们很容易去处理控制, 在 jdk java.util 并发包中已经为我们提供了这些方法去加锁, 比如 Synchronized 关键字或者 Lock 锁, 都可以处理.
但是我们现在的应用程序如果只部署一台服务器, 那并发量是很差的, 如果同时有上万的请求, 很有可能造成服务器压力过大而瘫痪.
想想双十一和大年三十晚上十点, 瓜分支付宝红包等业务场景, 自然需要用到多台服务器去同时处理这些业务, 这些服务可能会有上百台同时处理.
但是我们想一想, 如果有 100 台服务器要处理分红包的业务, 现在假设有 1 亿的红包, 1 千万个人分, 金额随机, 那么这个业务场景下, 是不是必须确保这 1 千万个人最后分的红包金额总和等于 1 亿?
如果处理不好~~ 每人分到 100 万, 那马云爸爸估计大年初一, 就得宣布破产了~~
常规锁会造成什么情况?
首先说一下我们为什么要搞集群. 简单理解就是, 需求量 (请求并发量) 变大了, 一个工人处理能力有限, 那就多招一些工人来一起处理.
假设 1 千万个请求平均分配到 100 台服务器上, 每个服务器接收 10w 的请求.
这 10w 个请求并不是在同一秒中来的, 可能是在 1-2 个小时内, 可以联想下我们三十晚上开红包, 等到 10:20 开始, 有的人立马开了, 有的人等到 12 点才想起来.
那这样的话, 平均到每一秒上的请求也就不到 1 千个, 这种压力一般的服务器还是可以承受的:
第一个用户来分, 请求到来后, 需要在 1 亿里面给他分一部分钱, 金额随机, 假设第一个人分到了 100, 那就要在这 1 亿中减去 100 块, 剩下 99999900 块.
第二个用户再来分, 金额随机, 这次分 200 块, 那就需要在剩下的 99999900 块中再减去 200 块, 剩下 99999700 块.
等到第 10w 个用户来, 一看还有 1000w, 那这 1000w 全成他的了.
等于是在每个服务器中去分 1 亿, 也就是 10w 个用户分了 1 亿, 最后总计有 100 个服务器, 要分 100 亿.
如果真这样了, 虽说马云爸爸不会破产(据最新统计马云有 2300 亿人民币), 那分红包的开发项目组, 以及产品经理, 可以 GG 了~
简化结构图如下:
分布式锁怎么去处理?
那么为了解决这个问题, 让 1000 万用户只分 1 亿, 而不是 100 亿, 这个时候分布式锁就派上用处了.
分布式锁可以把整个集群就当作是一个应用一样去处理, 那么也就需要这个锁独立于每一个服务之外, 而不是在服务里面.
假设第一个服务器接收到用户 1 的请求后, 不能只在自己的应用中去判断还有多少钱可以分了, 而需要去外部请求专门负责管理这 1 亿红包的人(服务), 问他: 哎, 我这里要分 100 块, 给我 100.
管理红包的妹子 (服务) 一看, 还有 1 个亿, 那好, 给你 100 块, 然后剩下 99999900 块.
第二个请求到来后, 被服务器 2 获取, 继续去询问, 管理红包的妹子, 我这边要分 10 块, 管理红包的妹子先查了下还有 99999900, 那就说: 好, 给你 10 块, 那就剩下 99999890 块.
等到第 1000w 个请求到来后, 服务器 100 拿到请求, 继续去询问, 管理红包的妹子, 我要 100, 妹子翻了翻白眼, 对你说, 就剩 1 块了, 爱要不要, 那这个时候就只能给你 1 块了(1 块也是钱啊, 买根辣条还是可以的).
这些请求编号 1,2 不代表执行的先后顺序, 正式的场景下, 应该是 100 台服务器每个服务器持有一个请求去访问负责管理红包的妹子(服务).
那在管红包的妹子那里同时会接收到 100 个请求, 这个时候就需要在负责红包的妹子那里加个锁就可以了(抛绣球), 你们 100 个服务器谁拿到锁(抢到绣球), 谁就进来和我谈, 我给你分, 其他人就等着去吧.
经过上面的分布式锁的处理后, 马云爸爸终于放心了, 决定给红包团队每人加一个鸡腿.
简化的结构图如下:
分布式锁的实现有哪些?
说到分布式锁的实现, 还是有很多的, 有数据库方式的, 有 Redis 分布式锁, 有 Zookeeper 分布式锁等等.
我们如果采用 Redis 作为分布式锁, 那么上图中负责 "红包的妹子(服务)", 就可以替换成 Redis, 请自行脑补.
1为什么 Redis 可以实现分布式锁?
首先 Redis 是单线程的, 这里的单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性), 即一个线程处理所有网络请求, 其他模块仍用了多个线程.
在实际的操作中过程大致是这样子的: 服务器 1 要去访问发红包的妹子, 也就是 Redis, 那么它会在 Redis 中通过 "setnx key value" 操作设置一个 Key 进去, Value 是啥不重要, 重要的是要有一个 Key, 也就是一个标记.
而且这个 Key 你爱叫啥叫啥, 只要所有的服务器设置的 Key 相同就可以.
假设我们设置一个, 如下图:
那么我们可以看到会返回一个 1, 那就代表了成功.
如果再来一个请求去设置同样的 Key, 如下图:
这个时候会返回 0, 那就代表失败了.
那么我们就可以通过这个操作去判断是不是当前可以拿到锁, 或者说可以去访问 "负责发红包的妹子", 如果返回 1, 那我就开始去执行后面的逻辑, 如果返回 0, 那就说明已经被人占用了, 我就要继续等待.
当服务器 1 拿到锁之后, 进行了业务处理, 完成后, 还需要释放锁, 如下图所示:
删除成功返回 1, 那么其他的服务器就可以继续重复上面的步骤去设置这个 Key, 以达到获取锁的目的.
当然以上的操作是在 Redis 客户端直接进行的, 通过程序调用的话, 肯定就不能这么写, 比如 Java 就需要通过 Jedis 去调用, 但是整个处理逻辑基本都是一样的.
通过上面的方式, 我们好像是解决了分布式锁的问题, 但是想想还有没有什么问题呢?
对, 问题还是有的, 可能会有死锁的问题发生, 比如服务器 1 设置完之后, 获取了锁之后, 忽然发生了宕机.
那后续的删除 Key 操作就没法执行, 这个 Key 会一直在 Redis 中存在, 其他服务器每次去检查, 都会返回 0, 他们都会认为有人在使用锁, 我需要等.
为了解决这个死锁的问题, 我们就需要给 Key 设置有效期了. 设置的方式有 2 种:
第一种就是在 Set 完 Key 之后, 直接设置 Key 的有效期 "expire key timeout" , 为 Key 设置一个超时时间, 单位为 Second, 超过这个时间锁会自动释放, 避免死锁.
这种方式相当于, 把锁持有的有效期, 交给了 Redis 去控制. 如果时间到了, 你还没有给我删除 Key, 那 Redis 就直接给你删了, 其他服务器就可以继续去 Setnx 获取锁.
第二种方式, 就是把删除 Key 权利交给其他的服务器, 那这个时候就需要用到 Value 值了, 比如服务器 1, 设置了 Value 也就是 Timeout 为当前时间 +1 秒 .
这个时候服务器 2 通过 Get 发现时间已经超过系统当前时间了, 那就说明服务器 1 没有释放锁, 服务器 1 可能出问题了, 服务器 2 就开始执行删除 Key 操作, 并且继续执行 Setnx 操作.
但是这块有一个问题, 也就是不光你服务器 2 可能会发现服务器 1 超时了, 服务器 3 也可能会发现, 如果刚好服务器 2 Setnx 操作完成, 服务器 3 就接着删除, 是不是服务器 3 也可以 Setnx 成功了?
那就等于是服务器 2 和服务器 3 都拿到锁了, 那就问题大了. 这个时候怎么办呢?
这个时候需要用到 "GETSET key value" 命令了. 这个命令的意思就是获取当前 Key 的值, 并且设置新的值.
假设服务器 2 发现 Key 过期了, 开始调用 getset 命令, 然后用获取的时间判断是否过期, 如果获取的时间仍然是过期的, 那就说明拿到锁了.
如果没有, 则说明在服务 2 执行 getset 之前, 服务器 3 可能也发现锁过期了, 并且在服务器 2 之前执行了 getset 操作, 重新设置了过期时间.
那么服务器 2 就需要放弃后续的操作, 继续等待服务器 3 释放锁或者去监测 Key 的有效期是否过期.
这块其实有一个小问题是, 服务器 3 已经修改了有效期, 拿到锁之后, 服务器 2 也修改了有效期, 但是没能拿到锁.
但是这个有效期的时间已经被在服务器 3 的基础上又增加一些, 但是这种影响其实还是很小的, 几乎可以忽略不计.
2为什么 Zookeeper 可实现分布式锁?
百度百科是这么介绍的: ZooKeeper 是一个分布式的, 开放源码的分布式应用程序协调服务, 是 Google 的 Chubby 一个开源的实现, 是 Hadoop 和 Hbase 的重要组件.
那对于我们初次认识的人, 可以理解成 ZooKeeper 就像是我们的电脑文件系统, 我们可以在 d 盘中创建文件夹 a, 并且可以继续在文件夹 a 中创建文件夹 a1,a2.
那我们的文件系统有什么特点? 那就是同一个目录下文件名称不能重复, 同样 ZooKeeper 也是这样的.
在 ZooKeeper 所有的节点, 也就是文件夹称作 Znode, 而且这个 Znode 节点是可以存储数据的.
我们可以通过 "create /zkjjj nice" 来创建一个节点, 这个命令就表示, 在根目录下创建一个 zkjjj 的节点, 值是 nice.
同样这里的值, 和我在前面说的 Redis 中的一样, 没什么意义, 你随便给.
另外 ZooKeeper 可以创建 4 种类型的节点, 分别是:
持久性节点
持久性顺序节点
临时性节点
临时性顺序节点
首先说下持久性节点和临时性节点的区别:
持久性节点表示只要你创建了这个节点, 那不管你 ZooKeeper 的客户端是否断开连接, ZooKeeper 的服务端都会记录这个节点.
临时性节点刚好相反, 一旦你 ZooKeeper 客户端断开了连接, 那 ZooKeeper 服务端就不再保存这个节点.
顺便也说下顺序性节点, 顺序性节点是指, 在创建节点的时候, ZooKeeper 会自动给节点编号比如 0000001,0000002 这种的.
Zookeeper 有一个监听机制, 客户端注册监听它关心的目录节点, 当目录节点发生变化 (数据改变, 被删除, 子目录节点增加删除) 等, Zookeeper 会通知客户端.
在 Zookeeper 中如何加锁?
下面我们继续结合我们上面的分红包场景, 描述下在 Zookeeper 中如何加锁.
假设服务器 1, 创建了一个节点 /zkjjj, 成功了, 那服务器 1 就获取了锁, 服务器 2 再去创建相同的锁, 就会失败, 这个时候就只能监听这个节点的变化.
等到服务器 1 处理完业务, 删除了节点后, 他就会得到通知, 然后去创建同样的节点, 获取锁处理业务, 再删除节点, 后续的 100 台服务器与之类似.
注意这里的 100 台服务器并不是挨个去执行上面的创建节点的操作, 而是并发的, 当服务器 1 创建成功, 那么剩下的 99 个就都会注册监听这个节点, 等通知, 以此类推.
但是大家有没有注意到, 这里还是有问题的, 还是会有死锁的情况存在, 对不对?
当服务器 1 创建了节点后挂了, 没能删除, 那其他 99 台服务器就会一直等通知, 那就完蛋了.
这个时候就需要用到临时性节点了, 我们前面说过了, 临时性节点的特点是客户端一旦断开, 就会丢失.
也就是当服务器 1 创建了节点后, 如果挂了, 那这个节点会自动被删除, 这样后续的其他服务器, 就可以继续去创建节点, 获取锁了.
但是我们可能还需要注意到一点, 就是惊群效应: 举一个很简单的例子, 当你往一群鸽子中间扔一块食物, 虽然最终只有一个鸽子抢到食物, 但所有鸽子都会被惊动来争夺, 没有抢到...
就是当服务器 1 节点有变化, 会通知其余的 99 个服务器, 但是最终只有 1 个服务器会创建成功, 这样 98 还是需要等待监听, 那么为了处理这种情况, 就需要用到临时顺序性节点.
大致意思就是, 之前是所有 99 个服务器都监听一个节点, 现在就是每一个服务器监听自己前面的一个节点.
假设 100 个服务器同时发来请求, 这个时候会在 /zkjjj 节点下创建 100 个临时顺序性节点 /zkjjj/000000001,/zkjjj/000000002, 一直到 /zkjjj/000000100, 这个编号就等于是已经给他们设置了获取锁的先后顺序了.
当 001 节点处理完毕, 删除节点后, 002 收到通知, 去获取锁, 开始执行, 执行完毕, 删除节点, 通知 003~ 以此类推.
来源: http://www.tuicool.com/articles/vQFRjaA