Vorwort
Verteilte Sperren werden häufig in verteilten Anwendungen verwendet. Wenn Sie etwas Neues verstehen möchten, müssen Sie zunächst seinen Ursprung verstehen, damit Sie es besser verstehen und sogar Schlussfolgerungen ziehen können.
Wenn wir über verteilte Sperren sprechen, denken wir zunächst natürlich an verteilte Anwendungen.
Bevor wir die Anwendung in verteilte Anwendungen aufteilen, können wir beim Lesen öffentlicher Ressourcen in einigen gleichzeitigen Szenarien, z. B. dem Abzug von Inventar und dem Verkauf von Tickets, einfach eine Synchronisierung oder Sperre verwenden Dies kann erreicht werden .
Aber nach der Verteilung der Anwendung wechselt das System vom vorherigen Einzelprozess- und Multithread-Programm zu einem Mehrprozess- und Multithread-Programm. Zu diesem Zeitpunkt reicht die obige Lösung offensichtlich nicht aus.
Daher besteht eine in der Branche häufig verwendete Lösung normalerweise darin, sich auf eine Komponente eines Drittanbieters zu verlassen und deren eigene Exklusivität zu nutzen, um einen gegenseitigen Ausschluss mehrerer Prozesse zu erreichen. Zum Beispiel:
Eindeutiger Index basierend auf DB.
ZK-basierte temporäre geordnete Knoten.
Basierend auf den NX EX
Parametern von Redis.
Die Diskussion hier basiert hauptsächlich auf Redis.
Da Redis ausgewählt wird, muss es exklusiv sein. Gleichzeitig ist es am besten, über einige Grundfunktionen von Schlössern zu verfügen:
Hohe Leistung (hohe Leistung beim Hinzufügen und Entsperren)
Sie können blockierende Sperren mit nicht blockierender Sperre verwenden.
Es kann kein Deadlock auftreten.
Verfügbarkeit (die Sperre kann nicht fehlschlagen, nachdem der Knoten ausgefallen ist).
Die Verwendung eines NX-Parameters in Redis set key
kann hier ein erfolgreiches Schreiben sicherstellen, auch wenn der Schlüssel nicht vorhanden ist. Und durch Hinzufügen des EX-Parameters kann der Schlüssel nach einer Zeitüberschreitung automatisch gelöscht werden.
Durch die Verwendung der beiden oben genannten Funktionen kann sichergestellt werden, dass nur ein Prozess gleichzeitig die Sperre erhält und kein Deadlock auftritt (im schlimmsten Fall wird der Schlüssel nach einer Zeitüberschreitung automatisch gelöscht).
Der Implementierungscode lautet wie folgt:
private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; public boolean tryLock(String key, String request) { String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME); if (LOCK_MSG.equals(result)){ return true ; }else { return false ; } }
Beachten Sie, dass hier die
String set(String key, String value, String nxxx, String expx, long time);
API von jedis verwendet wird.
Dieser Befehl kann die Atomizität von NX EX sicherstellen.
Achten Sie darauf, die beiden Befehle (NX EX) nicht getrennt auszuführen. Wenn nach NX ein Problem mit dem Programm auftritt, kann es zu einem Deadlock kommen.
Sie können auch eine Blockierungssperre implementieren:
//一直阻塞 public void lock(String key, String request) throws InterruptedException { for (;;){ String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME); if (LOCK_MSG.equals(result)){ break ; } //防止一直消耗 CPU Thread.sleep(DEFAULT_SLEEP_TIME) ; } } //自定义阻塞时间 public boolean lock(String key, String request,int blockTime) throws InterruptedException { while (blockTime >= 0){ String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME); if (LOCK_MSG.equals(result)){ return true ; } blockTime -= DEFAULT_SLEEP_TIME ; Thread.sleep(DEFAULT_SLEEP_TIME) ; } return false ; }
Das Entsperren ist auch sehr einfach. Löschen Sie einfach diesen Schlüssel und Alles wird gut, zum Beispiel mit dem Befehl del key
.
Aber die Realität ist oft nicht so einfach.
Wenn Prozess A die Sperre erhält und eine Zeitüberschreitung festlegt, die Sperre jedoch aufgrund eines langen Ausführungszyklus nach der Zeitüberschreitung automatisch aufgehoben wird. Zu diesem Zeitpunkt erhält Prozess B die Sperre und gibt sie bald wieder frei. Auf diese Weise gibt Prozess B die Sperre von Prozess A frei.
Der beste Weg ist also, bei jedem Entsperren festzustellen, ob das Schloss Ihnen gehört .
Zu diesem Zeitpunkt muss es in Verbindung mit dem Verriegelungsmechanismus implementiert werden.
Beim Sperren müssen Sie einen Parameter übergeben und diesen Parameter als Wert dieses Schlüssels verwenden, damit Sie bei jedem Entsperren beurteilen können, ob die Werte gleich sind.
Der Entsperrcode kann also kein einfacher del
sein.
public boolean unlock(String key,String request){ //lua script String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = null ; if (jedis instanceof Jedis){ result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request)); }else if (jedis instanceof JedisCluster){ result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request)); }else { //throw new RuntimeException("instance is error") ; return false ; } if (UNLOCK_MSG.equals(result)){ return true ; }else { return false ; } }
Hier wird ein lua
-Skript verwendet, um festzustellen, ob die Werte gleich sind, und der Befehl del wird nur ausgeführt, wenn sie gleich sind.
Die Verwendung von lua
kann hier auch die Atomizität der beiden Operationen sicherstellen.
Damit auch die vier oben genannten Grundfunktionen erfüllt werden können:
Die Verwendung von Redis kann die Leistung sicherstellen.
Siehe oben für blockierende Schlösser und nicht blockierende Schlösser.
Verwenden Sie den Timeout-Mechanismus, um den Deadlock zu lösen.
Redis unterstützt die Clusterbereitstellung, um die Verfügbarkeit zu verbessern.
habe ich selbst eine vollständige Implementierung erstellt und sie wurde in der Produktion verwendet. Interessierte Freunde können sie sofort verwenden:
Maven-Abhängigkeit:
<dependency> <groupId>top.crossoverjie.opensource</groupId> <artifactId>distributed-redis-lock</artifactId> <version>1.0.0</version> </dependency>
Konfigurations-Bean:
@Configuration public class RedisLockConfig { @Bean public RedisLock build(){ RedisLock redisLock = new RedisLock() ; HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ; JedisCluster jedisCluster = new JedisCluster(hostAndPort) ; // Jedis 或 JedisCluster 都可以 redisLock.setJedisCluster(jedisCluster) ; return redisLock ; } }
Verwendung:
@Autowired private RedisLock redisLock ; public void use() { String key = "key"; String request = UUID.randomUUID().toString(); try { boolean locktest = redisLock.tryLock(key, request); if (!locktest) { System.out.println("locked error"); return; } //do something } finally { redisLock.unlock(key,request) ; } }
Es ist sehr einfach zu verwenden. Der Hauptzweck hier besteht darin, Spring zu verwenden, um uns bei der Verwaltung der RedisLock-Singleton-Bean zu helfen. Wenn wir also die Sperre aufheben, müssen wir den Schlüssel und die Anforderung manuell übergeben (da der gesamte Kontext nur eine RedisLock-Instanz hat) (die API sieht nicht so aus). besonders elegant).
Sie können auch ein neues RedisLock erstellen und den Schlüssel und die Anforderung jedes Mal eingeben, wenn Sie das Schloss verwenden, was beim Entsperren sehr praktisch ist. Sie müssen die RedisLock-Instanz jedoch selbst verwalten. Jedes hat seine eigenen Vor- und Nachteile.
Während der Arbeit an diesem Projekt muss ich Einzeltest erwähnen.
Da diese Anwendung stark von Komponenten Dritter (Redis) abhängig ist, müssen wir diese Abhängigkeit im Einzeltest ausschließen. Beispielsweise hat ein anderer Partner das Projekt geforkt und wollte einen einzelnen Test lokal ausführen, aber das Ergebnis konnte nicht ausgeführt werden:
Es kann sein, dass die IP und der Port von Redis nicht mit diesen übereinstimmen im Einzeltest.
Redis selbst kann auch Probleme haben.
Es ist auch möglich, dass der Student kein Redis in seiner Umgebung hat.
Daher ist es am besten, diese externen instabilen Faktoren zu beseitigen und nur den Code zu testen, den wir geschrieben haben.
Dann können Sie das einzelne Testtool vorstellen Mock
.
Die Idee ist sehr einfach, nämlich alle externen Ressourcen zu blockieren, auf die Sie angewiesen sind. Zum Beispiel: Datenbank, externe Schnittstelle, externe Dateien usw.
使用方式也挺简单,可以参考该项目的单测:
@Test public void tryLock() throws Exception { String key = "test"; String request = UUID.randomUUID().toString(); Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong())).thenReturn("OK"); boolean locktest = redisLock.tryLock(key, request); System.out.println("locktest=" + locktest); Assert.assertTrue(locktest); //check Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong()); }
这里只是简单演示下,可以的话下次仔细分析分析。
它的原理其实也挺简单,debug 的话可以很直接的看出来:
这里我们所依赖的 JedisCluster 其实是一个 cglib 代理对象
。所以也不难想到它是如何工作的。
比如这里我们需要用到 JedisCluster 的 set 函数并需要它的返回值。
Mock 就将该对象代理了,并在实际执行 set 方法后给你返回了一个你自定义的值。
这样我们就可以随心所欲的测试了,完全把外部依赖所屏蔽了。
至此一个基于 Redis 的分布式锁完成,但是依然有些问题。
如在 key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。
就算 Redis 是集群部署的,如果每个节点都只是 master 没有 slave,那么 master 宕机时该节点上的所有 key 在那一时刻都相当于是释放锁了,这样也会出现并发问题。就算是有 slave 节点,但如果在数据同步到 salve 之前 master 宕机也是会出现上面的问题。
感兴趣的朋友还可以参考 Redisson 的实现。
Das obige ist der detaillierte Inhalt vonSo implementieren Sie verteilte Sperren basierend auf Redis. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!