自定义注解+lua脚本实现 redis分布式锁(支持续锁)

目录

DistributedLock注解

LockMethodAspect 切面

 RedisLock  锁实现(lua脚本)

refreshLockTask 锁续约任务


首先项目集成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);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值