Redis分布式锁

在分布式环境中,实现分布式锁是比较复杂的,不能像传统的 synchronizedReentrantLock 这种使用进程锁的方式,因为如果你的项目部署了多个副本,使用这种方式上锁,都是锁定的当前进程的锁对象,如果请求进入不同的副本中,对不同的副本锁是无效的。

Redis分布式锁的问题

不设置过期时间

我们知道,可以使用 Redis 的 setNx 命令实现 Redis的分布式锁,使用 Redis 的 lua 脚本实现解锁, Spring data Redis中的 RedisConnection#setNX(byte[] key, byte[] value) 接收两个参数,分别为 key,和 value,此方法不能设置key的过期时间。如果key不设置过期时间,就可能造成死锁,如应用程序没有正常的解锁,就会导致后续的需要获取此key的线程一直拿不到锁,造成死锁。所以,应该给这个key设置合理的过期时间。

过期时间设置太短

同样,如果一个key的过期时间设置太短,被锁定的临界区代码还没有执行完,key就自动过期了,会导致其它线程也能获取到这个key的锁,此时,两个线程同时进入被锁定的临界区代码块中,锁失效。

配合 @Transactional 事物注解使用不当的影响

如下一段代码,在高并发的情况会出现问题,解决方法:

  • lock.lock(); 加锁的逻辑放在调用此方法调用者,谁要调用此方法,自己先加锁,在加锁的临界区代码中调用此方法。

    如果有100个地方要调用此方法,那就要写100遍lock.lock()类似的代码,你会非常不爽…

  • 其实,我们只要保证 lock.lock() 加锁方法在事物AOP 拦截之前执行,且lock.unlock()解锁方法在事物 AOP 拦截提交之后执行即可

@Override
@Transactional
public void transactionalAndRedisLockTest(String keyId) {
    Lock lock = new RedisLock("transactionalAndRedisLockTest:" + keyId);
    lock.lock();
    try {
    	// 执行业务逻辑,先查询数据库记录,判断库存是否满足,如果满足减去库存,再执行update操作
    } finally {
    // 执行完业务逻辑,解锁后,此时事物还没有提交(因为有Transactional,需要整个方法执行完后,才会提交事物),数据库的记录并没有发生变化,
    // 此时,另一个线程是可以获取到锁的,那它可以进入到try语句中,如果这个线程查询是前一个线程还没有提交的数据,此时,锁的意义失效
        lock.unlock();
    }
}

终极Redis分布式锁的代码实现

定时任务

此定时任务是定时刷新key过期时间,防止 key设置的过期时间太短,业务方法还没有执行完,导致其它线程进入加锁的临界区代码中。

package com.hk.core.redis.locks;

import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 定义此Schedule,是防止Redis 锁key过期时间设置太短,导致key自动过期后,加锁的代码块还没有执行完成,导致其它线程也获取锁
 *
 * @author Kevin
 */
class RedisLockSchedule {

   private static final ConcurrentHashMap<String, ScheduledFuture<?>> FUTURE_MAP;

    private static final ScheduledThreadPoolExecutor POOL_EXECUTOR;

    static {
        var creator = new CustomizableThreadCreator();
        creator.setDaemon(false);
        creator.setThreadGroupName("Redis-Lock-");
        POOL_EXECUTOR = new ScheduledThreadPoolExecutor(2, creator::createThread);
        FUTURE_MAP = new ConcurrentHashMap<>(128);
    }

    static void register(String key, Runnable runnable, long period, TimeUnit timeUnit) {
        ScheduledFuture<?> fixedRate = POOL_EXECUTOR.scheduleAtFixedRate(runnable, 0, period, timeUnit);
        ScheduledFuture<?> oldFuture = FUTURE_MAP.put(key, fixedRate);
        if (Objects.nonNull(oldFuture)) {
            oldFuture.cancel(true);
        }
    }

    static void cancel(String key) {
        ScheduledFuture<?> future = FUTURE_MAP.remove(key);
        if (Objects.nonNull(future)) {
            future.cancel(true);
        }
    }
}

Redis锁实现核心类
------------------------------------------------------------------------
package com.hk.core.redis.locks;

import com.hk.commons.util.ArrayUtils;
import com.hk.commons.util.Lazy;
import com.hk.commons.util.ObjectUtils;
import com.hk.commons.util.SpringContextHolder;
import com.hk.core.redis.scripting.RedisScriptSource;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 基于 Redis setNx 与 lua 脚本实现分布式锁,支持可重入。
 *
 * @author Kevin
 */
public class RedisLock implements Lock {

    /**
     * 线程睡眠时间,单位:毫秒
     */
    private static final long SLEEP_TIME = 10;

    /**
     * <pre>
     * 默认过期时间: 20 秒
     * </pre>
     */
    private static final long EXPIRE_SECONDS = 20;
    /**
     * lua 脚本内容,lua 脚本能保证原子性执行
     */
    private static final DefaultRedisScript<Long> LUA_SCRIPT;

    /**
     * 非事物的 redisTemplate
     */
    private final static Lazy<StringRedisTemplate> STRING_REDIS_TEMPLATE_LAZY =
            Lazy.of(() -> SpringContextHolder.getBean("stringRedisTemplateDisabledTransactionSupport",
                    StringRedisTemplate.class));

    static {
        LUA_SCRIPT = new DefaultRedisScript<>();
        /*
        * 这里 LOCK是一段lua脚本,内容为:
        *    if redis.call("get",KEYS[1]) == ARGV[1] then 
        *        return redis.call("del",KEYS[1])
        *    else
        *        return 0
        *     end
        */
        LUA_SCRIPT.setScriptSource(RedisScriptSource.LOCK);
        LUA_SCRIPT.setResultType(Long.class);
    }

    /**
     * redis Key
     */
    private final String key;
    /**
     * key 过期时间,防止死锁
     */
    private final long expire;


    /**
     * 默认过期时间 2 秒
     *
     * @param key key
     */
    public RedisLock(String key) {
        this(key, EXPIRE_SECONDS);
    }

    /**
     * 默认过期时间 2 秒
     *
     * @param key key
     */
    public RedisLock(String key, long expire) {
        this.key = key;
        this.expire = expire <= 0 ? EXPIRE_SECONDS : expire;
    }

    /**
     * 获取锁
     */
    @Override
    public void lock() {
        while (!tryLock()) {
            if (SLEEP_TIME >= 0) {
                try {
                    TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    /**
     * 可被打断的获取 lock
     */
    @Override
    public void lockInterruptibly() throws InterruptedException {
        if (Thread.interrupted()) {//如果当前线程已中段
            throw new InterruptedException();
        }
        while (!tryLock()) {
            TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
        }
    }

    /**
     * 尝试获取锁,立即返回。如果返回  true ,加锁成功
     *
     * @return true or false
     */
    @Override
    public boolean tryLock() {
        /*
         * 只有当Key 不存在时,设置key的值,
         *  注意,redisTemplate.setIfAbsent方法,在 pipeline / transaction 的方法调用此方法时, 会返回 null,
         *  所以这里使用  stringRedisTemplateDisabledTransactionSupport 的 redisTemplate
         */
        var result = ObjectUtils.defaultIfNull(STRING_REDIS_TEMPLATE_LAZY.get().opsForValue().setIfAbsent(key,
                Long.toString(Thread.currentThread().getId()), expire, TimeUnit.SECONDS), Boolean.FALSE);
        if (result && expire > 1) { //过期时间大于1秒才设置开启线程定时刷新过期时间
            registerSchedule();
        }
        return result;
    }

    /**
     * 初始化线程并启动
     */
    private void registerSchedule() {
        cancelSchedule();
        long period;
        TimeUnit timeUnit;
        if (expire > 2) {
            period = expire - 2;
            timeUnit = TimeUnit.SECONDS;
        } else {
            period = 500;
            timeUnit = TimeUnit.MILLISECONDS;
        }
        RedisLockSchedule.register(key, () -> {
            //刷新过期时间,如果没有成功,可能是key不存在了,直接结束此任务
            if (!ObjectUtils.defaultIfNull(STRING_REDIS_TEMPLATE_LAZY.get().expire(key, expire, TimeUnit.SECONDS),
                    Boolean.FALSE)) {
                cancelSchedule();
            }
        }, period, timeUnit);
    }

    private void cancelSchedule() {
        RedisLockSchedule.cancel(key);
    }

    /**
     * 在指定的时间段获取锁,超出指定的时间立即返回
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (Thread.interrupted()) {
            throw new InterruptedException();
        }
        long max = System.nanoTime() + unit.toNanos(time);
        while (System.nanoTime() < max) {
            if (tryLock()) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        STRING_REDIS_TEMPLATE_LAZY.get().execute(LUA_SCRIPT, ArrayUtils.asArrayList(key),
                Long.toString(Thread.currentThread().getId()));
        cancelSchedule();
    }

    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException("Redis Lock newCondition");
    }
}
Redis锁使用注解方式
package com.hk.core.redis.annotations;

import java.lang.annotation.*;

/**
 * redis 锁使用注解的方式
 *
 * @author Kevin
 * @see com.hk.core.redis.locks.RedisLock
 * @see com.hk.core.redis.aspect.RedisLockInterceptor
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {

    /**
     * <pre>
     * 锁的 Key,支持 SPEL 表达式
     * SPEL格式如:
     *      #参数名,如方法为method(int value) ,则可以使用 #value 获取
     *      也可以使用计算公式: 如 "1+1" -> 结果为2
     *      方法参数也可以使用对象,如 method(int value, Object obj) ,如果想以obj的属性作为key,可以写成 #obj.属性名。
     *      如果方法参数有数组,如 method(int[]arr) ,可以使用 #arr[0] 获取第几个元素作为key
     *      如果方法参数有map,如 method(int[]arr,Map<String,Object> map) ,可以使用 #map[key] 获取map指定key的值作为锁的key
     *      如果方法参数有list,如 method(List<String> list) ,可以使用 #list[0] 获取list指定的索引作为锁的key
     * </pre>
     */
    String key();

    /**
     * 过期时间,单位为秒,默认20 秒
     */
    long expireSeconds() default 20;
}

Redis锁注解 AOP

注意:类上加了 @Order 注解,此AOP 会在 TransactionInterceptor 的前面。

package com.hk.core.redis.aspect;

import com.hk.commons.util.StringUtils;
import com.hk.core.redis.annotations.RedisLock;
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.core.annotation.Order;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;

import java.util.Objects;

/**
 * <pre>
 *
 * 加 Order 注解,必需要在 事务之前执行(也就是这个Aspect 拦截器要在 {@link org.springframework.transaction.interceptor.TransactionInterceptor} 之前),事务提交之后,再释放类。
 *      先使用redis上锁、再开启事物、再执行业务代码,再提交事物 ,再释放redis 锁,才能让下一个线程能获取到锁。
 * </pre>
 * <p>
 * {@link RedisLock} aspect
 *
 * @author Kevin
 */
@Aspect
@Order(value = Integer.MIN_VALUE)
public class RedisLockInterceptor {

    @Pointcut("@annotation(com.hk.core.redis.annotations.RedisLock)")
    public void redisLock() {
    }

    @Around("redisLock()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        if (pjp.getSignature() instanceof MethodSignature signature) {
            var method = signature.getMethod();
            var redisLock = method.getAnnotation(RedisLock.class);
            if (Objects.nonNull(redisLock)) {
                String key = redisLock.key();
                var parser = new SpelExpressionParser();
                var parameters = signature.getMethod().getParameters();
                var args = pjp.getArgs();
                var builder = SimpleEvaluationContext.forReadWriteDataBinding();
                int argLen = args.length;
                if (argLen > 1) {
                    builder.withRootObject(args[0]);
                }
                var context = builder.build();
                for (int i = 0; i < argLen; i++) {
                    context.setVariable(parameters[i].getName(), args[i]);
                }
                try {
                    key = parser.parseExpression(redisLock.key()).getValue(context, String.class);
                } catch (Exception e) {
                    // can not parse spel,use original key.
                }
                if (StringUtils.isEmpty(key)) {
                    key = pjp.getTarget().getClass().getName() + method.getName();
                }
                var lock = new com.hk.core.redis.locks.RedisLock(key, redisLock.expireSeconds());
                lock.lock();
                try {
                    return pjp.proceed();
                } finally {
                    //保证释放锁时,事物已提交
                    lock.unlock();
                }
            }
        }
        return pjp.proceed();
    }
}

测试
基于注解方式
@Transactional
@RedisLock(key = "#keyId")
public void transactionalAndRedisLockTest(String keyId) {
   //您的业务逻辑	
}
基于编程式

主要是使用 transactionTemplate 执行

private final TransactionTemplate transactionTemplate;

/**
* 编程式方式
*/
public void transactionalAndRedisLockTest(String keyId) {
    Lock lock = new RedisLock(keyId);
    lock.lock();
    try {
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                //要执行的业务逻辑
                return null;
            }
        });
    } finally {
        lock.unlock();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellowordx007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值