分布式锁的一点理解

如下内容为目前本身理解的总结,若有错误请你们指正。java

什么是锁

  • 在单进程的系统中,当存在多个线程能够同时改变某个变量(可变共享变量)时,就须要对变量或代码块作同步,使其在修改这种变量时可以线性执行消除并发修改变量。node

  • 而同步的本质是经过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么须要在某个地方作个标记,这个标记必须每一个线程都能看到,当标记不存在时能够设置该标记,其他后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记能够理解为锁。linux

  • 不一样地方实现锁的方式也不同,只要能知足全部线程都能看获得标记便可。如java中synchronize是在对象头设置标记,Lock接口的实现类基本上都只是某一个volitile修饰的int型变量其保证灭个线程都能拥有对该int的可见性和原子修改,linux内核中也是利用互斥量或信号量等内存数据作标记。
  • 除了利用内存数据作锁其实任何互斥的都能作锁(只考虑互斥状况),如流水表中流水号与时间结合作幂等校验能够看做是一个不会释放的锁,或者使用某个文件是否存在做为锁等。只须要知足在对标记进行修改能保证原子性和内存可见性便可。redis


分布式

分布式状况

此处主要指集群模式下,多个相同服务同时开启.数据库

  • 分布式与单机状况下最大的不一样在于其不是多线程而是多进程。
  • 多线程因为能够共享堆内存,所以能够简单的采起内存做为标记存储位置。而进程之间甚至可能都不在同一台物理机上,所以须要将标记存储在一个全部进程都能看到的地方。

分布式锁

  • 当在分布式模型下,数据只有一份(或有限制),此时须要利用锁的技术控制某一时刻修改数据的进程数。
  • 与单机模式下的锁不只须要保证进程可见,还须要考虑进程与锁之间的网络问题。(我以为分布式状况下之因此问题变得复杂,主要就是须要考虑到网络的延时和不可靠。。。一个大坑)安全

  • 分布式锁仍是能够将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如Redis、Memcache。至于利用数据库、文件等作锁与单机的实现是同样的,只要保证标记能互斥就行。服务器


单机Redis锁

基本锁

  • 原理:利用Redis的setnx若是不存在某个key则设置值,设置成功则表示取得锁成功。
  • 缺点:若是获取锁后的进程,在还没执行完的时候挂调了,则锁永远不会释放。

改进型

  • 改进:在基本型是锁上的setnx后设置expire,保证即便获取锁的进程不主动释放锁,过一段时间后也能自动释放。
  • 缺点:
    1. setnx与expire不是一个原子操做,可能执行完setnx该进程就挂了。
    2. 当锁过时后,该进程还没执行完,可能形成同时多个进程取得锁。(貌似这个问题目前尚未很优雅的解决方案)

再改进

  • 改进:利用Lua脚本,将setnx与expire变成一个原子操做,可解决一部分问题。
  • 缺点:仍是锁过时的问题。

步骤

1. 直接调用Lua脚本原子setnx同时expire,设置一个随机值。
2. 获取到锁则执行同步代码块,没获取则根据业务场景能够选择自旋、休眠、或作一个等待队列等拥有锁进程来唤醒(相似Synchronize的同步队列)。
3. 当同步代码块执行完成,先判断锁的key是不是本身设置的,若是是则删除key(可利用Lua作成原子操做),不是则代表本身的锁已通过期,不须要删除。(这时候就出现了多进程同时有锁的问题了)

总结

通常状况下直接用setnx加expire就够了,但从安全性的角度看仍是存在一下几个问题:网络

  1. 单点问题。单机Redis只在单机上,若是单机down了,那么全部须要用分布式锁的地方均获取不到锁,所有阻塞。须要作好降级的处理。
  2. 可能出现多进程同时拥有锁。

Redlock

Redlock是Redis的做者antirez给出的集群模式的Redis分布式锁,它基于N个彻底独立的Redis节点(一般状况下N能够设置成5)。多线程

步骤

1. 获取当前时间(毫秒数)。
2. 按顺序依次向N个Redis节点执行获取锁的操做。获取锁的操做与单机锁同样。
3. 若是获取锁成功的节点数>=N/2+1,则再计算获取锁的时间有没有超过锁过时时间(可考虑设置一个必须留多长的时间给代码块执行),若是超过了则认为取锁失败。
4. 若是取锁失败则应该对全部节点进行释放锁的操做。

优化

  • 当有5个节点,某次上锁对a,b,c三个节点上锁成功,然后c立刻down了,此时还没经过AOF或RDB写入磁盘。然后c又立刻恢复,此时c没有上锁数据,所以此时可能出现c,d,e三个节点被别的进程上锁。因此在节点恢复时应该延时起码一个锁的过时时间。

Zookeeper锁

zookeeper锁相关基础知识

  • zk通常由多个节点构成(单数),采用zab一致性协议。所以能够将zk当作一个单点结构,对其修改数据其内部自动将全部节点数据进行修改然后才提供查询服务。
  • zk的数据以目录树的形式,每一个目录称为 znode, znode中可存储数据(通常不超过1M),还能够在其中增长子节点。
  • 子节点有三种类型。序列化节点,每在该节点下增长一个节点自动给该节点的名称上自增。临时节点,一旦建立这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。
  • Watch机制,client能够监控每一个节点的变化,当产生变化会给client产生一个事件。

zk基本锁

  • 原理:利用临时节点与watch机制。每一个锁占用一个普通节点/lock,当须要获取锁时在/lock下建立一个临时节点,建立成功则表示获取锁成功,失败则watch/lock节点,有删除操做后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。并发

  • 缺点:全部取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后全部等待进程一块儿来建立节点,并发量很大。

zk锁 优化

  • 原理:上锁改成建立临时有序节点,每一个上锁的节点均能建立节点成功,知识其序号不一样。只有序号最小的能够拥有锁,当须要不是最小的则watch序号排在前面的一个节点(公平锁)。

  • 步骤:

1. 在/lock节点下建立一个有序临时节点(EPHEMERAL_SEQUENTIAL)。
2. 判断建立的节点序号是否最小,若是是最小则获取锁成功。不是则取锁失败,而后watch序号比自己小的前一个节点。
3. 当取锁失败,设置watch后则等待watch事件到来后,再次判断是否序号最小。
4. 取锁成功则执行代码,最后删除自己节点,释放了锁。

分布式锁总结

分布式锁存在的问题

  1. 都可能存在多进程拥有锁的状况。redis锁主要是expire时间与代码执行时间的问题,zk锁的问题在于zk是经过心跳监控进程存活状态,若是进程进行GC pause或者由于网络缘由致使很长时间没与zk联系,则将致使zk认为进程已挂,然后锁自动释放,而此时进程并未挂任然在执行。
  2. Redlock锁的时间问题。因为redis的expire的实现是经过pexpireat,若是某个节点发生时钟跳跃,则该节点可能过早释放锁致使一系列问题。

解决方案

  1. 获取锁时提供一个fencing token(两种说法,一种说须要有序,一种说随机值就能够,我以为随机值就能够),在进程获取锁后对数据进行操做时,数据所在的资源服务器须要去锁中查看当前token,若是token对的才执行,不对则放弃执行。
  2. 我以为对于放弃执行的应该在咱们的代码块中增长相似事物的rollback的操做。所以若是资源服务器拒绝了咱们的操做则代表此时起码已经存在了另一个进程拥有锁了,为了保证数据安全性不能继续执行,所以须要回滚到执行代码块以前而继续去竞争锁。
  3. 至于Redis锁的时间问题,Antirez说在运维层面是能够控制时钟跳跃的区间的,只要能控制跳跃区间与expire的比例就没问题,详细可看《基于Redis的分布式锁真的安全吗?》

总结

  1. 大多数时候采用zk锁就行了,不必再考虑安全性的问题。其实也能够经过zk锁+幂等校验来达到双层保障。
  2. fencing 机制须要对数据服务进行修改适配,我的以为没这个必要吧。。。

目前就这些了。。。。后面想到再补充吧。

引用:基于Redis的分布式锁真的安全吗?
基于Redis的分布式锁真的安全吗?上
基于Redis的分布式锁真的安全吗?下

相关文章
相关标签/搜索