说到分布式,就必然涉及到分布式锁的概念,如何保证不同机器不同线程的分布式锁同步呢。
Redis是线程安全的,单线程操作的。
1、保持互斥性:
jedis.setnx(); 将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
public Long setnx(final String key, final String value) {
checkIsInMultiOrPipeline();
client.setnx(key, value);
return client.getIntegerReply();
}
2、防止死锁,则为资源设置一个失效时间;
jedis.expire();用来设置超时时间;
public Long expire(final String key, final int seconds) {
checkIsInMultiOrPipeline();
client.expire(key, seconds);
return client.getIntegerReply();
}
在多线程并发环境下,任何非原子性的操作,都可能导致问题。
这段代码中,如果设置过期时间前,redis实例崩溃,就无法设置过期时间。如果客户端没有正确的释放锁,那么该锁(永远不会过期),就永远不会被释放。
public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
Long result = jedis.setnx(key, uniqueId);
if (1 == result) {
//如果该redis实例崩溃,那就无法设置过期时间了
jedis.expire(key, expireTime);
}
}
实现方式,在value中传入失效时间。存在如下问题:
public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
long expireTs = System.currentTimeMillis() + expireTime;
// 锁不存在,当前线程加锁成果
if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
return true;
}
// 如果所已经存在了
// 获取当前所得value
// 如果value存在,且实现时间已经小于了当前时间
// 通过getSet方法,获取设置key值,并且为key设置新的失效时间
// 返回是否设置成功
String value = jedis.get(key);
//如果当前锁存在,且锁已过期
if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
//锁过期,设置新的过期时间
String oldValue = jedis.getSet(key, String.valueOf(expireTs));
if (oldValue != null && oldValue.equals(value)) {
// 多线程并发下,只有一个线程会设置成功
// 设置成功的这个线程,key的旧值一定和设置之前的key的值一致
return true;
}
}
// 其他情况,加锁失败
return true;
}
释放锁:jedis.del(key); 调用该方法直接解锁,但不是自己加锁的,也会被删除。
加锁 直接使用set命令同时设置唯一id和过期时间;
解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。
@Component
public class RedisUtils {
/**
* Only set the key if it does not already exist
*/
private static final String NXXX_NX = "NX";
/**
* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1 GB).
* @param key 保存的key值
* @param value 保存的value值
* @param expireTime 超时时间,单位毫秒
* @return true 保存成功 false 保存失败
*/
public static boolean setNX(String key, String value, long expireTime){
if(StringUtils.isEmpty(key) || expireTime <= 0){
return false;
}
try (Jedis jedis = jedisPool.getResource()){
String result = jedis.set(key, value, NXXX_NX, EXPIRE_TIME_UNITS_PX, expireTime);
return STATUS_CODE_REPLY_SUCCESS.equalsIgnoreCase(result);
}catch (Throwable t){
logger.error("[E1][redis setNX Throwable][key:{},value:{},time:{}]", key, value, expireTime, t);
return false;
}
}
}
// 源码
public String set(final String key, final String value, final String nxxx, final String expx,
final long time) {
checkIsInMultiOrPipeline();
client.set(key, value, nxxx, expx, time);
return client.getStatusCodeReply();
}
其中解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。
/**
* 释放锁
* @param key 锁的信息
* @return 是否释放锁成功
*/
public static boolean release(String key){
return RedisUtils.del(key, ThreadIdUtils.get());
}
/**
* 删除key的值为value的key
* @param key 保存的key值
* @param value 保存的value值
* @return true 删除成功 false 删除失败
*/
public static boolean del(String key, String value){
if(StringUtils.isEmpty(key)){
return false;
}
try (Jedis jedis = jedisPool.getResource()){
Long result = (Long)jedis.eval(LUA_DEL_KEY_VALUE, 1, key, value);
return result == 1;
}catch (Throwable t){
logger.error("[E1][redis del Throwable][key:{},value:{}]", key, value, t);
return false;
}
}
private final static String LUA_DEL_KEY_VALUE =
"if redis.call(\"get\",KEYS[1]) == ARGV[1]" // 如果key的value等于传输的value
+ " then"
+ " return redis.call(\"del\",KEYS[1])" // 删除key
+ " else"
+ " return -1" // 返回-1
+ " end";
参考文章