提示:以下是本篇文章正文内容,下面案例可供参考
一、注解
注解方式侵入性更低,基本上不需要修改原生代码,只需要加注解就行
代码如下(示例):
public @interface LockAnnotation {
/**
* 加锁的key的前缀
*
* @return
*/
String lockPrefix() default "";
/**
* 加锁的key的值
*
* @return
*/
String lockKey();
/**
* 加锁的key的后缀
*
* @return
*/
String lockSuffix() default "";
/**
* 锁自动释放时间,单位s
*
* @return
*/
int lockTime() default 3;
/**
* 获取锁的最大等待时间,单位s,默认不等待,0即为快速失败
*
* @return
*/
int waitTime() default 0;
/**
* 未获取锁提示消息
*
* @return
*/
String failMessage() default "这会儿人真的有点多:( 请稍等一下下";
/**
* 未获取锁是否提示消息
*
* @return 默认通知
*/
boolean remindFailMessage() default true;
}
二、切面的实现@Aspect
代码如下(示例):
@Aspect
@Component
public class LockAspect {
private static Logger logger = LoggerFactory.getLogger(LockAspect.class);
@Resource
private Lock lock;
/**
* 环绕增强,能控制切点执行前,执行后
*/
@Around("@annotation(lockAnnotation)")
public Object lockAround(ProceedingJoinPoint joinPoint, LockAnnotation lockAnnotation) throws Throwable {
//获取连接点方法运行时的入参列表
Object[] args = joinPoint.getArgs();
// 通过joinPoint获取被注解方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//获取被拦截方法参数名列表
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
// 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
String[] params = discoverer.getParameterNames(signature.getMethod());
//SPEL上下文
EvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SPEL上下文中
assert params != null;
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], args[len]);
}
// 解析过后的Spring表达式对象
Expression expression = parser.parseExpression(lockAnnotation.lockKey());
// 表达式从上下文中计算出实际参数值
String lockKey = expression.getValue(context, String.class);
String lockPrefix = lockAnnotation.lockPrefix();
String lockSuffix = lockAnnotation.lockSuffix();
String key = lockPrefix + lockKey + lockSuffix;
int lockTime = Math.max(lockAnnotation.lockTime(), 1);
int waitTime = Math.max(lockAnnotation.waitTime(), 0);
String randomValue = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
long endTime = System.currentTimeMillis() + waitTime * 1000;
do {
if (lock.setLock(key, randomValue, lockTime)) {
logger.info("获得锁成功,方法名为{},参数为{}", joinPoint.getSignature(),
Lists.newArrayList(args).stream().map(obj -> JSONObject.toJSONString(ObjectUtils.defaultIfNull(obj, "null")))
.collect(Collectors.joining("-")));
try {
return joinPoint.proceed(args);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
boolean b = lock.releaseLock(lockKey, randomValue);
if (b) {
logger.info("Lock release lock success");
}
}
}
int sleepTime = Math.min(300, waitTime * 100);
logger.info("获锁失败,本次等待{}ms继续获取锁,方法名为{},参数为{}", sleepTime, joinPoint.getSignature(),
Lists.newArrayList(args).stream().map(obj -> JSONObject.toJSONString(ObjectUtils.defaultIfNull(obj, "null")))
.collect(Collectors.joining("-")));
Thread.sleep(ThreadLocalRandom.current().nextInt(sleepTime));
} while (System.currentTimeMillis() <= endTime);
logger.info("获得锁失败,之前共等待{}ms,方法将不执行,方法名为{},参数为{}", System.currentTimeMillis() - startTime, joinPoint.getSignature()
, Lists.newArrayList(args).stream().map(Object::toString)
.collect(Collectors.joining("-")));
if (lockAnnotation.remindFailMessage()) {
throw new RuntimeException(lockAnnotation.failMessage());
}
return null;
}
}
三、使用RedisConnection实现分布式锁
RedisConnection实现分布锁的方式,采用redisTemplate操作redisConnection实现setnx和setex两个命令连用.
代码如下(示例):
@Resource
public RedisTemplate<Object, Object> redisTemplate;
/**
* redisTemplate操作redisConnection实现setnx和setex两个命令连用
* 获得锁操作
*
* @param key 锁的Key
* @param value 锁里面的值
* @param expire 锁失效时间
* @return
*/
public boolean setLock(String key, String value, long expire) {
try {
Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.set(key.getBytes(), value.getBytes(), Expiration.seconds(expire), RedisStringCommands.SetOption.ifAbsent());
}
});
return result;
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
三、使用lua释放锁
之所以没有采用先get再del的操作,而是采用lua脚本是因为需要保持操作的原子性。
假设采用先get再del的方式,则get和del是分步执行的。那么如果要求a在执行del操作之前,
万一因为其他原因导致没有及时del,此时锁过期自动释放了,
这时请求b发现可以创建锁,就创建了锁。然后请求a突然又恢复正常去释放锁,
但此时锁的持有者是请求b,请求a误删了请求b持有的锁。
就会造成安全问题,因为Redis没有get和del合二为一的操作,
要解决该问题只能通过lua脚本将这两个操作合二为一,一起执行才行。
代码如下(示例):
private static Logger logger = LoggerFactory.getLogger(Lock.class);
private static final String UNLOCK = "unlock.lua";
private static final AtomicReference<String> DELOCK = new AtomicReference<>();
@Resource
public RedisTemplate<Object, Object> redisTemplate;
/**
* 释放锁操作
*
* @param key 锁的Key
* @param value 锁里面的值
* @return
*/
public boolean releaseLock(String key, String value) {
RedisScript<Boolean> lockScript = RedisScript.of(DELOCK.get(), Boolean.class);
// 封装参数
List<Object> keyList = new ArrayList<>();
keyList.add(key);
keyList.add(value);
return redisTemplate.execute(lockScript, keyList);
}
/**
* 在初始化bean的时候都会执行该方法
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
//加载lua
ClassPathResource resource = new ClassPathResource(UNLOCK);
String luaContent = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
//如果当前值 ==为预期值,则将luaContent设置为给定的更新值
DELOCK.compareAndSet(null, luaContent);
}
lua(示例):
local lockKey = KEYS[1]
local lockValue = KEYS[2]
-- get key
local result_1 = redis.call('get', lockKey)
if result_1 == lockValue
then
local result_2= redis.call('del', lockKey)
return result_2
else
return false
end
三、案列
完
感谢您的阅读
如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。