分布式锁特性

  • 互斥
    分布式锁需要保证在不同节点的不同线程的互斥。
  • 重入性
    同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁
  • 锁超时
    锁超时:和本地锁一样支持锁超时,防止死锁
  • 支持阻塞和非阻塞
    和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)

mysql 实现分布式锁

lock要加事务,过程主要分为:

  • 1、先查询记录,通过一个业务标识来查询得知有没有锁
  • 2、查询不到记录就说明没锁,自己占用锁,做插入操作:业务id 加 节点线程信息等等
  • 3、查询到记录,看节点、线程信息是否一样,一样获得锁,重入锁,并更新记录count值:count+1,不一样则没有锁
  • 3、查询到记录,没有锁,继续等待,超时则返回。

unlock 要加事务,过程:

  • 1、获取锁信息,判断count值,大于1则更新count:count - 1
  • 2、count == 1 :直接删除记录,表明释放锁
  • 3、其他情况告知释放锁失败
优点与缺点
  • 适用场景: Mysql分布式锁一般适用于资源不存在数据库,可以吧mysql分布式锁封装为一个组件提供出去。
  • 优点:理解起来简单,不需要维护额外的第三方中间件(比如Redis,Zk)。
  • 缺点:虽然容易理解但是实现起来较为繁琐,需要自己考虑锁超时,加事务等等。性能局限于数据库,一般对比缓存来说性能较低。对于高并发的场景并不是很适合。

redis 做分布式锁

Redis因为其性能好,单线程,实现起来简单等优点,比较适合做分布式锁。
1、get
2、set
3、delete
不过get、set之间有一些问题,如果两个请求同时get,这样就会出现同时set方法,这是一个问题,所以有了另一张方法,通过setNx(set if not exist)方法来实现,获取锁:

  • setnx(lockkey, value) ,value 可以为当前时间+过期超时时间
    如果返回 0,则锁失败,成功则设置 value为一个时间,主要防止宕机之后,这个锁一直存在,无妨释放,通过时间可以判断这个key是否过期
  • 没有锁成功,则get(lockkey) 获取值 oldExpireTime
    来查看锁时间是否过期

为什么用setnx方法?它是set if not exist的缩写,也就是如果不存在就set,如果返回1则表示加锁成功,如果返回0则表示其它线程先执行成功了。

释放锁:

  • 通过delete操作来释放锁

这里有一个明显问题:如果过了时间,方法还没执行完,自然也不会释放锁,但是其他线程会读取时间来判断,会误认为已经释放锁了,会去竞争锁,这样可能会导致并发问题,所以有人提出了:基于 redisson 做分布式锁。redisson 是 redis 官方的分布式锁组件,它主要策略是:每获得一个锁时,只设置一个很短的超时时间,同时起一个线程在每次快要到超时时间时去刷新锁的超时时间。在释放锁的同时结束这个线程。

除了上述问题,存在一个问题:在我们的系统架构里存在一个单点故障,如果Redis的master节点宕机了怎么办呢?大多数人会选择加一个slave节点!做master-slave模式,但有人提出:

  • 线程A在master节点拿到了锁。
  • master节点在把A创建的key写入slave之前宕机了。
  • slave变成了master节点。
  • 线程B也得到了和A还持有的相同的锁。(因为原来的slave里面还没有A持有锁的信息)

如果这两个线程操作一个对象,则会出现问题,Martin 提出了Redlock方法,Redlock算法的主要思想是:假设我们有N个Redis master节点,这些节点都是完全独立的,我们可以运用前面的方案来对前面单个的Redis master节点来获取锁和解锁,如果我们总体上能在合理的范围内或者N/2+1个锁,那么我们就可以认为成功获得了锁。

但此方法引起了antirez的反驳,他的观点主要从超时时间出发,主要有两点:

  • FGC
    如果一个线程A 的环境碰到了 FGC,且 FGC的时间超过了超时时间,另一个线程B就会获得锁, FGC过后,线程A、B都修改、提交了数据,这里就会有问题,IO或者网络的堵塞或波动都可能造成系统停顿,都会引发这个问题。
  • 依赖系统时间
    (1) Client 1 从 A、B、D、E五个节点中,获取了 A、B、C三个节点获取到锁,我们认为他持有了锁
    (2) 由于 B 的系统时间比别的系统走得快,B就会先于其他两个节点优先释放锁。
    (3) Clinet 2 可以从 B、D、E三个节点获取到锁。在整个分布式系统就造成 两个 Client 同时持有锁了
Redis优点与缺点

优点:Redis实现简单,性能对比ZK和Mysql较好,吞吐量十分客观,对于高并发情况应付自如,自带超时保护,对于网络抖动的情况也可以利用超时删除策略保证不会阻塞所有流程。

缺点:没有线程唤醒机制、网络抖动可能会引起锁删除失败,需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群。

zookeeper

ZooKeeper是以Paxos算法为基础分布式应用程序协调服务,他的特性是可以创建有序、临时节点,利用这个节点可以来做一些事情,加锁流程:

  • 创建一个锁节点:要求这个节点下面的节点类型都是 临时、有序的。
  • 每一个线程过来都在锁节点下面注册一个节点,这个子节点就是临时、有序
  • 每个子节点通过Watcher机制监听序号比自己小一个的节点,当这个节点消失了,自己就被唤醒
  • 每次取序号最小的节点来获得锁

释放锁流程:

  • 删除当前节点。

如果有重入锁要求,可以本地维护一个hash对象,里面存入线程信息,加锁的时候往里加信息,释放锁就删除信息。

zookeeper 优点与缺点
  • 优点
    ZK可以不需要关心锁超时时间,实现起来有现成的第三方包,比较方便,并且支持读写锁,ZK获取锁会按照加锁的顺序,所以其是公平锁。对于高可用利用ZK集群进行保证。
  • 缺点
    ZK需要额外维护,增加维护成本,性能和Mysql相差不大,依然比较差。