Zusammenfassung der verteilten Sperrroutine

Ich nehme am "Nuggets · Sailing Program" teil

Was ist eine verteilte Sperre?

Sehen wir uns zunächst an, was eine Sperre ist: Sperre ist eine Lösung für Dateninkonsistenzen, die dadurch verursacht werden, dass mehrere Threads gleichzeitig dieselbe Ressource verwenden. Normalerweise sind die Sperren, die Benutzer verwenden, eigenständige Sperren, z. B. synchronizedSchlüsselwörter und ReentrantLockSperren, die nur Sperren innerhalb desselben Prozesses sind. Jetzt haben viele Dienste mehr als eine Instanz, sodass die Einzelmaschinensperre überhaupt nutzlos ist. Also wurde das Konzept der verteilten Sperre eingeführt.

Die Funktion der verteilten Sperre ist genau die gleiche wie die der eigenständigen Sperre, außer dass sie normalerweise von einem Drittanbieterdienst implementiert werden muss. Die Mainstream-Dienste von Drittanbietern haben redisund mysql.

Lassen Sie uns die beiden Implementierungsmethoden und Unterschiede nacheinander erklären, sowie die meiner Meinung nach beste Methode.

Redis implementiert verteilte Sperren

Redis unterstützt normalerweise eine höhere Parallelität, und Redis bietet einen atomaren Befehl, sodass es als verteilte Sperre geeignet ist:

SET key value NX PX 30000
复制代码

Dieser Befehl bedeutet, einen Schlüsselwert vom Typ Zeichenfolge auf redis zu setzen, der automatisch nach 30 Sekunden abläuft und nur dann erfolgreich gesetzt wird, wenn der Schlüssel nicht existiert. Dies entspricht ganz unseren Anforderungen für verteilte Sperren.Da Redis Single-Threaded ist, können wir diese Operation auf der Client-Seite ausführen, und es wird sichergestellt, dass nur eine Instanz erfolgreich eingerichtet wird. Bedeutet dies, dass die Sperre erfolgreich ist demnächst? Ein Teil des Codes lautet wie folgt:

// 仅当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 {
    // 可以返回服务器繁忙,请稍后再试之类的友好提示
}
复制代码

Einige Ratschläge zu Transaktionen. Allgemein gesagt, wenn die aktuelle Methode zwei oder mehr Operationen umfasst, die Daten ändern, müssen Sie Transaktionen verwenden

Im Allgemeinen ist das obige Schema für die allgemeine Parallelität völlig ausreichend, weist jedoch noch einige Mängel auf:

  1. Das Beurteilen, ob die Sperre vom aktuellen Thread gehalten wird, und das Freigeben der Sperre stellt keine atomare Operation dar. Wenn nur beurteilt wird, dass die Sperre vom aktuellen Thread gehalten wird, läuft sie in der nächsten Sekunde ab und wird von anderen Threads gehalten Dieses Mal werden dann bald andere Threads freigegeben.
  2. Ähnliche Probleme gibt es beim Einreichen von Sachen

Um dieses Problem zu lösen, können wir das lua-Skript verwenden, um die atomare Operation zum Freigeben der Sperre auszuführen:

//释放锁的时候判断了锁是否当前线程持有
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
复制代码

Das Lua-Skript kann sicherstellen, dass wir beurteilen, ob der Wert unserer Erwartung entspricht, und nur wenn er gleich ist, wird die Ressource freigegeben. Und es ist eine atomare Operation.

Dies sieht jedoch schön aus und löst das obige Problem 1, aber Problem 2 kann immer noch nicht gelöst werden, da Transaktionen und Redis-Operationen keine atomaren Operationen sind. Das lua-Skript kann uns offensichtlich nicht dabei helfen, mysql-Transaktionen einzureichen, also was sollen wir tun?

下面就推荐大名鼎鼎的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主键唯一的性质,将新增数据这一动作的成功与否作为获取锁的结果。对于实现自动过期。我们可以增加字段来实现,增加一个过期时间字段和创建时间字段。

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

Ich denke du magst

Origin juejin.im/post/7145637351765573662
Empfohlen
Rangfolge