Redis分布式锁
介绍
随着微服务的兴起,比如Spring Cloud、Dubbo的等分布式解决方案地兴起,java的jvm锁某些场景下已经不在适用。特定的场景下,往往依靠分布式锁,常见的分布式锁实现方式有Redis分布式锁以及Zookeeper分布式锁。Redis分布式锁在项目编码中更为常见,比较成熟的框架有Redisson,就实现了分布式锁。虽然项目中我们可以使用比较成熟的Redisson,但是这篇文章侧重于自己实现一个分布式锁。
原理
Redis可以作为分布式锁的关键因素有2个:1.单线程模型;2.基于内存高性能。单线程模型意味着,在任意时刻,只有一个线程可以持有并操作一个key。如果我们实现以个分布式锁,当A线程持有锁时,其他任何一个线程不能再获取同样的锁。因此,加锁的操作必须是一个原子操作。
SETNX命令
SETNX是redis的一个命令,也是一个原子操作命令,当redis中key不存在时,使用SETNX命令给key设置一个value,设值成功时返回1,若key存在,则设值失败,返回0。完美契合了我们加锁的条件,原子操作、加锁成功返回true、加锁失败返回false。命令执行如下:
问题
java jvm锁中提供tryLock(long** **time, TimeUnit unit)方法,当程序试图加锁时,可以设置持有锁的时间,超过持锁时间后,即使没有手动释放锁,锁也可以被释放掉,可以有效防止死锁。redis分布式锁如何实现这个功能呢?我们想到了EXPIRE,当SETNX获取锁成功后,在使用EXPIRE设置锁的过期时间。但是,SETNX、EXPIRE两个命令顺序执行就不是原子操作了。redis提供了lua脚本的方式,可以在脚本中执行redis命令,执行lua脚本是原子操作,因此我们可以在lua脚本中SETNX,再EXPIRE。
代码
# SETNX_EXPIRE.lua
if (redis.call('setnx', KEYS[1], ARGV[1]) == 1)
then
redis.call('expire', KEYS[1], ARGV[2])
return true
else
return false
end
# COMPARE_DELETE.lua
if (redis.call('get', KEYS[1]) == ARGV[1])
then
return redis.call('del', KEYS[1]) == 1
end
return false
package com.github.rxyor.distributed.lock;
import com.github.rxyor.common.util.FileUtil;
import com.github.rxyor.common.util.RandomUtil;
import com.github.rxyor.redis.util.LettuceConnectionUtil;
import io.lettuce.core.RedisClient;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import java.util.Objects;
import java.util.Optional;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
/**
*<p>
*Redis 分布式锁
*</p>
*
* @author liuyang
* @date 2019-05-14 Tue 18:51:00
* @since 1.0.0
*/
public class RedisDistributedLock implements DistributedLock {
/**
* taskId本地线程缓存
*/
private static final ThreadLocal<String> LOCAL_TASK_IDS = new ThreadLocal<>();
/**
* redis key本地线程缓存
*/
private static final ThreadLocal<String> LOCAL_KEYS = new ThreadLocal<>();
/**
* 默认锁失效时间
*/
private static final long DEFAULT_TIMEOUT = 5L;
/**
* 加锁LUA脚本
*/
private static final String LUA_LOCK_SCRIPT;
/**
* 释放锁LUA脚本
*/
private static final String LUA_UNLOCK_SCRIPT;
static {
LUA_LOCK_SCRIPT = readLuaLockScript();
LUA_UNLOCK_SCRIPT = readLuaUnlockScript();
}
/**
* redis 客户端
*/
private RedisClient redisClient;
/**
* redis key前缀
*/
@Getter
@Setter
private String keyPrefix;
public RedisDistributedLock(RedisClient redisClient) {
Objects.requireNonNull(redisClient, "redisClient can't be null");
this.redisClient = redisClient;
}
/**
* 获取锁
*
* @param redisKey
* @param timeout 超时时间(秒)
* @return 获取锁结果
*/
@Override
public boolean getLock(String redisKey, Long timeout) {
long expire = (timeout == null || timeout <= 0L ? DEFAULT_TIMEOUT : timeout);
String redisKeyWithPrefix = this.gainRedisKey(redisKey);
LOCAL_KEYS.set(redisKeyWithPrefix);
String taskId = this.generateTaskId();
LOCAL_TASK_IDS.set(taskId);
return evalRedisScript(LUA_LOCK_SCRIPT, new String[]{redisKeyWithPrefix},
new String[]{taskId, String.valueOf(expire)});
}
/**
* 释放锁
*
* @param redisKey
* @return 释放结果
*/
@Override
public boolean releaseLock(String redisKey) {
String redisKeyWithPrefix = this.gainRedisKey(redisKey);
boolean success = evalRedisScript(LUA_UNLOCK_SCRIPT, new String[]{redisKeyWithPrefix},
LOCAL_TASK_IDS.get());
if (success) {
this.clean();
}
return success;
}
@Override
public boolean releaseLock() {
String redisKeyWithPrefix = this.gainRedisKey(null);
boolean success = evalRedisScript(LUA_UNLOCK_SCRIPT, new String[]{redisKeyWithPrefix},
LOCAL_TASK_IDS.get());
if (success) {
this.clean();
}
return success;
}
/**
* 执行lua脚本
*
* @param script 脚本
* @param keys redis key
* @param values redis参数
* @return 执行结果
*/
private boolean evalRedisScript(String script, String[] keys, String... values) {
StatefulRedisConnection<String, String> conn = null;
try {
conn = LettuceConnectionUtil.getConnection(redisClient);
RedisCommands<String, String> commands = conn.sync();
return commands.<Boolean>eval(script, ScriptOutputType.BOOLEAN, keys, values);
} finally {
LettuceConnectionUtil.releaseConnection(conn);
}
}
/**
* 拼装或从缓存中取redis key
*
* @param key 原始redis key
* @return String
*/
private String gainRedisKey(String key) {
if (StringUtils.isNotEmpty(key)) {
String redisKeyPrefix = Optional.ofNullable(keyPrefix).orElse("");
return redisKeyPrefix + key;
}
return Optional.ofNullable(LOCAL_KEYS.get()).filter(s -> StringUtils.isNotEmpty(s))
.orElseThrow(() -> new IllegalArgumentException("redis key can't be empty"));
}
/**
* 生成一个唯一的TaskId(UUID)
* @return UUID
*/
private String generateTaskId() {
return RandomUtil.createUuid();
}
/**
* 清空ThreadLocal缓存
*/
private void clean() {
LOCAL_KEYS.remove();
LOCAL_TASK_IDS.remove();
}
/**
* 读取加锁LUA脚本
* @return String
*/
private static String readLuaLockScript() {
return FileUtil.readTextFromResource(RedisDistributedLock.class, "/lua/SETNX_EXPIRE.lua");
}
/**
* 读取释放锁LUA脚本
* @return String
*/
private static String readLuaUnlockScript() {
return FileUtil.readTextFromResource(RedisDistributedLock.class, "/lua/COMPARE_DELETE.lua");
}
}