遇到个需要使用分布式锁的场景,觉得每个方法都去加锁麻烦,使用切面实现一下。记录一下实现方式。
1. redis分布式锁
引用 基于redisTemplate的redis的分布式锁正确打开方式 文章代码:
@Component
@Slf4j
public class RedisDistributedLock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public 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 boolean setLock(String key,String value, long expire) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
//String uuid = UUID.randomUUID().toString();
return commands.set(key, value, "NX", "PX", expire);
};
String result = redisTemplate.execute(callback);
return !Utils.isEmpty(result);
} catch (Exception e) {
log.error("set redis occured an exception", e);
}
return false;
}
public String get(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
log.error("get redis occured an exception", e);
}
return "";
}
public boolean releaseLock(String key,String value) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(value);
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
};
Long result = redisTemplate.execute(callback);
return result != null && result > 0;
} catch (Exception e) {
log.error("release lock occured an exception", e);
} finally {
// 清除掉ThreadLocal中的数据,避免内存溢出
//lockFlag.remove();
}
return false;
}
}
2. 实现切面方式 (注解方式):
a. 实现锁接口
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestLock {
/**
* key 注解使用方法上,传入redis的key
*/
@NotNull
String lockKey();
}
b. 实现切面方式:
第一种:反射机制实现:
@Aspect
@Component
@Slf4j
public class TestAspect {
@Resource
RedisDistributedLock redisDistributedLock;
//已注解为切点
@Pointcut("@annotation(XXX.TestLock)")
public void TestLock() {
}
@Around(value = "TestLock()")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
Object target = joinPoint.getTarget();
String methodName = joinPoint.getSignature().getName();
// 获取注解
Method method = getAccessibleMethodByName(target, methodName);
TestLock TestLock = method.getAnnotation(TestLock.class);
if(Utils.isBlank(TestLock)){
return result;
}
//获取注解参数
String key = TestLock.lockKey();
key = getRedisKey(key, joinPoint);
String value = UUID.randomUUID().toString();
long expireTime = 60000*3L;
//设置锁
boolean lock = redisDistributedLock.setLock(RedisKey.REDIS_KEY_LOCK + key,value, expireTime);
try {
if (!lock) {
log.info("key:{}", key);
return result;
}
result = joinPoint.proceed();
}catch (Exception e) {
log.error(key+":\\n\"" + e.getMessage());
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
redisDistributedLock.releaseLock(RedisKey.REDIS_KEY_LOCK + key,value);
}
return result;
}
//根据注解中的参数,获取方法参数中对应的属性值 ,如果对应属性中的值为空则返回注解中的参数key
public String getRedisKey(String key,ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
if(Utils.isBlank(args)){
return key;
}
Object ob1 = args[0];
if(Utils.isBlank(args)){
return key;
}
Object value = getValue(ob1, key);
if(Utils.isBlank(value)){
return key;
}
return value.toString();
}
/**
* 获取对象对应的属性值
*/
private Object getValue(Object class1,String key){
Class<?> clazz1 = class1.getClass();
Field[] field1 = clazz1.getDeclaredFields();
try {
for (Field aField1 : field1) {
aField1.setAccessible(true);
if (aField1.getName().equals(key)) {
return aField1.get(class1);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
return null;
}
/**
* 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. 如向上转型到Object仍无法找到, 返回null. 只匹配函数名。
* 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object...
* args)
*/
public static Method getAccessibleMethodByName(final Object obj, final String methodName) {
Validate.notNull(obj, "object can't be null");
Validate.notBlank(methodName, "methodName can't be blank");
for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) {
Method[] methods = searchType.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
makeAccessible(method);
return method;
}
}
}
return null;
}
/**
* 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
*/
public static void makeAccessible(Method method) {
if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !method.isAccessible()) {
method.setAccessible(true);
}
}
}
第二种:使用表达式实现
@Aspect
@Component
@Slf4j
public class TestAspect {
@Resource
RedisDistributedLock redisDistributedLock;
//已注解为切点
@Pointcut("@annotation(XXX.TestLock)")
public void TestLock() {
}
@Around(value = "TestLock()")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
Object target = joinPoint.getTarget();
String methodName = joinPoint.getSignature().getName();
// 获取注解
Method method = getAccessibleMethodByName(target, methodName);
TestLock testLock = method.getAnnotation(TestLock.class);
if(Utils.isBlank(testLock)){
return result;
}
//获取注解参数
String key = testLock.lockKey();
//表达式方式实现
if(!Utils.isBlank(joinPoint.getArgs())) {
Expression expression = getExpression(joinPoint, testLock);
Object expressionValue = expression.getValue(joinPoint.getArgs()[0]);
if(!Utils.isBlank(expressionValue)) {
key = expressionValue.toString();
}
}
String value = UUID.randomUUID().toString();
long expireTime = 60000*3L;
//设置锁
boolean lock = redisDistributedLock.setLock(RedisKey.REDIS_KEY_LOCK + key,value, expireTime);
try {
if (!lock) {
log.info("key:{}", key);
return result;
}
result = joinPoint.proceed();
}catch (Exception e) {
log.error(key+":\\n\"" + e.getMessage());
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
redisDistributedLock.releaseLock(RedisKey.REDIS_KEY_LOCK + key,value);
}
return result;
}
private static final ConcurrentHashMap<String, Expression> EL_EXPRESSION_CACHE = new ConcurrentHashMap<>();
//表达式值获取
private Expression getExpression(ProceedingJoinPoint joinPoint, TestLock idempotent) {
String key = joinPoint.getSignature().getName();
Expression exp = EL_EXPRESSION_CACHE.get(key);
if (exp == null) {
synchronized (EL_EXPRESSION_CACHE) {
exp = EL_EXPRESSION_CACHE.get(key);
if (exp == null) {
String idempotentIdEl = idempotent.lockKey();
ExpressionParser parser = new SpelExpressionParser();
exp = parser.parseExpression(idempotentIdEl);
EL_EXPRESSION_CACHE.put(key, exp);
}
}
}
return exp;
}
/**
* 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. 如向上转型到Object仍无法找到, 返回null. 只匹配函数名。
* 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object...
* args)
*/
public static Method getAccessibleMethodByName(final Object obj, final String methodName) {
Validate.notNull(obj, "object can't be null");
Validate.notBlank(methodName, "methodName can't be blank");
for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) {
Method[] methods = searchType.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
makeAccessible(method);
return method;
}
}
}
return null;
}
/**
* 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
*/
public static void makeAccessible(Method method) {
if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !method.isAccessible()) {
method.setAccessible(true);
}
}
}
3. 使用方式
/**
* 注解在方法上
* systemUser
*
**/
@ScheduleLock(lockKey = "userId")
public void Test(SystemUser systemUser) {
createReportService.createPaymentTable();
}
//假设现SystemUser为下面所示
public class SystemUser(){
private String userId;
}
lockKey: 传入redis锁,该值可以直接是需要在redis加锁时候的key值,也可以设置为传入参数中的某个属性值,
现假设systemUser中存在userId,并且userId为“123”,此时redis加锁时候的key值为“123”;
假设systemUser不存在或者不存在userId的属性,此时redis加锁时候的key值为“userId”;
当传入参数为更复杂的对应时,反射获取参数的方式将不适用,用表达式方式可以获取,如下:
/**
* 注解在方法上
* systemUser
*
**/
@ScheduleLock(lockKey = "systemUser1.userId")
public void Test(SystemUser systemUser) {
createReportService.createPaymentTable();
}
//假设现SystemUser为下面所示
public class SystemUser(){
private String userId;
private SystemUser systemUser1;
}