Home  >  Article  >  Database  >  Comparison of three types of Redis distributed locks

Comparison of three types of Redis distributed locks

藏色散人
藏色散人forward
2020-11-04 14:48:092369browse

The following column Redis Tutorial will introduce to you the comparison of three types of Redis distributed locks. I hope it will be helpful to friends in need!

The synchronized or Lock we usually use are thread locks, which are valid for multiple threads in the same JVM process. Because the essence of the lock is to store a mark in the memory, recording who is the thread that acquired the lock, this mark is visible to every thread. However, the multiple order services we started are multiple JVMs. The locks in the memory are obviously not shared. Each JVM process has its own lock. Naturally, the mutual exclusion of threads cannot be guaranteed. At this time, we need to use Distributed lock. There are three commonly used solutions: 1. Database-based implementation 2. Zookeeper-based temporary serialization node implementation 3. redis implementation. In this article, we introduce the implementation of redis.
 To implement distributed locks, three points must be met: visible to multiple processes, mutually exclusive, and reentrant.

1) Visible to multiple processes

redis itself is based outside the JVM, so it can be visible to multiple processes Require.

2) Mutual exclusion

That is, only one process can obtain the lock mark at the same time. We can use redis In the setnx implementation, only the first execution will succeed and return 1, and 0 will be returned in other cases.

 

Release the lock
To release the lock, you only need to delete the key of the lock, and use the del xxx command. However, if the service suddenly goes down before we execute del, the lock will never be deleted. So we can set the expiration time through the setex command.

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) {
        }
    }
}

The above distributed lock has been implemented, but there are two other problems that may occur at this time:
One: When acquiring the lock
Setnx successfully acquired the lock, but the setex service failed before it could The problem is that a deadlock occurs again due to this non-atomic operation. In fact, redis provides nx and ex commands.

 
2: When releasing the lock
1. Three processes: A, B and C, are executing tasks and competing for the lock. At this time, A acquires the lock and sets The automatic expiration time is 10s
2. A starts to execute the business. For some reason, the business is blocked and takes more than 10 seconds. At this time, the lock is automatically released
3. B starts trying to acquire the lock at this time. Because the lock has been automatically released, the lock was successfully acquired
4. A completed the business execution at this time and executed the lock release logic (deleting the key), so B's lock was released, and B is actually still executing the business
5. At this time, process C tried to acquire the lock and succeeded because A deleted B's lock.
The problem arises: B and C acquired the lock at the same time, violating mutual exclusivity! How to solve this problem? Before deleting the lock, we should determine whether the lock is a lock set by ourselves. If not (for example, our own lock has been released over time), then do not delete it. So we can store the unique identifier of the current thread when setting the lock! Before deleting the lock, check whether the value inside is consistent with your own identification release. If it is inconsistent, it means that it is not your own lock, so do not delete it.

/**
 * 第二种分布式锁 */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;
    }
}

After implementing distributed locks in the above way, most problems can be easily solved. Many blogs on the Internet also implement this method, but there are still some scenarios that are not satisfactory. For example, after a method obtains a lock, you may not be able to obtain the lock if you call the method within the method. At this time we need to improve the lock into a reentrant lock.

 

3) Reentrancy lock

  也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像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(&#39;exists&#39;, key) == 0) then -- 判断锁是否已存在
    redis.call(&#39;hset&#39;, key, threadId, &#39;1&#39;); -- 不存在, 则获取锁
    redis.call(&#39;expire&#39;, key, releaseTime); -- 设置有效期
    return 1; -- 返回结果end;if(redis.call(&#39;hexists&#39;, key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己    
    redis.call(&#39;hincrby&#39;, key, threadId, &#39;1&#39;); -- 如果是自己,则重入次数+1
    redis.call(&#39;expire&#39;, key, releaseTime); -- 设置有效期
    return 1; -- 返回结果end;return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的脚本(注释删掉,不然运行报错)

local key = KEYS[1]; -- 第1个参数,锁的keylocal threadId = ARGV[1]; -- 第2个参数,线程唯一标识if (redis.call(&#39;HEXISTS&#39;, key, threadId) == 0) then -- 判断当前锁是否还是被自己持有
    return nil; -- 如果已经不是自己,则直接返回end;local count = redis.call(&#39;HINCRBY&#39;, key, threadId, -1); -- 是自己的锁,则重入次数-1if (count == 0) then -- 判断是否重入次数是否已经为0
    redis.call(&#39;DEL&#39;, 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锁就开发完成了。

The above is the detailed content of Comparison of three types of Redis distributed locks. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:cnblogs.com. If there is any infringement, please contact admin@php.cn delete