SpringBoot2.0以后,redis 的库替换为了lettuce
本文主要转载一些网上比较可靠的实现方式,希望对大家有帮助
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ide{
/**
* 设置请求锁定时间,超时后自动释放锁
* @return
*/
int lockTime() default 10;
}
AOP 实现 注解 @Ide
的拦截处理
/**
* 接口幂等性的 -- 分布式锁实现
*/
@Slf4j
@Aspect
@Component
public class ReqSubmitAspect {
@Autowired
private RedisLock redisLock;
@Pointcut("@annotation(com.laiease.common.annotation.Ide)")
public void idePointCut() {
}
@Around("idePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 使用分布式锁 机制-实现
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Ide ide = method.getAnnotation(Ide.class);
int lockSeconds = ide.lockTime();
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
AssertUtils.notNull(request, "request can not null");
// 获取请求的凭证,本项目中使用的JWT,可对应修改
String token = request.getHeader("Token");
String requestURI = request.getRequestURI();
String key = getIdeKey(token, requestURI);
String clientId = CmUtil.getUUID();
// 获取锁
boolean lock = redisLock.tryLock(key, clientId, lockSeconds);
log.info("tryLock key = [{}], clientId = [{}]", key, clientId);
if (lock) {
log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 获取锁成功
Object result;
try {
// 执行进程
result = joinPoint.proceed();
} finally {
// 解锁
redisLock.releaseLock(key, clientId);
log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
log.info("tryLock fail, key = [{}]", key);
throw new RuntimeException("重复请求,请稍后再试!");
}
}
private String getIdeKey(String token, String requestURI) {
return token + requestURI;
}
}
redis 分布式锁工具类
@Component
public class RedisLock {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
// 当前设置 过期时间单位, EX = seconds; PX = milliseconds
private static final String SET_WITH_EXPIRE_TIME = "EX";
//lua
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public boolean tryLock(String lockKey, String clientId, long seconds) {
return redisTemplate.execute((RedisCallback) redisConnection -> {
// Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object nativeConnection = redisConnection.getNativeConnection();
RedisSerializer stringRedisSerializer = (RedisSerializer) redisTemplate.getKeySerializer();
byte[] keyByte = stringRedisSerializer.serialize(lockKey);
byte[] valueByte = stringRedisSerializer.serialize(clientId);
// lettuce连接包下 redis 单机模式
if (nativeConnection instanceof RedisAsyncCommands) {
RedisAsyncCommands connection = (RedisAsyncCommands) nativeConnection;
RedisCommands commands = connection.getStatefulConnection().sync();
String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
if (LOCK_SUCCESS.equals(result)) {
return true;
}
}
// lettuce连接包下 redis 集群模式
if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
RedisAdvancedClusterAsyncCommands connection = (RedisAdvancedClusterAsyncCommands) nativeConnection;
RedisAdvancedClusterCommands commands = connection.getStatefulConnection().sync();
String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
if (LOCK_SUCCESS.equals(result)) {
return true;
}
}
if (nativeConnection instanceof JedisCommands) {
JedisCommands jedis = (JedisCommands) nativeConnection;
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
}
return false;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public boolean releaseLock(String lockKey, String clientId) {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(RELEASE_LOCK_SCRIPT);
redisScript.setResultType(Integer.class);
// Integer execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), clientId);
Object execute = redisTemplate.execute((RedisConnection connection) -> connection.eval(
RELEASE_LOCK_SCRIPT.getBytes(),
ReturnType.INTEGER,
1,
lockKey.getBytes(),
clientId.getBytes()));
if (RELEASE_SUCCESS.equals(execute)) {
return true;
}
return false;
}
}
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
package com.hz.tgb.data.redis.lock;
import cn.hutool.core.util.IdUtil;
import com.hz.tgb.entity.Book;
import com.hz.tgb.spring.SpringUtils;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Supplier;
/**
* Redis分布式锁 - 集群版
*
* @author hezhao on 2019.11.13
*/
@Component
public class RedisClusterLockUtil {
/*
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
*/
private static final Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);
private static RedisTemplate cacheTemplate;
/** OK: Redis操作是否成功 */
private static final String REDIS_OK = "OK";
/** CONN_NOT_FOUND: Redis链接类型不匹配 */
private static final String REDIS_CONN_NOT_FOUND = "CONN_NOT_FOUND";
/** 解锁是否成功 */
private static final Long RELEASE_SUCCESS = 1L;
/** 解锁Lua脚本 */
private static final String UNLOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* The number of nanoseconds for which it is faster to spin
* rather than to use timed park. A rough estimate suffices
* to improve responsiveness with very short timeouts.
*/
private static final long spinForTimeoutThreshold = 1000000L;
/**
* 加锁
* @param lockKey 锁键
* @param requestId 请求唯一标识
* @param expireTime 缓存过期时间
* @param unit 时间单位
* @return true: 加锁成功, false: 加锁失败
*/
@SuppressWarnings("all")
public static boolean lock(String lockKey, String requestId, long expireTime, TimeUnit unit) {
// 加锁和设置过期时间必须是原子操作,否则在高并发情况下或者Redis突然崩溃会导致数据错误。
try {
// 以毫秒作为过期时间
long millisecond = TimeoutUtils.toMillis(expireTime, unit);
String result = execute(connection -> {
Object nativeConnection = connection.getNativeConnection();
RedisSerializer
分享基于redis一个分布式锁实现,
特点:
1/ 非重入,等待锁时使用线程sleep
2/使用 redis的 SETNX 带过期时间的方法
3/使用ThreadLocal保存锁的值,在锁超时时,防止删除其他线程的锁,使用lua 脚本保证原子性;
实现如下,欢迎提出指正:
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.util.Assert;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
/**
* 只支持springboot2 以后的Redis分布式锁(lettuce底层,不支持jedis)
* 不支持重入
*
* 经过测试,在本地redis情况下,一次lock和releaseLock 总花费约3ms
*/
public class RedisLock extends AbstractLock {
private RedisTemplate redisTemplate;
private ThreadLocal lockValue = new ThreadLocal<>();
private final Logger logger = LoggerFactory.getLogger(RedisLock.class);
private static final String REDIS_LIB_MISMATCH = "Failed to convert nativeConnection. " +
"Is your SpringBoot main version > 2.0 ? Only lib:lettuce is supported.";
private static final String UNLOCK_LUA;
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 ");
UNLOCK_LUA = sb.toString();
}
public RedisLock(RedisTemplate redisTemplate) {
Assert.notNull(redisTemplate,"redisTemplate should not be null.");
this.redisTemplate = redisTemplate;
}
/**
* 加锁
* @param key
* @param expireSeconds
* @param retryTimes
* @param sleepMillis
* @return
*/
@Override
public boolean lock(String key, long expireSeconds, int retryTimes, long sleepMillis) {
boolean result = tryLock(key, expireSeconds);
while((!result) && retryTimes-- > 0){
try {
logger.debug("Lock failed, retrying..." + retryTimes);
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
return false;
}
result = tryLock(key, expireSeconds);
}
return result;
}
/**
* 尝试Lock
* @param key
* @param expireSeconds
* @return
*/
@SuppressWarnings("unchecked")
private boolean tryLock(String key, long expireSeconds) {
String uuid = UUID.randomUUID().toString();
try {
String result = redisTemplate.execute(new RedisCallback() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
try{
Object nativeConnection = connection.getNativeConnection();
byte[] keyByte = key.getBytes(StandardCharsets.UTF_8);
byte[] valueByte = uuid.getBytes(StandardCharsets.UTF_8);
String resultString = "";
if(nativeConnection instanceof RedisAsyncCommands){
RedisAsyncCommands command = (RedisAsyncCommands) nativeConnection;
resultString = command
.getStatefulConnection()
.sync()
.set(keyByte, valueByte, SetArgs.Builder.nx().ex(expireSeconds));
}else if(nativeConnection instanceof RedisAdvancedClusterAsyncCommands){
RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
resultString = clusterAsyncCommands
.getStatefulConnection()
.sync()
.set(keyByte, keyByte, SetArgs.Builder.nx().ex(expireSeconds));
}else{
logger.error(REDIS_LIB_MISMATCH);
}
return resultString;
}catch (Exception e){
logger.error("Failed to lock, closing connection",e);
closeConnection(connection);
return "";
}
}
});
boolean eq = "OK".equals(result);
if(eq) {
lockValue.set(uuid);
}
return eq;
} catch (Exception e) {
logger.error("Set redis exception", e);
return false;
}
}
/**
* 释放锁
* 有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
* 使用lua脚本删除redis中匹配value的key
* @param key
* @return false: 锁已不属于当前线程 或者 锁已超时
*/
@SuppressWarnings("unchecked")
@Override
public boolean releaseLock(String key) {
try {
String lockValue = this.lockValue.get();
if(lockValue==null){
return false;
}
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
byte[] valueBytes = lockValue.getBytes(StandardCharsets.UTF_8);
Object[] keyParam = new Object[]{keyBytes};
Long result = redisTemplate.execute(new RedisCallback() {
public Long doInRedis(RedisConnection connection) throws DataAccessException {
try{
Object nativeConnection = connection.getNativeConnection();
if (nativeConnection instanceof RedisScriptingAsyncCommands) {
/**
* 不要问我为什么这里的参数这么奇怪
*/
RedisScriptingAsyncCommands command = (RedisScriptingAsyncCommands) nativeConnection;
RedisFuture future = command.eval(UNLOCK_LUA, ScriptOutputType.INTEGER, keyParam, valueBytes);
return getEvalResult(future,connection);
}else{
logger.warn(REDIS_LIB_MISMATCH);
return 0L;
}
}catch (Exception e){
logger.error("Failed to releaseLock, closing connection",e);
closeConnection(connection);
return 0L;
}
}
});
return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock exception", e);
}
return false;
}
private Long getEvalResult(RedisFuture future,RedisConnection connection){
try {
Object o = future.get();
return (Long)o;
} catch (InterruptedException |ExecutionException e) {
logger.error("Future get failed, trying to close connection.", e);
closeConnection(connection);
return 0L;
}
}
private void closeConnection(RedisConnection connection){
try{
connection.close();
}catch (Exception e2){
logger.error("close connection fail.", e2);
}
}
/**
* 查看是否加锁
* @param key
* @return
*/
@Override
public boolean isLocked(String key) {
Object o = redisTemplate.opsForValue().get(key);
return o!=null;
}
}
https://www.jianshu.com/p/fbedd907a48a
https://blog.csdn.net/sinat_27403673/article/details/105775846
http://www.manongjc.com/article/65529.html