目录
首先项目集成redis,并把redisTemplate bean注入
DistributedLock注解
import java.lang.annotation.*;
/**
* 分布式锁
* 设计思路:
* 1、DistributedLock通过注解方式,简化分布式锁在业务上的使用
* 2、分布式锁有很多实现,通过接口定义,实现可扩展。本次用redis实现。并通过lua脚本确保上锁和释放锁是一个事务内的操作。
* 3、通过切面${@link LockMethodAspect}实现加锁和释放锁
* 4、未来根据需求交互可以实现是否等待锁,而不是拿不到锁就立即放回失败(false)
* 用法:
* 1、在对应的service或者dao层使用${@link DistributedLock}注解即可。尽量保证该service和dao层操作的原子性。
* 2、参考${@link LgPaymentProviderImpl#lockGetPaymentDto}
* 注意点:
* 使用分布式锁注解的方法要满足切面的要求,例如不能和调用该方法的方法在一个类中,方法的访问权限不能是private的
*
* @author: fengz1
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DistributedLock {
/**
* Spring Expression Language (SpEL)表达式
* 并发锁Key的前缀bfb:lock:${方法名} 拼上key(),作为redis的Key。
* 示例:
* 1、@DistributedLock。 此时key="nwms:lock:default"
* 2、@DistributedLock(key = "'other'+#user.userId")。 若userId=1,此时key="nwms:lock:other1"
*
*
* @return
*/
String key() default "'default'";
/**
* 锁的过期秒数,默认是5秒
*
* @return
*/
int expire() default 5;
/**
* 获取锁的重试间隔时间(单位毫秒)
*
* @return
*/
int tryLock() default 100;
}
LockMethodAspect 切面
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.ThreadPoolExecutor;
import static org.apache.commons.lang3.StringUtils.*;
/**
* 基于redis的分布式锁的切面
*
* @author: fengz1
* @create: 2022-10-20 08:49
**/
@Aspect
@Component
@Order(0)
public class LockMethodAspect {
Logger LOG = LoggerFactory.getLogger("default");
static final String KEY_FORMAT_DISTRIBUTED_LOCK = "nwms:lock:";
private IDistributedLock distributedLock;
private RedisTemplate<String, String> redisTemplate;
private ThreadPoolTaskExecutor refreshLockExecutor;
@Autowired
@Qualifier(value = "redisLock")
public void setDistributedLock(IDistributedLock distributedLock) {
this.distributedLock = distributedLock;
}
@Autowired
public void setRedisTemplate(
RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Autowired
public void setObservePaymentStatusExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("ObservePaymentStatus");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
this.refreshLockExecutor = executor;
}
@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
StandardEvaluationContext context = new StandardEvaluationContext(joinPoint.getArgs());
setContextVariables(context, joinPoint);
String keyItem = "default";
if (isNotBlank(distributedLock.key())) {
keyItem = new SpelExpressionParser().parseExpression(distributedLock.key()).getValue(context, String.class);
}
keyItem = KEY_FORMAT_DISTRIBUTED_LOCK + keyItem;
String value = UUID.randomUUID().toString();
try {
LOG.info("DistributedLock[key=" + JSON.toJSONString(keyItem) + "] execute lock");
boolean isLock = this.distributedLock.lock(keyItem, value, distributedLock.expire());
if (!isLock) {
long startTime = System.currentTimeMillis();
while (!isLock) {
try {
Thread.sleep(distributedLock.tryLock());
}
catch (Exception ignored) {
}
if (System.currentTimeMillis() - startTime > 10000) {
throw new RuntimeException("get key Exception.");
}
isLock = this.distributedLock.lock(keyItem, value, distributedLock.expire());
}
}
try {
refreshLockExecutor
.execute(new refreshLockTask(keyItem, value, distributedLock.expire(), redisTemplate));
return joinPoint.proceed();
}
catch (Throwable throwable) {
LOG.error("DistributedLock[key=" + JSON.toJSONString(keyItem) + "] execute error: ", throwable);
throw new RuntimeException();
}
}
finally {
LOG.info("DistributedLock[key=" + JSON.toJSONString(keyItem) + "] execute unlock");
this.distributedLock.unlock(keyItem, value);
}
}
private void setContextVariables(StandardEvaluationContext standardEvaluationContext, JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
String[] parametersName = new LocalVariableTableParameterNameDiscoverer().getParameterNames(targetMethod);
if (parametersName != null) {
for (int i = 0; i < args.length; i++) {
standardEvaluationContext.setVariable(parametersName[i], args[i]);
}
}
}
}
RedisLock 锁实现(lua脚本)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
/**
* redis分布式锁实现
* @author: fengz1
* @create: 2020-10-20 09:48
**/
@Component
public class RedisLock implements IDistributedLock {
public static final int DEFAULT_SECOND_LEN = 10;
private static final String LOCK_LUA =
"if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('expire', KEYS[1], ARGV[2]) return 'true' else return 'false' end";
private static final String UNLOCK_LUA =
"if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) end return 'true' ";
private static final String LIST_LOCK_LUA =
"for key,value in ipairs(KEYS)\n"
+ "do\n"
+ " if redis.call('setnx',value, ARGV[1]) ~= 1 then return 'false' else redis.call('expire', value, ARGV[2]) \n"
+ " end end return 'true'";
private static final String List_UNLOCK_LUA =
"for key,value in ipairs(KEYS)\n"
+ "do\n"
+ " if redis.call('get', value) == ARGV[1] then redis.call('del', value) else return 'false' \n"
+ " end end return 'true'";
private RedisScript lockRedisScript;
private RedisScript unLockRedisScript;
private RedisScript lockListRedisScript;
private RedisScript unLockListRedisScript;
private RedisSerializer<String> argsSerializer;
private RedisSerializer<String> resultSerializer;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 初始化lua 脚本
*/
@PostConstruct
public void init() {
argsSerializer = new StringRedisSerializer();
resultSerializer = new StringRedisSerializer();
lockRedisScript = RedisScript.of(LOCK_LUA, String.class);
unLockRedisScript = RedisScript.of(UNLOCK_LUA, String.class);
lockListRedisScript = RedisScript.of(LIST_LOCK_LUA, String.class);
unLockListRedisScript = RedisScript.of(List_UNLOCK_LUA, String.class);
}
@Override
public boolean lock(String lock, String val) {
return this.lock(lock, val, DEFAULT_SECOND_LEN);
}
@Override
public boolean lock(String lock, String val, int second) {
List<String> keys = Collections.singletonList(lock);
String flag = redisTemplate.execute(lockRedisScript, argsSerializer, resultSerializer, keys, val, String.valueOf(second));
return Boolean.parseBoolean(flag);
}
@Override
public void unlock(String lock, String val) {
List<String> keys = Collections.singletonList(lock);
redisTemplate.execute(unLockRedisScript, argsSerializer, resultSerializer, keys, val);
}
@Override
public boolean lockList(List<String> locks, String val, int second) {
String flag = redisTemplate.execute(lockListRedisScript, argsSerializer, resultSerializer, locks, val, String.valueOf(second));
return Boolean.parseBoolean(flag);
}
@Override
public void unlockList(List<String> locks, String val) {
redisTemplate.execute(unLockListRedisScript, argsSerializer, resultSerializer, locks, val);
}
}
refreshLockTask 锁续约任务
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
class refreshLockTask implements Runnable {
Logger LOG = LoggerFactory.getLogger("default");
private final String key;
private final String value;
private final int expireTime;
private RedisTemplate<String, String> redisTemplate;
/**
* 线程延迟执行时间,单位是毫秒。偏移2/3*expireTime
*/
private int delayExeTime;
public refreshLockTask(String key, String value, int expireTime, RedisTemplate<String, String> redisTemplate) {
this.key = key;
this.value = value;
this.expireTime = expireTime;
this.delayExeTime = expireTime * 1000 * 2 / 3;
this.redisTemplate = redisTemplate;
}
@Override
public void run() {
try {
while (true) {
Thread.sleep(delayExeTime);
String valueCache = redisTemplate.opsForValue().get(key);
if (StringUtils.equals(valueCache, value)) {
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
delayExeTime = expireTime * 1000;
}
else {
break;
}
}
}
catch (Exception e) {
LOG.error("Refresh redis key error", e);
}
}
}