V1
tryLock(key){
if (( SETNX Key 1 ) == 1){ //@1
EXPIRE Key Seconds //@2
return true
}
return false
}
release(key){
DELETE Key
}
缺点:
- 当@1执行成功后,由于[业务系统宕机]导致@2没有设置成功,造成其他client永远无法上锁
- 当@1执行成功后,由于[redis宕机发生主从切换]导致@2没有设置成功,造成其他client永远无法上锁
- tryLock成功后由于业务执行异常或者gc导致EXPIRE 过期超时,另一个client获取到锁,造成多个客户端同时执行
- 在第三个缺点的基础上可能误删除另一个客户端上的锁
V2
tryLock(key,UniqId){
if (( SETNX Key UniqId ExpireSeconds) == 1){ //保证原子性
return true
}
return false
}
release(key,UniqId){
EVAL( //lua脚本保证原子性
//LuaScript
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
)
}
缺点:
- 这个方案redis单机情况下是目前最优的分布式锁方案,但是如果在Redis集群环境下依然存在问题:由于Redis集群数据同步为异步,假设在Master节点获取到锁后未完成数据同步情况下Master节点crash,此时在新的Master节点依然可以获取锁,所以多个Client同时获取到了锁,只能是业务代码做好幂等调用
java实现
package com.paulzhangcc.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.paulzhangcc.common.util.InetUtils.findFirstNonLoopbackAddress;
@Component
@ConditionalOnProperty(prefix = "lock.redis", name = "enabled", matchIfMissing = true)
public class RedisLock {
@Autowired
RedisTemplate<String, String> stringRedisTemplate;
private final static Logger logger = LoggerFactory.getLogger(RedisLock.class);
/**
* 单个锁有效期
*/
private static final int DEFAULT_SINGLE_EXPIRE_SECONDS = 1000;
/**
* 批量锁有效期
*/
private static final int DEFAULT_BATCH_EXPIRE_SECONDS = 1000;
/**
* 尝试间隔时间
*/
private static final int RETRY_INTERVAL_SECONDS = 100;
private static InetAddress inetAddress = findFirstNonLoopbackAddress();
public String generateUniqueValue() {
String host = "";
if (inetAddress != null) {
host = host + inetAddress.getHostAddress() + "-";
}
return host + UUID.randomUUID().toString().replaceAll("-", "");
}
protected int retryInterval() {
return RETRY_INTERVAL_SECONDS;
}
protected int singleLockExpireSeconds() {
return DEFAULT_SINGLE_EXPIRE_SECONDS;
}
protected int batchLockExpireSeconds() {
return DEFAULT_BATCH_EXPIRE_SECONDS;
}
protected boolean seeLockCompetition() {
return false;
}
public boolean tryLock(String key, String generateUniqueValue) {
return tryLock(key, generateUniqueValue, 0L, null);
}
public boolean tryLock(String key, String generateUniqueValue, long timeout, TimeUnit unit) {
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(generateUniqueValue)) {
throw new IllegalArgumentException();
}
try {
logger.info("lock key start:" + key + ",value:" + generateUniqueValue);
//系统计时器的当前值,以毫微秒为单位。
long nano = System.nanoTime();
do {
Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, generateUniqueValue, singleLockExpireSeconds(), TimeUnit.SECONDS);
if (setIfAbsent != null && setIfAbsent.booleanValue()) {
logger.info("lock key end:" + key + ",isSuccess:true");
return Boolean.TRUE;
} else if (logger.isDebugEnabled() || seeLockCompetition()) {
String desc = stringRedisTemplate.opsForValue().get(key);
logger.info("lock key: " + key + " locked by another business:" + desc);
}
if (timeout <= 0) {
break;
}
Thread.sleep(retryInterval());
} while ((System.nanoTime() - nano) < unit.toNanos(timeout));
} catch (Exception e) {
logger.error("lock key error:" + key, e);
}
logger.info("lock key end:" + key + ",isSuccess:false");
return Boolean.FALSE;
}
public boolean tryLock(List<String> keys, String generateUniqueValue) {
return tryLock(keys, generateUniqueValue, 0L, null);
}
public boolean tryLock(List<String> keys, String generateUniqueValue, long timeout, TimeUnit unit) {
if (keys == null || keys.size() == 0 || StringUtils.isEmpty(generateUniqueValue)) {
throw new IllegalArgumentException();
}
logger.info("lock keys start:" + keys + ",value:" + generateUniqueValue);
try {
Map<String, String> mkeys = new HashMap<>(8);
long nano = System.nanoTime();
for (String key : keys) {
mkeys.put(key, generateUniqueValue);
}
do {
Boolean multiSetIfAbsent = stringRedisTemplate.opsForValue().multiSetIfAbsent(mkeys);
if (multiSetIfAbsent != null && multiSetIfAbsent.booleanValue()) {
for (String key : keys) {
stringRedisTemplate.expire(key, batchLockExpireSeconds(), TimeUnit.SECONDS);
}
logger.info("lock keys end:" + keys + ",isSuccess:true");
return Boolean.TRUE;
} else if (logger.isDebugEnabled() || seeLockCompetition()) {
String desc = stringRedisTemplate.opsForValue().get(keys.get(0));
logger.info("lock keys: " + keys + " locked by another business:" + desc);
}
if (timeout <= 0) {
break;
}
Thread.sleep(retryInterval());
} while ((System.nanoTime() - nano) < unit.toNanos(timeout));
} catch (Exception e) {
logger.error("lock keys error:" + keys, e);
}
logger.info("lock keys end:" + keys + ",isSuccess:false");
return Boolean.FALSE;
}
public RedisScript<String> unlockScript = RedisScript.of(
"local v = ARGV[1]\n" +
"for i,key in pairs(KEYS) do\n" +
"\tlocal v1 = redis.call(\"get\",key)\n" +
"\tif (v == v1) \n" +
"\tthen\n" +
"\t\tredis.call(\"del\",key)\n" +
//"\t\tredis.log(redis.LOG_NOTICE,\"delete key=\"..key..\",value=\"..v1)\n" +
"\tend\n" +
"end ");
public void unLock(String key, String generateUniqueValue) {
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(generateUniqueValue)) {
throw new IllegalArgumentException();
}
List<String> keyList = new ArrayList();
keyList.add(key);
unLock(keyList, generateUniqueValue);
}
public void unLock(List<String> keys, String generateUniqueValue) {
if (keys == null || keys.size() == 0 || StringUtils.isEmpty(generateUniqueValue)) {
throw new IllegalArgumentException();
}
stringRedisTemplate.execute(unlockScript, keys, generateUniqueValue);
}
}
package com.paulzhangcc.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Enumeration;
/**
* @author paul
* @description
* @date 2019/3/13
*/
public class InetUtils {
private final static Logger logger = LoggerFactory.getLogger(InetUtils.class);
public static InetAddress findFirstNonLoopbackAddress() {
InetAddress result = null;
try {
int lowest = Integer.MAX_VALUE;
for (Enumeration<NetworkInterface> nics = NetworkInterface
.getNetworkInterfaces(); nics.hasMoreElements();) {
NetworkInterface ifc = nics.nextElement();
if (ifc.isUp()) {
logger.trace("Testing interface: " + ifc.getDisplayName());
if (ifc.getIndex() < lowest || result == null) {
lowest = ifc.getIndex();
}
else if (result != null) {
continue;
}
}
}
}
catch (IOException ex) {
logger.error("Cannot get first non-loopback address", ex);
}
if (result != null) {
return result;
}
try {
return InetAddress.getLocalHost();
}
catch (UnknownHostException e) {
logger.warn("Unable to retrieve localhost");
}
return null;
}
}