我正在参与「启航计划」

什么是分布式锁?

首要回顾下什么是锁吧,锁是处理多个线程一起操作同一个资源而导致数据不一致的一种处理方法。一般人们运用的锁是单机锁,如synchronized关键字和ReentrantLock锁,都仅仅同一个进程内的锁。而现在不少服务都会布置不止一个实例,所以导致单机锁底子没用了。所以就引入了分布式锁的概念。

分布式锁锁作用跟单机锁彻底一样, 仅仅它一般需求借助第三方服务来完成。主流的第三方服务有redismysql

下面来一一解说这两种完成方法和差异,以及个人认为的最好方法。

redis完成分布式锁

redis一般支持较高并发,且redis供给了一个原子指令,所以合适作为分布式锁,:

SET key value NX PX 30000

这条指令表明设置一个string类型的key-value到redis,30秒后主动过期,且仅当key不存在时才会设置成功。这就挺契合咱们分布式锁的要求了,由于redis是单线程,咱们能够在客户端进行此操作,会确保只要一个实例设置成功,不久代表加锁成功了吗?部分代码如下:

// 仅当key不存在才会设置成功,一般是将key设置为需求操作的资源仅有id,
// 例如,咱们需求秒杀产品,key就设置为产品id
// 而 value一般设置为随机数,来确保开释锁的时分是当时线程持有。我这儿运用【hutool】东西生成了16位随机字符串
// 过期时刻也需求设置,由于假如该线程出现反常,就会导致资源无法开释,形成其他线程永久拿不到锁了
String randomString = RandomUtil.randomString(16);
//此方法会回来一个成果来表明是否操作成功
Boolean result = redisTemplate.opsForValue().setIfAbsent("productId", randomString);
//加锁成功
if (Boolean.TRUE.equals(result)) {
  try {
    // 业务处理
   } catch (Exception ignored) {
    // 回滚业务
   } finally {
    // 判别该锁是否当时线程持有,是才会开释
    if (redisTemplate.opsForValue().get("productId").equals(randomString)) {
        //提交业务
      //开释锁
      redisTemplate.delete("productId");
     } else {
      //业务履行时刻过长导致锁主动失效,此刻需求开释资源,如回滚事物等
     } 
   }
}
//加锁失败
else {
  // 能够回来服务器繁忙,请稍后再试之类的友好提示
}

关于业务的一些主张,一般来说,假使当时方法涉及到2个或以上修正数据的操作,需求运用业务

一般来说,关于一般并发,上述方案彻底够用,可是,它仍然有少许缺陷:

  1. 判别锁是否为当时线程持有和开释锁操作不是原子操作,假使,刚刚判别完锁是当时线程持有,下一秒就过期了,此刻又被其他线程持有,那么不久会开释其他线程持有的锁了吗?
  2. 提交事物的时分也会有类似问题

针对这个问题,咱们能够运用lua脚原本进行开释锁的原子操作:

//开释锁的时分判别了锁是否当时线程持有
if redis.call("get",KEYS[1]) == ARGV[1] then
  return redis.call("del",KEYS[1])
else
  return 0
end

lua脚本能够确保咱们判别value是否与咱们预期持平,只要持平时才会开释资源。并且这是一个原子操作。

可是,这样看起来很美好,能够处理上述问题1,不过问题2仍然无法得到处理,由于业务与redis操作不是原子操作。lua脚本很明显无法做到帮咱们提交mysql业务,那么应该怎样办呢?

下面就推荐大名鼎鼎的redisson,它是一个redis客户端,主要支持一些分布式相关的东西,其间就有分布式锁。

说白了,上述两个问题,咱们已经处理了其间一个了,采用了lua脚本处理,而别的一个问题,底子原因是由于主动过期时刻设置多大问题。

那么,咱们应该设置多长时刻呢?

首要咱们应该尽可能的设置一个确保在业务能够正常履行结束的范围。

可是,其实不管咱们设置多少,理论上来说都不合适,由于你无法确保业务代码履行的详细时刻。假使设置小了,导致业务履行结束后锁过期,还要额外进行回滚操作,设置大了,可能导致其他线程堵塞时刻过长。所以,这个时刻怎样设置都不好使,总会有瑕疵。

redisson采用了看门狗设置,也便是会起一个看护线程,来监测这个线程是否开释锁,假如此线程一向在活动,且过期时刻快要结束,看门狗机制就会主动续期。

所以,看门狗机制确保了,线程持有锁后,只要线程还在活跃,且锁未开释,锁会永不过期,有人看到这儿可能会怀疑了,永不过期? ,那么岂不是跟没设置过期时刻没啥两样,哈哈,并不是,我说的仅仅理论上永不过期,实际上咱们的代码终究会履行结束(除非写了死循环)。

redisson开释锁的时分同样采用了lua脚本的方法判别是否当时锁持有。

最佳实践代码如下:

合适并发一般的状况:

RLock lock = redissonClient.getLock("productId");
//会一向堵塞去获取锁,直到成功,默许30秒过期,会主动续期
lock.lock();
try {
  //履行业务代码
  //正常履行,然后提交业务,这儿锁会一向续期,所以不必忧虑锁会主动过期
} catch (Exception ignored) {
  //反常,回滚业务
} finally {
  //开释锁,需求判别一下是否当时线程持有
  if (lock.isHeldByCurrentThread()) {
    lock.unlock();
   }
}

合适并发较高的状况:

RLock lock = redissonClient.getLock("productId");
//会一向堵塞去获取锁,直到成功,但5秒还未获取锁,会回来成果,锁默许30秒过期,会主动续期
// ture代表获取锁成功,否则失败
if (lock.tryLock(5, TimeUnit.SECONDS)) {
  try {
    //履行业务代码
    //正常履行,然后提交业务,这儿锁会一向续期,所以不必忧虑锁会主动过期导致业务提交后才过期
   } catch (Exception ignored) {
    //反常,回滚业务
   } finally {
    //开释锁,需求判别一下是否当时线程持有
    //留意:这儿假如不判别也是能够的,只不过会抛出反常
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
     }
   }
} else {
  // 能够回来服务器繁忙,请稍后再试之类的友好提示
}

关于redisson的看门狗失效状况:

// 没有Watch Dog ,10s后锁开释
lock.lock(10, TimeUnit.SECONDS);
// 没有Watch Dog ,10s后锁开释,尝试获取100s
lock.tryLock(100, 10, TimeUnit.SECONDS);

redisson默许锁过期时刻为30s,只要设置了过期时刻,看门狗机制就会失效

MYSQL完成分布式锁

mysql完成分布式锁的方法最为简单,咱们能够使用mysql主键仅有的性质,将新增数据这一动作的成功与否作为获取锁的成果。关于完成主动过期。咱们能够增加字段来完成,增加一个过期时刻字段和创立时刻字段。

开释锁便是删去数据即可,假如锁支持主动失效,需求在开释锁时添加相应条件以防止开释锁时刚好主动失效。