Spring aop优雅实现redis分布式锁 aop应用redis分布式锁

redis分布式锁切面实现

  • 说明
  • @DistributedLock
  • RedisLockAspect
  • RedisLockUtils
  • 应用

说明

网上找了一部分aop实现分布式锁的设计,感觉都不是特别好用,就自己写了一份。可以满足绝大部分分布式锁需求,请直接看代码,方法名很明了,注释也很明了啦,希望您看到不足的地方或者有不同见解的,还请再评论里回复,我会十分高兴和您探讨,十分感谢。

@DistributedLock

注解定义

import org.springframework.core.annotation.AliasFor;
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;
import java.util.concurrent.TimeUnit;

/**
 * @author Zl
 * @date 2019/8/2
 * @since
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {

    /**
     * 过期时间
     * 时间按时间单位换算
     * @return
     */
    long expire() default 3;

    /**
     * 等待时长
     * 时间按时间单位换算
     * 当为0时,不等待 默认不等待
     *
     * @return
     */
    long waitTime() default 0;

    /**
     * 时间单位 默认为秒
     *
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * springEl表达式
     * 为空时取方法名称锁方法
     *
     * @return
     */
    String key() default "";

    /**
     * 定义lock作用域,避免key重复
     * 为空时取类完整包名
     *
     * @return
     */
    String lockName() default "";
    
    /**
     * 异常i18n编码定义
     * 用于获取失败后做异常信息抛出
     * @return
     */
    @AliasFor("errorCode")
    String value();

    @AliasFor("value")
    String errorCode() default "";

}

RedisLockAspect

redis分布式锁切面处理

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @author Zl
 * @date 2019/8/2
 * @since
 */
@Slf4j
@Aspect
@Component
public class RedisLockAspect {

    private ExpressionParser parser = new SpelExpressionParser();

    @Autowired
    private RedisLockUtils redisLockUtils;


    @Pointcut("@annotation(com.ztesoft.zsmart.nros.base.annotation.DistributedLock)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String className = point.getTarget().getClass().getName();
        Object[] args = point.getArgs();
        String[] paramNames = signature.getParameterNames();
        //参数写入SpringEl域中
        EvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        //获取切面注解
        DistributedLock lock = method.getAnnotation(DistributedLock.class);
        TimeUnit timeUnit = lock.timeUnit();
        //redis key过期时间
        long expire = timeUnit.toMillis(lock.expire());
        //获取锁等待时间
        long waitTime = timeUnit.toMillis(lock.waitTime());
        //业务i18n异常编码
        String errorCode = lock.value();
        //key为空时锁方法,否则按SpringEl表达式取值
        String key = StringUtils.isEmpty(lock.key()) ? method.getName() : parser.parseExpression(lock.key()).getValue
                (context, String.class);
        //作用域为空时取className
        String lockName = StringUtils.isEmpty(lock.lockName()) ? className : lock.lockName();
        //构造redisKey
        String redisKey = lockName + "#" + key;
        boolean success = false;
        try {
            success = redisLockUtils.setLock(redisKey, expire, waitTime);
            if (success) {
                log.info("获取分布式锁成功,class={},method={},key={}", className, method, redisKey);
                //执行方法
                return point.proceed();
            }
        }
        catch (Exception e) {
            log.error("执行业务发生错误,class={},method={},args={}", className, method, args);
            throw e;
        }
        finally {
            if(success){
                redisLockUtils.releaseLock(redisKey);
            }
        }
        log.info("获取分布式锁失败,class={},method={},key={}", className, method, redisKey);
        //失败处理逻辑 此处抛出异常
        ExceptionHandler.publish(errorCode);
        return null;
    }

}

RedisLockUtils

与redis的交互域

import io.lettuce.core.SetArgs;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * redis 分布式锁Utils
 *
 * @author Zl
 * @date 2019/8/2
 * @since
 */
@Slf4j
@Component
public class RedisLockUtils {

    private static final DefaultRedisScript<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 ");
        DefaultRedisScript<String> script = new DefaultRedisScript<>();
        script.setScriptText(sb.toString());
        UNLOCK_LUA = script;
    }

    private static final String LOCK_VALUE = "1";

    private static final String LOCK_HEARD = "lock:";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public boolean setLock(String key, String value, long expire) {
        if (StringUtils.isBlank(value)) {
            value = LOCK_VALUE;
        }
        return setNx(buildKey(key), value, expire);
    }

    public boolean setLock(String key, long expire) {
        return setNx(key, null, expire);
    }


    public boolean setLock(String key, long expire, long waitTime) {
        return setLock(key, null, expire, waitTime);
    }

    public boolean setLock(String key, String value, long expire, long waitTime) {
        if (waitTime == 0L) {
            return setLock(key, value, expire);
        }
        long start = System.currentTimeMillis();
        while (true) {
            //检测是否超时
            if (System.currentTimeMillis() - start > waitTime) {
                return false;
            }
            if (setLock(key, value, expire)) {
                return Boolean.TRUE;
            }
        }
    }

    public Optional<String> getLockValue(String key) {
        String o = redisTemplate.opsForValue().get(buildKey(key));
        return Optional.ofNullable(o);
    }


    public boolean releaseLock(String key) {
        return releaseLock(key, LOCK_VALUE);
    }

    public boolean releaseLock(String key, String value) {
        try {
            Object execute = redisTemplate.execute(
                    (RedisConnection connection) -> connection.eval(
                            UNLOCK_LUA.getScriptAsString().getBytes(),
                            ReturnType.INTEGER,
                            1,
                            buildKey(key).getBytes(),
                            value.getBytes())
            );
            return execute.equals(1L);
        } catch (Exception e) {
            log.error("release lock occured an exception", e);
        } finally {
        }
        return false;
    }

    /**
     * @param key         key值
     * @param value       value值
     * @param expiredTime 毫秒
     * @return
     */
    private boolean setNx(String key, String value, long expiredTime) {
        Boolean resultBoolean = null;
        try {
            resultBoolean = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                Object nativeConnection = connection.getNativeConnection();
                String redisResult = "";
                @SuppressWarnings("unchecked")
                RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
                //lettuce连接包下序列化键值,否知无法用默认的ByteArrayCodec解析
                byte[] keyByte = stringRedisSerializer.serialize(key);
                byte[] valueByte = stringRedisSerializer.serialize(value);
                // lettuce连接包下 redis 单机模式setnx
                if (nativeConnection instanceof RedisAsyncCommands) {
                    RedisAsyncCommands commands = (RedisAsyncCommands) nativeConnection;
                    //同步方法执行、setnx禁止异步
                    redisResult = commands
                            .getStatefulConnection()
                            .sync()
                            .set(keyByte, valueByte, SetArgs.Builder.nx().px(expiredTime));
                }
                // lettuce连接包下 redis 集群模式setnx
                if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                    RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                    redisResult = clusterAsyncCommands
                            .getStatefulConnection()
                            .sync()
                            .set(keyByte, keyByte, SetArgs.Builder.nx().px(expiredTime));
                }
                //返回加锁结果
                return "OK".equalsIgnoreCase(redisResult);
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
        return resultBoolean != null && resultBoolean;
    }

    private String buildKey(String key) {
        return LOCK_HEARD + key;
    }
}

应用

    /**
     * 锁className+#+test
     */
    @DistributedLock("*CENTER-100001")
    public void test(){}

    /**
     * 锁className+#+test1
     * key过期设置为100毫秒
     */
    @DistributedLock(value = "*CENTER-100001",expire = 100,timeUnit = TimeUnit.MILLISECONDS)
    public void test1(){}

    /**
     * 锁className+#+test2
     * 轮询10秒获取
     */
    @DistributedLock(value = "*CENTER-100001",waitTime = 10)
    public void test2(){}

    /**
     * 锁testNamespace+#+test3
     */
    @DistributedLock(value = "*CENTER-100001",lockName = "testNamespace")
    public void test3(){}

    /**
     * 锁className+#+id
     */
    @DistributedLock(value = "*CENTER-100001",key = "#id")
    public void test(String id){}

    /**
     * 锁className+#+id
     */
    @DistributedLock(value = "*CENTER-100001",key = "#o.id")
    public void test(Object o){}

如上,value与errorCode 按具体项目修改实现,有固定放回格式的可以采用返回错误返回值,这里时抛出异常信息构造i18nMassage。也可以考虑el表达式取值后加上方法名。

你可能感兴趣的:(java,redis,springBoot)