Die Kolumne „Redis-Tutorial“ stellt Ihnen den Vergleich von drei Arten von verteilten Redis-Sperren vor. Ich hoffe, dass sie Freunden in Not hilfreich sein wird! Die synchronisierten oder Sperren, die wir normalerweise verwenden, sind Thread-Sperren, die für mehrere Threads im selben JVM-Prozess wirksam sind. Da der Kern der Sperre darin besteht, eine Markierung im Speicher zu speichern, die aufzeichnet, wer der Thread ist, der die Sperre erhalten hat, ist diese Markierung für jeden Thread sichtbar. Bei den von uns gestarteten Mehrfachauftragsdiensten handelt es sich jedoch offensichtlich nicht um eine gemeinsame Sperre. Der gegenseitige Ausschluss von Threads kann derzeit nicht garantiert werden sperren. Es gibt drei häufig verwendete Lösungen: 1. Datenbankbasierte Implementierung 2. Zookeeper-basierte temporäre Serialisierungsknotenimplementierung 3. Redis-Implementierung. In diesem Artikel stellen wir die Implementierung von Redis vor.
Um verteilte Sperren zu implementieren, müssen drei Punkte erfüllt sein: für mehrere Prozesse sichtbar, sich gegenseitig ausschließend und wiedereintrittsfähig.
1)
Redis selbst basiert außerhalb der JVM und erfüllt daher die Anforderungen der Sichtbarkeit mehrerer Prozesse.
2)Gegenseitiger Ausschluss
Das heißt, nur ein Prozess kann gleichzeitig die Sperrmarkierung erhalten. Wir können sie über setnx von redis implementieren. Nur die erste Ausführung ist erfolgreich und gibt 1 zurück in anderen Fällen wird 0 zurückgegeben.
Sperre freigeben
Um die Sperre freizugeben, müssen Sie nur den Schlüssel der Sperre löschen und den Befehl del xxx verwenden. Wenn der Dienst jedoch plötzlich ausfällt, bevor wir del ausführen, wird die Sperre nie gelöscht. Daher können wir die Ablaufzeit über den Befehl setex festlegen.
import java.util.UUID;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;/** * 第一种分布式锁 */@Componentpublic class RedisService {private final Logger log = LoggerFactory.getLogger(this.getClass()); @Autowired JedisPool jedisPool; // 获取锁之前的超时时间(获取锁的等待重试时间) private long acquireTimeout = 5000; // 获取锁之后的超时时间(防止死锁) private int timeOut = 10000; /** * 获取分布式锁 * @return 锁标识 */ public boolean getRedisLock(String lockName,String val) { Jedis jedis = null; try { jedis = jedisPool.getResource(); // 1.计算获取锁的时间 Long endTime = System.currentTimeMillis() + acquireTimeout; // 2.尝试获取锁 while (System.currentTimeMillis() < endTime) { // 3. 获取锁成功就设置过期时间 if (jedis.setnx(lockName, val) == 1) { jedis.expire(lockName, timeOut/1000); return true; } } } catch (Exception e) { log.error(e.getMessage()); } finally { returnResource(jedis); } return false; } /** * 释放分布式锁 * @param lockName 锁名称 */ public void unRedisLock(String lockName) { Jedis jedis = null; try { jedis = jedisPool.getResource(); // 释放锁 jedis.del(lockName); } catch (Exception e) { log.error(e.getMessage()); } finally { returnResource(jedis); } }// =============================================== public String get(String key) { Jedis jedis = null; String value = null; try { jedis = jedisPool.getResource(); value = jedis.get(key); log.info(value); } catch (Exception e) { log.error(e.getMessage()); } finally { returnResource(jedis); } return value; } public void set(String key, String value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.set(key, value); } catch (Exception e) { log.error(e.getMessage()); } finally { returnResource(jedis); } } /** * 关闭连接 */ public void returnResource(Jedis jedis) { try { if(jedis!=null) jedis.close(); } catch (Exception e) { } } }Die oben genannte verteilte Sperre wurde implementiert, es können jedoch derzeit zwei weitere Probleme auftreten:
Erstens: Beim Erwerb der Sperre
Setnx hat die Sperre erfolgreich erworben, aber der Setex-Dienst ist ausgefallen, bevor dies möglich war Bei dieser nichtatomaren sexuellen Operation kam es erneut zu einem Stillstand. Tatsächlich stellt Redis NX- und EX-Befehle bereit.
2: Beim Aufheben der Sperre
2. A beginnt mit der Ausführung des Geschäfts. Aus irgendeinem Grund wurde das Geschäft blockiert und es dauerte mehr als 10 Sekunden. Zu diesem Zeitpunkt wurde die Sperre automatisch aufgehoben. 3. B begann zu diesem Zeitpunkt zu versuchen, die Sperre zu erhalten, da die Sperre automatisch erfolgte freigegeben und die Sperre wurde erfolgreich erworben 4. A Dies Wenn das Geschäft ausgeführt wird, wird die Sperrfreigabelogik (Löschen des Schlüssels) ausgeführt, sodass die Sperre von B freigegeben wird und B tatsächlich immer noch das Geschäft ausführt
5. Dabei Gleichzeitig versucht Prozess C, die Sperre zu erhalten, und es gelingt ihm, weil A die Sperre von B gelöscht hat.
Es entsteht ein Problem: B und C haben die Sperre gleichzeitig erworben und damit gegen die gegenseitige Ausschließlichkeit verstoßen! Wie kann dieses Problem gelöst werden? Bevor wir die Sperre löschen, sollten wir feststellen, ob es sich um eine von uns selbst festgelegte Sperre handelt. Wenn nicht (z. B. unsere eigene Sperre wurde im Laufe der Zeit aufgehoben), löschen Sie sie nicht. So können wir beim Setzen der Sperre die eindeutige Kennung des aktuellen Threads speichern! Überprüfen Sie vor dem Löschen der Sperre, ob der darin enthaltene Wert mit Ihrer eigenen Identifikationsfreigabe übereinstimmt. Wenn er inkonsistent ist, bedeutet dies, dass es sich nicht um Ihre eigene Sperre handelt. Löschen Sie sie daher nicht.
/** * 第二种分布式锁 */public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final Long RELEASE_SUCCESS = 1L; /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { if (jedis.get(lockKey).equals(requestId)) { System.out.println("释放锁..." + Thread.currentThread().getName() + ",identifierValue:" + requestId); jedis.del(lockKey); return true; } return false; } }
Wiedereintrittssperre
:
也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像synchronized就是一个重入锁,它是通过moniter函数记录当前线程信息来实现的。实现可重入锁需要考虑两点:
获取锁:首先尝试获取锁,如果获取失败,判断这个锁是否是自己的,如果是则允许再次获取, 而且必须记录重复获取锁的次数。
释放锁:释放锁不能直接删除了,因为锁是可重入的,如果锁进入了多次,在内层直接删除锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。
下面我们假设锁的key为“ lock ”,hashKey是当前线程的id:“ threadId ”,锁自动释放时间假设为20 获取锁的步骤: 1、判断lock是否存在 EXISTS lock 2、不存在,则自己获取锁,记录重入层数为1. 2、存在,说明有人获取锁了,下面判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId 3、不存在,说明锁已经有了,且不是自己获取的,锁获取失败. 3、存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20 释放锁的步骤: 1、判断当前线程id作为hashKey是否存在: HEXISTS lock threadId 2、不存在,说明锁已经失效,不用管了 2、存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 , 3、获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock
因此,存储在锁中的信息就必须包含:key、线程标识、重入次数。不能再使用简单的key-value结构, 这里推荐使用hash结构。
获取锁的脚本(注释删掉,不然运行报错)
local key = KEYS[1]; -- 第1个参数,锁的keylocal threadId = ARGV[1]; -- 第2个参数,线程唯一标识local releaseTime = ARGV[2]; -- 第3个参数,锁的自动释放时间if(redis.call('exists', key) == 0) then -- 判断锁是否已存在 redis.call('hset', key, threadId, '1'); -- 不存在, 则获取锁 redis.call('expire', key, releaseTime); -- 设置有效期 return 1; -- 返回结果end;if(redis.call('hexists', key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己 redis.call('hincrby', key, threadId, '1'); -- 如果是自己,则重入次数+1 redis.call('expire', key, releaseTime); -- 设置有效期 return 1; -- 返回结果end;return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
释放锁的脚本(注释删掉,不然运行报错)
local key = KEYS[1]; -- 第1个参数,锁的keylocal threadId = ARGV[1]; -- 第2个参数,线程唯一标识if (redis.call('HEXISTS', key, threadId) == 0) then -- 判断当前锁是否还是被自己持有 return nil; -- 如果已经不是自己,则直接返回end;local count = redis.call('HINCRBY', key, threadId, -1); -- 是自己的锁,则重入次数-1if (count == 0) then -- 判断是否重入次数是否已经为0 redis.call('DEL', key); -- 等于0说明可以释放锁,直接删除 return nil; end;
完整代码
import java.util.Collections;import java.util.UUID;import org.springframework.core.io.ClassPathResource;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.scripting.support.ResourceScriptSource;/** * Redis可重入锁 */public class RedisLock { private static final StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class); private static final DefaultRedisScript<Long> LOCK_SCRIPT; private static final DefaultRedisScript<Object> UNLOCK_SCRIPT; static { // 加载释放锁的脚本 LOCK_SCRIPT = new DefaultRedisScript<>(); LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua"))); LOCK_SCRIPT.setResultType(Long.class); // 加载释放锁的脚本 UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua"))); } /** * 获取锁 * @param lockName 锁名称 * @param releaseTime 超时时间(单位:秒) * @return key 解锁标识 */ public static String tryLock(String lockName,String releaseTime) { // 存入的线程信息的前缀,防止与其它JVM中线程信息冲突 String key = UUID.randomUUID().toString(); // 执行脚本 Long result = redisTemplate.execute( LOCK_SCRIPT, Collections.singletonList(lockName), key + Thread.currentThread().getId(), releaseTime); // 判断结果 if(result != null && result.intValue() == 1) { return key; }else { return null; } } /** * 释放锁 * @param lockName 锁名称 * @param key 解锁标识 */ public static void unlock(String lockName,String key) { // 执行脚本 redisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(lockName), key + Thread.currentThread().getId(), null); } }
至此,一个比较完善的redis锁就开发完成了。
Das obige ist der detaillierte Inhalt vonVergleich von drei Arten verteilter Redis-Sperren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!