Bootstrap

Redis 分布式锁实现方案

一、概述

分布式锁,即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。

基于 Redis 单机实现的分布式锁,其方式和 Memcached 的实现方式类似,利用 Redis 的 SETNX 命令,此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。而基于 Redis 多机实现的分布式锁Redlock,是Redis 的作者 antirez 为了规范 Redis 分布式锁的实现,提出的一个更安全有效的实现机制。

二、基于 Redis 单机实现的分布式锁

1、 使用 SETNX 指令

使用 Redis 的 SETNX 指令,该指令只在 key 不存在的情况下,将 key 的值设置为 value,若 key 已经存在,则 SETNX 命令不做任何动作。key 是锁的唯一标识,可以按照业务需要锁定的资源来命名。

2、SETNX + value值是(系统时间+过期时间)

setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁


long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
        return true;
} 
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);    
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
         return true;
    }
}        
//其他情况,均返回加锁失败
return false;
}

3、Redis分布式锁方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)

lua脚本如下:


if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;

加锁代码如下:


 String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";   
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);

三、基于Redisson框架

其实 Redisson 也封装 可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、 信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、 闭锁(CountDownLatch)等。具体参考:Redisson详解及开发实例

大体流程如下:

watch dog自动延期机制:
看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用redisson进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效

1、引入依赖:


<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.19.2</version>
</dependency>  

2、配置类实现


@Bean
public RedissonClient  redissonClient(){
Config config = new Config();
config.setTransportMode(TransportMode.EPOLL); // 默认是NIO的方式
config.useClusterServers()
      //可以用"rediss://"来启用SSL连接,前缀必须是redis:// or rediss://
      .addNodeAddress("redis://127.0.0.1:7181");
return Redisson.create(config);
}

3、工具类


import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.UUID;
 
/**
 * Redisson 加锁
 */
@Component
public class RedissonUtil {
 
    @Resource
    private RedissonClient redissonClient;
 
    public String getKey(){
        return UUID.randomUUID().toString();
    }
 
    public String getKey(Class<?> tClass, Thread thread){
        return tClass.toString() + "_" + thread.getStackTrace()[2].getMethodName();
    }
 
    public RLock getClint(String key){
        RReadWriteLock lock = redissonClient.getReadWriteLock(key);
        return lock.writeLock();
    }
 
    public void lock(String key) {
        this.getClint(key).lock();
    }
 
    public void unLock(String key) {
        this.getClint(key).unlock();
    }

    public void lock(String key, long expire) {
        try {
            this.getClint(key).tryLock(expire, TimeUnit.SECONDS);
        } catch (Exception e) {

        }
    }
 
}
 

4、测试代码:


 @Autowired
 RedissonUtil redissonUtil;
 String key = "leo";
        long extime = 10;
        boolean islock = redissonUtil.lock(key, extime);
        if (islock) {
            try {

            } finally {
                redissonUtil.unLock(key);
            }
        }
;