分布式锁的使用场景:
当我们在面对多个进程使用(一般是先读后改)共享内存这种并发场景时,因为读和写是两个操作,不能保证原子性, 所以需要借助外界的“锁”来将这两个操作锁起来保证原子性。
如果是单机的话, 可以使用系统或语言层面提供的锁来解决。 但是如果处于分布式环境中呢?多台机器没有共享内存可用, 如果保证?
我们抽象一下, 可以把这个问题变成如何在分布式环境下保证同一时刻我在做某事的时候别人不能做,只有等我做完了他才能做。
这个时候我们可以参考系统或语言层面锁的设计来设计自己的锁。把锁作为独立于各个系统外的一个用来做标志位的共享内存(通过网络去保证每个节点都能访问到)。
现在梳理出来的分布式锁的需求如下 :
- 可以通过网络让每个节点都访问到。
- 可以提供“锁”,也就是说某些操作具有唯一性和可复用性,你执行了别人就不能执行,只有等你还原了别人才能执行。
基于这两个需求, 我们可以先看看
最常见的mysql是否符合我们的需求:
需求一肯定满足,再来看需求二,
方法一、mysql中存在唯一索引,我们可以在上锁的时候把我们的锁名作为索引列插入到数据库, 如果插入成功则说明抢锁成功, 插入失败则说明锁被其他人持有。解锁的时候再把这条记录删除。
方法二、使用 insert into xxx where not exiet语句代替唯一索引,原理同上。
Mysql作为分布式锁存在的缺陷在于上锁和解锁是两个操作, 如果上锁后程序无法解锁,就出现死锁,导致不可用,所以还需要在逻辑上做一些处理,比如数据中加入锁过期时间,插入失败进行查询看是否过期,过期就删除然后重新插入。
再来看看redis:
需求一肯定满足,看看需求二,
Redis本身执行命令的时候就是单线程模型,所以天然是原子性的,我们可以使用 setnx 命令来实现加锁,setnx 是如果数据不存在则写入成功,否则写入失败,跟mysql里追求的唯一一样。使用删除命令来实现解锁。我们也可以再加个过期时间来解决消费者挂了的问题。
当然你如想要做得严谨一些,比如你自己加的锁只能你自己删除, 也可以通过lua脚本实现,看业务需求。
如果你觉得加锁和设置过期时间是两个操作不安全, 你也可以通过这个命令来实现:
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)
案例:设置name=liuxinglin,失效时长100s,不存在时设置
set name liuxinglin ex 100 nx
以上两种都是非阻塞的,相当于乐观锁的实现, 你也可以找满足需求的其他方式实现,综合可用性、性能、体量等需求考虑。
如果要实现阻塞式的分布式锁呢?
我们可以使用 Zookeeper 来实现,
大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可(断开连接也会删除)。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。