原理是利用redis的setnx命令
public interface RedisDistributedLock {
boolean lock(String key);
boolean lock(String key, long waitMillis);
boolean lock(String key, long waitMillis, long sleepMillis);
boolean lock(String key, long expire, long waitMillis, long sleepMillis);
boolean lock(String key, long expire, long waitMillis, long sleepMillis, int retries);
boolean release(String key);
}
实现类
package com.xxx.service.impl;
import com.xxx.service.RedisDistributedLock;
import io.lettuce.core.RedisFuture;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.async.RedisScriptingAsyncCommands;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Repository;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
@Repository
@Slf4j
public class RedisDistributedLockImpl implements RedisDistributedLock {
@Autowired
@Qualifier("objRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
/**
* 锁前缀
*/
private static final String ROOT_KEY = "lock:";
/**
* 过期时间,ms
*/
private static final long EXPIRE = 15000L;
/**
* 最长等待时间,ms
*/
private static final long WAIT_MILLIS = 10000L;
/**
* 重试等待时间,ms
*/
private static final long SLEEP_MILLIS = 500L;
/**
* 最多重试次数
*/
private static final int RETRIES = Integer.MAX_VALUE;
/**
* 最多重试次数
*/
private static final String OK = "OK";
/**
* 使用 ThreadLocal 存储 key 的 value 值,防止同步问题
*/
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final String REDIS_LIB_MISMATCH = "Failed to convert nativeConnection. Is your SpringBoot main version > 2.0 ? Only lib:lettuce is supported.";
/**
* 原子操作释放锁 Lua 脚本
*/
private static final String LUA_UNLOCK_SCRIPT;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get',KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call('del',KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
LUA_UNLOCK_SCRIPT = sb.toString();
}
@Override
public boolean lock(String key) {
return setLock(key, EXPIRE, WAIT_MILLIS, SLEEP_MILLIS, RETRIES);
}
@Override
public boolean lock(String key, long waitMillis) {
return setLock(key, EXPIRE, waitMillis, SLEEP_MILLIS, RETRIES);
}
@Override
public boolean lock(String key, long waitMillis, long sleepMillis) {
return setLock(key, EXPIRE, waitMillis, SLEEP_MILLIS, RETRIES);
}
@Override
public boolean lock(String key, long expire, long waitMillis, long sleepMillis) {
return setLock(key, expire, waitMillis, sleepMillis, RETRIES);
}
@Override
public boolean lock(String key, long expire, long waitMillis, long sleepMillis, int retries) {
return setLock(key, expire, waitMillis, sleepMillis, retries);
}
/**
* 获取 Redis 锁
*
* @param key 锁名称
* @param expire 锁过期时间
* @param retries 最多重试次数
* @param sleepMillis 重试等待时间
* @param waitMillis 最长等待时间
* @return
*/
private boolean setLock(String key, long expire, long waitMillis, long sleepMillis, int retries) {
//检查 key 是否为空
if (key == null || "".equals(key)) {
log.error("setLock error, key is empty");
return false;
}
try {
long startTime = System.currentTimeMillis();
key = ROOT_KEY + key;
//可重入锁判断
String v = threadLocal.get();
if (v != null && isReentrantLock(key, v)) {
return true;
}
//获取锁
String value = UUID.randomUUID().toString();
log.debug("setLock key:[{}], retries:[{}], value:[{}]", key, retries, value);
while (!this.setNx(key, value, expire)) {
//超过最大重试次数后获取锁失败
log.error("setLock error, key:[{}], retries:[{}]", key, retries);
if (retries-- < 1) {
return false;
}
//等待下一次尝试
Thread.sleep(sleepMillis);
//超过最长等待时间后获取锁失败
if (System.currentTimeMillis() - startTime > waitMillis) {
log.error("setLock error, key:[{}], retries:[{}], waitMillis:[{}]", key, retries, waitMillis);
return false;
}
}
threadLocal.set(value);
return true;
} catch (Exception e) {
log.error("redis lock get, key:{}, expire:{}, waitMillis:{}, sleepMillis:{}, retries: {}, error:{}",
key, expire, waitMillis, sleepMillis, retries, e.getMessage());
return false;
}
}
/**
* SET if Not exists
*/
private boolean setNx(String key, String value, long expire) {
Boolean resultBoolean = null;
try {
resultBoolean = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
Object nativeConnection = connection.getNativeConnection();
String redisResult = "";
@SuppressWarnings("unchecked")
RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
//lettuce the serialization key value under the connection package, otherwise you can't parse with the default ByteArrayCodec
byte[] keyByte = stringRedisSerializer.serialize(key);
byte[] valueByte = stringRedisSerializer.serialize(value);
// lettuce connection package redis stand-alone mode setnx
if (nativeConnection instanceof RedisAsyncCommands) {
RedisAsyncCommands commands = (RedisAsyncCommands)nativeConnection;
redisResult = commands
.getStatefulConnection()
.sync()
.set(keyByte, valueByte, SetArgs.Builder.nx().ex(expire));
}
// lettuce connection package redis cluster mode setnx
else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
redisResult = clusterAsyncCommands
.getStatefulConnection()
.sync()
.set(keyByte, keyByte, SetArgs.Builder.nx().ex(expire));
}else{
log.error(REDIS_LIB_MISMATCH);
}
return OK.equalsIgnoreCase(redisResult);
});
} catch (Exception e) {
log.error("redis lock setNX, key:{}, value:{}, expire:{}, error:{}", key, value, expire,e.getMessage());
}
return resultBoolean != null && resultBoolean;
}
/**
* 可重入锁判断
*/
private boolean isReentrantLock(String key, String v) {
ValueOperations kvValueOperations = redisTemplate.opsForValue();
String value = (String) kvValueOperations.get(key);
if (value == null) {
return false;
}
return v.equals(value);
}
/**
* 释放锁
*/
@Override
public boolean release(String key) {
if (key == null || "".equals(key)) {
return false;
}
key = ROOT_KEY + key;
String value = threadLocal.get();
threadLocal.remove();
try {
return deleteKey(key, value);
} catch (Exception e) {
log.error("redis lock release, key:{}, error:{}", key, e.getMessage());
}
return false;
}
/**
* 删除 redis key
* <p>集群模式和单机模式执行脚本方法一样,但没有共同的接口
* <p>使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而 redis 锁自动过期失效的时候误删其他线程的锁
*/
private boolean deleteKey(String key, String arg) {
if(arg==null){
return false;
}
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
byte[] valueBytes = arg.getBytes(StandardCharsets.UTF_8);
Object[] keyParam = new Object[]{keyBytes};
Long result = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
try{
Object nativeConnection = connection.getNativeConnection();
if (nativeConnection instanceof RedisScriptingAsyncCommands) {
RedisScriptingAsyncCommands<Object,byte[]> command = (RedisScriptingAsyncCommands<Object,byte[]>) nativeConnection;
RedisFuture future = command.eval(LUA_UNLOCK_SCRIPT, ScriptOutputType.INTEGER, keyParam, valueBytes);
return getEvalResult(future,connection);
}else{
log.error(REDIS_LIB_MISMATCH);
return 0L;
}
}catch (Exception e){
log.error("Failed to releaseLock, closing connection",e);
closeConnection(connection);
return 0L;
}
}
});
return result != null && result > 0;
}
private Long getEvalResult(RedisFuture future,RedisConnection connection){
try {
Object o = future.get();
return (Long)o;
} catch (InterruptedException | ExecutionException e) {
log.error("Future get failed, trying to close connection.", e);
closeConnection(connection);
return 0L;
}
}
private void closeConnection(RedisConnection connection){
try{
connection.close();
}catch (Exception e2){
log.error("close connection fail.", e2);
}
}
}
使用时先lock 再在finally里面release。