主流分布式锁实现方案

谈到分布式系统,就不得不谈谈分布式锁。而主流的实现java分布式锁实现方案也就那么几种,有基于Redis实现的,也有基于ZK的,有基于数据库实现的分布式锁,下面我将谈谈它们各种的实现方案。

基于Redis实现分布式锁

基于Redis实现分布式锁应该是比较普遍的,实现起来比较简单.其主要是利用setnx来实现的,具体语法是setnx key val,当该key不存在时就设置value,如果已经存在该key了就直接返回。能这样做主要得益于Redis的单线程结构,能保证setnx是原子性的,其伪代码为:

 if (conn.setnx(lockKey, value) == 1) {

 }

这样做存在一个问题:

  1. 没有给lockKey设置过期时间,有可能导致该key一直不能释放,从而使其它线程不能访问。

有人立马反应过来了,"这还不简单?,用expire设置一下过期时间即可",所以就有了下面的这段代码:

if (conn.setnx(lockKey, value) == 1) {
      conn.expire(lockKey, expireTime);//expireTime为过期时间
}

到底这样做有没有问题呢?考虑一下这种情况,当线程刚刚通过setnx设置值完毕后,系统因为某个原因宕机(断点或者系统问题)导致还没来得及执行expire方法,这时也会引发和上面同样的问题。也就是说简单的设置一个过期时间还不行,因为没法保证setnxexpire是原子操作,随时都可能setnx成功但expire失败。幸运的是,Redis提供了这样一个原子性保证。像Jedis没有直接通过setnx设置过期时间的方法,可以使用这个方法:

 @Override
  public String set(final String key, final String value, final String nxxx, final String expx,
      final long time) {
    return new JedisClusterCommand(connectionHandler, maxRedirections) {
      @Override
      public String execute(Jedis connection) {
        return connection.set(key, value, nxxx, expx, time);
      }
    }.run(key);
  }

        还有一种情况,因为生产环境上Redis一般都是采用的集群,当master节点挂了以后会自动切换到slave。当其中一个线程拿到锁后,此时Redis 的master节点宕机了,因为Redis是通过异步复制将数据同步到从节点的,锁信息完全有可能没有同步成功到从节点,其它线程就能通过setnx获取到锁,从而引发多个线程同时获取锁的问题。那该怎么解决呢?如果业务上可以接受,我觉得没必要考虑这种情况,因为这种情况出现的几率非常少,反之就必须想一个解决方案。
如果Redis是单主单从的话,这个问题基于Redis很难解决,如果是多主多从,可以考虑对所有的master都加锁,即使其中一个master获取锁失败,也不会影响其它两个节点,当总数超过一半时也让其获取到锁。因为涉及到跨多个节点,需要小心的控制超时时间和锁的释放问题。

最后,在方法结束时和抛出异常的时候需要手动释放锁,最好是在finally执行。

基于Zookeeper实现分布式锁

一般用Zookeeper实现分布式锁有两种方案:

  • 基于Zookeeper不能重复创建同一个节点
    利用名称唯一性,加锁操作时,只需要所有客户端一起创建/Lock/test节点,只有一个创建成功,成功者获得锁。解锁时,只需删除/Lock/test节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁.
    这种方案的正确性和可靠性是ZooKeeper机制保证的,实现简单。缺点是会产生“惊群”效应,假如许多客户端在等待一把锁,当锁释放时候所有客户端都被唤醒,仅仅有一个客户端得到锁。
  • 基于临时有序节点
    对于加锁操作,可以让所有客户端都去/Lock目录下创建临时顺序节点,如果创建的客户端发现自身创建节点序列号是/Lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点).进入等待。对于解锁操作,只需要将自身创建的节点删除即可,然后唤醒自己的后一个节点。
    特点:利用临时顺序节点来实现分布式锁机制其实就是一种按照创建顺序排队的实现。这种方案效率高,避免了“惊群”效应,多个客户端共同等待锁,当锁释放时只有一个客户端会被唤醒。

由于Curator客户端已经提供了分布式锁的实现,可以直接使用如下代码:

InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
    try 
    {
        
    } finally {
        lock.release();
    }
}
基于数据库实现分布式锁

利用DB来实现分布式锁,有两种方案。两种方案各有好坏,但是总体效果都不是很好。但是实现还是比较简单的。

  1. 利用主键唯一约束:
    我们知道数据库是有唯一主键规则的,主键不能重复,对于重复的主键会抛出主键冲突异常。
    其实这和分布式锁实现方案基本是一致的,首先我们利用主键唯一规则,在争抢锁的时候向DB中写一条记录,这条记录主要包含锁的id、当前占用锁的线程名、重入的次数和创建时间等,如果插入成功表示当前线程获取到了锁,如果插入失败那么证明锁被其他人占用,等待一会儿继续争抢,直到争抢到或者超时为止
  2. 利用Mysql行锁的特性:
    利用for update加显式的行锁,这样就能利用这个行级的排他锁来实现分布式锁了,同时unlock的时候只要释放commit这个事务,就能达到释放锁的目的。
总结

Redis做分布式锁实现起来比较简单,如果不考虑master宕机引发的并发获取锁的问题,通过简单的setnx就可以实现,性能也很好。
如果项目中已经使用了ZK也可以考虑使用使用zk来做分布式锁,使用curator封装好的分布式锁即可,没必要自己实现。但是zk的主要优势还是做分布式协调,如果针对大压力场景下可能会引发性能问题,最好将其它业务进行隔离。
数据库做分布式锁适合压力比较小的情况,因为频繁的for update可能造成数据库连接的的耗尽,从而引发单点问题。

你可能感兴趣的:(主流分布式锁实现方案)