분산 잠금 루틴 요약

"너겟 · 세일링 프로그램"에 참가하고 있습니다

분산 잠금이란 무엇입니까?

먼저 잠금이 무엇인지 살펴보겠습니다 잠금은 동일한 리소스를 동시에 작동하는 여러 스레드로 인해 발생하는 데이터 불일치에 대한 솔루션입니다. 일반적으로 사람들이 사용하는 잠금은 동일한 프로세스 내에서만 잠금인 synchronized키워드 및 잠금 과 같은 독립 실행형 잠금 입니다. ReentrantLock이제 많은 서비스에 둘 이상의 인스턴스가 있으므로 단일 시스템 잠금은 전혀 쓸모가 없습니다. 그래서 분산 잠금의 개념이 도입되었습니다.

분산 잠금의 기능은 일반적으로 타사 서비스에서 구현해야 한다는 점을 제외하고 독립 실행형 잠금의 기능과 정확히 동일합니다. 주류 타사 서비스에는 redismysql.

두 가지 구현 방법과 차이점, 그리고 제가 생각하는 가장 좋은 방법을 하나씩 설명드리겠습니다.

Redis는 분산 잠금을 구현합니다.

Redis는 일반적으로 더 높은 동시성을 지원하고 redis는 원자 명령을 제공하므로 분산 잠금으로 적합합니다.

SET key value NX PX 30000
复制代码

이 명령은 문자열 유형의 키-값을 redis로 설정하는 것을 의미하며, 이는 30초 후에 자동으로 만료 되며 키가 존재하지 않는 경우에만 성공적으로 설정됩니다. 이것은 분산 잠금에 대한 요구 사항과 매우 일치합니다. 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
复制代码

루아 스크립트는 값이 우리의 기대치와 같은지 판단할 수 있도록 보장할 수 있으며, 같을 때만 리소스가 해제됩니다. 그리고 그것은 원자적 연산입니다.

그러나 이것은 보기에 좋고 위의 문제 1을 해결하지만 트랜잭션 및 redis 작업이 원자적 작업이 아니기 때문에 문제 2는 여전히 해결할 수 없습니다. 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主键唯一的性质,将新增数据这一动作的成功与否作为获取锁的结果。对于实现自动过期。我们可以增加字段来实现,增加一个过期时间字段和创建时间字段。

释放锁就是删除数据即可,如果锁支持自动失效,需要在释放锁时添加相应条件以防止释放锁时刚好自动失效。

рекомендация

отjuejin.im/post/7145637351765573662
рекомендация