敖癸不是敖葵是敖guǐ

甲乙丙丁戊己庚辛壬癸

导航

一个注解搞定redis分布式锁

首先自定义注解:@DistributeLock

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 方法分布式锁注解
 *
 * @author 敖癸
 * @formatter:on
 * @since 2024/3/4
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

    /** redis锁 key名称,支持字符串模板:如:KfptAppName:Lock:{}, {}表示占位符, 由args中的参数替换 */
    String key();

    /** 模板参数,支持SpEL表达式, ex: {"#serialNo", "#serialNo.length()", "T(java.util.Random).nextInt()", "isTure?'success':'false'", "normal"} */
    String[] args() default {};

    /** 锁过期时间(毫秒),默认3秒 */
    long expire() default 3000;

    /** 尝试获取锁超时时间 */
    long timeout() default 0;

    /** 尝试获取锁间隔时间(ms) */
    int tryInterval() default 5;

    /** 方法执行完是否立刻释放锁?默认true,表示方法执行完毕立即释放,false表示等到expire过期后释放 */
    boolean isRelease() default true;
}

AOP切面拦截注解

import cn.hutool.core.lang.Opt;
import cn.hutool.core.thread.ThreadUtil;
import com.yc.kfpt.starter.common.util.AspectUtil;
import com.yc.kfpt.starter.redis.annotation.DistributedLock;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import java.util.concurrent.TimeUnit;

/**
 * 分布式锁注解切面
 *
 * @author 敖癸
 * @formatter:on
 * @since 2023/11/29
 */
@Slf4j
@Aspect
@RequiredArgsConstructor
public class DistributedLockAspect {

	private RedisTemplate<String,Object> redisTemplate;
    private static final String REMOVE_IF_VAL = "if (redis.call('GET', KEYS[1]) == ARGV[1]) then return redis.call('DEL', KEYS[1]) else return 0 end";

    /**
     * 拦截被@DistributedLock注解的方法
     *
     * @param joinPoint
     * @param distributedLock
     * @return java.lang.Object
     * @author 敖癸
     * @since 2024/3/29 - 10:45
     */
    @Around("@annotation(distributedLock)")
    public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        // 根据注解参数生成redis key
        String key = generateKey(joinPoint, distributedLock);
        // 当前时间戳
        long currentTimestamp = System.currentTimeMillis();
        // 获取锁
        if (acquireLock(distributedLock, key, currentTimestamp)) {
            try {
                return joinPoint.proceed();
            } finally {
                // 方法执行完成是否需要立即释放锁
                if (distributedLock.isRelease()) {
                    // 释放锁, 释放时校验value值是否是当前线程的时间戳, 防止因为锁过期误删除其它线程的锁
                    RedisScript<Boolean> redisScript = RedisScript.of(REMOVE_IF_VAL, Boolean.class);
                    this.redisTemplate.execute(redisScript, Collections.singletonList(key), currentTimestamp);
                }
            }
        } else {
            // 获取锁失败
            throw new RuntimeException("获取"+key+"锁失败");
        }
    }

    /**
     * 根据注解参数生成redis key
     *
     * @param joinPoint
     * @param distributedLock
     * @return java.lang.String
     * @author 敖癸
     * @since 2024/3/29 - 10:49
     */
    private String generateKey(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
        String key = AspectUtil.buildTemplate(joinPoint, distributedLock.key(), distributedLock.args());
        return Opt.ofBlankAble(key).orElseThrow(()->new RuntimeException("redis key 不能为空"));
    }

    /**
     * 获取锁
     *
     * @param distributedLock
     * @param key
     * @param currentTimestamp
     * @return java.lang.Boolean
     * @author 敖癸
     * @since 2024/3/29 - 10:50
     */
    private Boolean acquireLock(DistributedLock distributedLock, String key, long currentTimestamp) {
        // 锁过期时间(ms)
        long expire = distributedLock.expire();
        // 尝试获取锁的等待时间(ms)
        long timeout = distributedLock.timeout();
        // 尝试获取锁
        boolean tryLock = tryLock(key, currentTimestamp, expire);
        // 如果获取锁失败,且超时等待时间大于0,则睡眠distributedLock.tryInterval() ms后重试
        while (!tryLock && timeout > 0) {
            // 如果超过timeout时间还没获取到锁,则获取锁失败
            if (System.currentTimeMillis() - currentTimestamp > timeout) {
                break;
            }
            ThreadUtil.sleep(distributedLock.tryInterval());
            // 在次尝试获取锁
            tryLock = tryLock(key, currentTimestamp, expire);
        }
        return tryLock;
    }

    /**
     * 尝试获取锁
     *
     * @param key
     * @param currentTimestamp
     * @param expire
     * @return boolean
     * @author 敖癸
     * @since 2024/3/29 - 10:53
     */
    private boolean tryLock(String key, long currentTimestamp, long expire) {
        return redisTemplate.opsForValue().setIfAbsent(key, currentTimestamp,expire, TimeUnit.MILLISECONDS);
    }

}

依赖的工具类


import com.yc.kfpt.commons.util.StrUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.Arrays;

/**
 * @author 敖癸
 * @formatter:on
 * @since 2024/3/4
 */
public class AspectUtil {

    private static final ExpressionParser PARSER = new SpelExpressionParser();

    /**
     * 解析切点的上下文对象
     *
     * @param joinPoint aop切点
     * @return org.springframework.expression.EvaluationContext
     * @author 敖癸
     * @since 2024/3/5 - 17:05
     */
    public static EvaluationContext buildEvaluationContext(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切点的方法参数名称列表
        String[] argNames = signature.getParameterNames();
        EvaluationContext context = new StandardEvaluationContext();
        // 获取切点的参数值列表
        Object[] methodArgs = joinPoint.getArgs();
        for (int i = 0; i < methodArgs.length; i++) {
            // 将参数名称和参数值一一对应
            context.setVariable(argNames[i], methodArgs[i]);
        }
        return context;
    }

    /**
     * SpEL模板解析
     *
     * @param joinPoint   aop切点
     * @param template    字符串模板
     * @param expressions SpEL参数
     * @return java.lang.String
     * @author 敖癸
     * @since 2024/3/5 - 17:03
     */
    public static String buildTemplate(ProceedingJoinPoint joinPoint, String template, String[] expressions) {
        return buildTemplate(template, expressions, buildEvaluationContext(joinPoint));
    }

    /**
     * SpEL模板解析
     *
     * @param context     切点的上下文对象
     * @param template    字符串模板
     * @param expressions SpEL参数
     * @return java.lang.String
     * @author 敖癸
     * @since 2024/3/5 - 17:03
     */
    public static String buildTemplate(String template, String[] expressions, EvaluationContext context) {
        if (StrUtil.isNotBlank(template) && expressions != null) {
            Object[] args = Arrays.stream(expressions).map(expression -> {
                try {
                    return PARSER.parseExpression(expression).getValue(context);
                } catch (Exception e) {
                    return expression;
                }
            }).toArray();
            template = StrUtil.format(template, args);
        }
        return template;
    }

}

食用方式:

	// 锁5秒过期, 默认方法执行完成立即释放(并发锁)
    @DistributedLock(key = "Lock:SqlExec:{}", args = "T(com.dmjy.util.SqlParserUtil).hash(#sql)", expire = 5000)
    public void scanData(String sql) {
		...
	}

	// 锁5秒过期, isRelease = false表示方法执行完成不立即释放(防重锁)
    @DistributedLock(key = "Lock:SqlExec:{}", args = "T(com.dmjy.util.SqlParserUtil).hash(#sql)", expire = 5000, isRelease = false)
    public void scanData(String sql) {
		...
	}
	
	// 锁5秒过期, timeout = 3000表示获取锁超时等待3秒, 超过3秒还没获取到, 就抛出runtime异常
    @DistributedLock(key = "Lock:SqlExec:{}", args = "T(com.dmjy.util.SqlParserUtil).hash(#sql)", expire = 5000, timeout = 3000)
    public void scanData(String sql) {
		...
	}

posted on 2024-03-29 11:11  敖癸  阅读(109)  评论(0)    收藏  举报  来源