实践基于Redis的分布式锁

作者: 程序熊大 | 来源:发表于2018-01-09 23:00 被阅读1924次

本文来自社区这周的讨论话题—— 技术专题讨论第四期:漫谈分布式锁,也总结了我对分布式锁的认知和使用经验。

应用场景

当多个机器(多个进程)会对同一条数据进行修改时,并且要求这个修改是原子性的。这里有两个限定:(1)多个进程之间的竞争,意味着JDK自带的锁失效;(2)原子性修改,意味着数据是有状态的,修改前后有依赖。

实现方式

  • 基于Redis实现,主要基于redis的setnx(set if not exist)命令;
  • 基于Zookeeper实现;
  • 基于version字段实现,乐观锁,两个线程可以同时读取到原有的version值,但是最终只有一个可以完成操作;

这三种方式中,我接触过第一和第三种。基于redis的分布式锁功能更加强大,可以实现阻塞和非阻塞锁。

基于Redis的实践

锁的实现

  • 锁的key为目标数据的唯一键,value为锁的期望超时时间点;
  • 首先进行一次setnx命令,尝试获取锁,如果获取成功,则设置锁的最终超时时间(以防在当前进程获取锁后奔溃导致锁无法释放);如果获取锁失败,则检查当前的锁是否超时,如果发现没有超时,则获取锁失败;如果发现锁已经超时(即锁的超时时间小于等于当前时间),则再次尝试获取锁,取到后判断下当前的超时时间和之前的超时时间是否相等,如果相等则说明当前的客户端是排队等待的线程里的第一个尝试获取锁的,让它获取成功即可。


    基于redis实现分布式锁逻辑.png
public class RedisDistributionLock {

    private static final Logger logger = LoggerFactory.getLogger(RedisDistributionLock.class);

    //key的TTL,一天
    private static final int finalDefaultTTLwithKey = 24 * 3600;

    //锁默认超时时间,20秒
    private static final long defaultExpireTime = 20 * 1000;

    private static final boolean Success = true;
    
    @Resource( name = "redisTemplate")
    private RedisTemplate<String, String> redisTemplateForGeneralize;

    /**
     * 加锁,锁默认超时时间20秒
     * @param resource
     * @return
     */
    public boolean lock(String resource) {
        return this.lock(resource, defaultExpireTime);
    }

    /**
     * 加锁,同时设置锁超时时间
     * @param key 分布式锁的key
     * @param expireTime 单位是ms
     * @return
     */
    public boolean lock(String key, long expireTime) {

        logger.debug("redis lock debug, start. key:[{}], expireTime:[{}]",key,expireTime);
        long now = Instant.now().toEpochMilli();
        long lockExpireTime = now + expireTime;

        //setnx
        boolean executeResult = redisTemplateForGeneralize.opsForValue().setIfAbsent(key,String.valueOf(lockExpireTime));
        logger.debug("redis lock debug, setnx. key:[{}], expireTime:[{}], executeResult:[{}]", key, expireTime,executeResult);

        //取锁成功,为key设置expire
        if (executeResult == Success) {
            redisTemplateForGeneralize.expire(key,finalDefaultTTLwithKey, TimeUnit.SECONDS);
            return true;
        }
        //没有取到锁,继续流程
        else{
            Object valueFromRedis = this.getKeyWithRetry(key, 3);
            // 避免获取锁失败,同时对方释放锁后,造成NPE
            if (valueFromRedis != null) {
                //已存在的锁超时时间
                long oldExpireTime = Long.parseLong((String)valueFromRedis);
                logger.debug("redis lock debug, key already seted. key:[{}], oldExpireTime:[{}]",key,oldExpireTime);
                //锁过期时间小于当前时间,锁已经超时,重新取锁
                if (oldExpireTime <= now) {
                    logger.debug("redis lock debug, lock time expired. key:[{}], oldExpireTime:[{}], now:[{}]", key, oldExpireTime, now);
                    String valueFromRedis2 = redisTemplateForGeneralize.opsForValue().getAndSet(key, String.valueOf(lockExpireTime));
                    long currentExpireTime = Long.parseLong(valueFromRedis2);
                    //判断currentExpireTime与oldExpireTime是否相等
                    if(currentExpireTime == oldExpireTime){
                        //相等,则取锁成功
                        logger.debug("redis lock debug, getSet. key:[{}], currentExpireTime:[{}], oldExpireTime:[{}], lockExpireTime:[{}]", key, currentExpireTime, oldExpireTime, lockExpireTime);
                        redisTemplateForGeneralize.expire(key, finalDefaultTTLwithKey, TimeUnit.SECONDS);
                        return true;
                    }else{
                        //不相等,取锁失败
                        return false;
                    }
                }
            }
            else {
                logger.warn("redis lock,lock have been release. key:[{}]", key);
                return false;
            }
        }
        return false;
    }

    private Object getKeyWithRetry(String key, int retryTimes) {
        int failTime = 0;
        while (failTime < retryTimes) {
            try {
                return redisTemplateForGeneralize.opsForValue().get(key);
            } catch (Exception e) {
                failTime++;
                if (failTime >= retryTimes) {
                    throw e;
                }
            }
        }
        return null;
    }

    /**
     * 解锁
     * @param key
     * @return
     */
    public boolean unlock(String key) {
        logger.debug("redis unlock debug, start. resource:[{}].",key);
        redisTemplateForGeneralize.delete(key);
        return Success;
    }
}

自定义注解使用分布式锁

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLockAnnoation {

    String keyPrefix() default "";

    /**
     * 要锁定的key中包含的属性
     */
    String[] keys() default {};

    /**
     * 是否阻塞锁;
     * 1. true:获取不到锁,阻塞一定时间;
     * 2. false:获取不到锁,立即返回
     */
    boolean isSpin() default true;

    /**
     * 超时时间
     */
    int expireTime() default 10000;

    /**
     * 等待时间
     */
    int waitTime() default 50;

    /**
     * 获取不到锁的等待时间
     */
    int retryTimes() default 20;
}

实现分布式锁的逻辑

@Component
@Aspect
public class RedisLockAdvice {

    private static final Logger logger = LoggerFactory.getLogger(RedisLockAdvice.class);

    @Resource
    private RedisDistributionLock redisDistributionLock;

    @Around("@annotation(RedisLockAnnoation)")
    public Object processAround(ProceedingJoinPoint pjp) throws Throwable {
        //获取方法上的注解对象
        String methodName = pjp.getSignature().getName();
        Class<?> classTarget = pjp.getTarget().getClass();
        Class<?>[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes();
        Method objMethod = classTarget.getMethod(methodName, par);
        RedisLockAnnoation redisLockAnnoation = objMethod.getDeclaredAnnotation(RedisLockAnnoation.class);

        //拼装分布式锁的key
        String[] keys = redisLockAnnoation.keys();
        Object[] args = pjp.getArgs();
        Object arg = args[0];
        StringBuilder temp = new StringBuilder();
        temp.append(redisLockAnnoation.keyPrefix());
        for (String key : keys) {
            String getMethod = "get" + StringUtils.capitalize(key);
            temp.append(MethodUtils.invokeExactMethod(arg, getMethod)).append("_");
        }
        String redisKey = StringUtils.removeEnd(temp.toString(), "_");

        //执行分布式锁的逻辑
        if (redisLockAnnoation.isSpin()) {
            //阻塞锁
            int lockRetryTime = 0;
            try {
                while (!redisDistributionLock.lock(redisKey, redisLockAnnoation.expireTime())) {
                    if (lockRetryTime++ > redisLockAnnoation.retryTimes()) {
                        logger.error("lock exception. key:{}, lockRetryTime:{}", redisKey, lockRetryTime);
                        throw ExceptionUtil.geneException(CommonExceptionEnum.SYSTEM_ERROR);
                    }
                    ThreadUtil.holdXms(redisLockAnnoation.waitTime());
                }
                return pjp.proceed();
            } finally {
                redisDistributionLock.unlock(redisKey);
            }
        } else {
            //非阻塞锁
            try {
                if (!redisDistributionLock.lock(redisKey)) {
                    logger.error("lock exception. key:{}", redisKey);
                    throw ExceptionUtil.geneException(CommonExceptionEnum.SYSTEM_ERROR);
                }
                return pjp.proceed();
            } finally {
                redisDistributionLock.unlock(redisKey);
            }
        }
    }
}

参考资料

  1. Java分布式锁三种实现方案
  2. Java注解的基础与高级应用
  3. 基于 AOP 和 Redis 实现的分布式锁
  4. 如何高效排查系统故障?一分钱引发的系统设计“踩坑”案例
  5. 用redis实现分布式锁

相关文章

  • 基于redis实现的分布式锁

    本文要点 基于redis实现分布式锁demo 基于redis实现分布式锁原理 基于redis实现分布式锁优缺点 正...

  • 分布式锁实现

    基于数据库实现分布式锁基于缓存(redis,memcached)实现分布式锁基于Zookeeper实现分布式锁 s...

  • 7.2-基于Redis实现分布式锁的几种坑你是否踩过《上》—小滴

    基于Redis实现分布式锁的几种坑你是否踩过《上》 简介:基于Redis实现分布式锁的几种坑 实现分布式锁 可以用...

  • 基于redis的分布式锁

    分布式锁实现方案 基于数据库实现分布式锁 基于缓存(redis,memcached,tair)实现分布式锁 基于Z...

  • 分布式锁的实现方式

    分布式锁通常有3种实现方式,即数据库乐观锁、基于redis的分布式锁和基于zookeeper的分布式锁。 一、基于...

  • 分布式锁之Redis实现(acquire)

    分布式锁一般有三种实现方式: 基于数据库的锁; 基于Redis的分布式锁; 基于ZooKeeper的分布式锁。 本...

  • Redis实现分布式锁

    分布式下的分布式锁一般实现有三种: 基于数据库的乐观锁 基于redis的分布式锁 基于zookeeper的分布式锁...

  • 84 redis实现分布式锁的原理

    1,Redis使用setnx 实现2,Redisson 分布式锁;Redis基于 setnx 实现分布式锁原理:R...

  • Redis实现分布式锁

    1. 分布式锁分类 数据库乐观锁 基于Redis的分布式锁 基于ZooKeeper的分布式锁 2. 组件依赖 po...

  • Redis分布式锁实现

    分布式锁实现方式 数据库乐观锁; 基于Redis的分布式锁; 基于ZooKeeper的分布式锁。 本篇将介绍第二种...

网友评论

  • 听雨_6beb:这样的话分布式部署的时候,两个服务的时间不一样,会不会有问题?用过期时间可以吗
  • 孤独而芒种:set key value ex 100 nx 即可解决上面的问题
  • 梅西激动地说:这个unlock中的del操作不能贸然进行吧, 如果线程A超时, 那么线程B就会获取到新锁, 此时如果A执行完毕, del了, 那么B也会受影响吧
  • zxy1994:赞,和我写的一个文件锁有用的原理都差不多。都是里面存了个时间,防止死锁
  • 一纳秒:题主你好,有个点不是很明白,关于期望超时时间点的超时设置,如果期望过期时间过短(比如设置为1ms),实际需要上锁的代码块执行需要50ms,看到代码描述只要期望超时时间<=当前时间就有机会获得锁,这样会不会造成需要上锁的代码块并发执行?
    一纳秒:@杜琪 我好像明白了,其实设置这个时间的目的是解决死锁问题的吧?如果一个程序执行太久不释放锁,可以通过这个时间来让其他线程有机会获得该锁
    一纳秒:@杜琪 那这个期望超时时间结合什么来设置比较合适呢?:grin:
    程序熊大:@一纳秒 会的👍
  • c9c0dac9dc84:这个锁实现有几个问题:
    1. //取锁成功,为key设置expire
    if (executeResult == Success) {
    //宕机,死锁
    }
    2.争用锁的服务系统时间得保持一致。
    3.争用锁的服务可能获取失败,但是会set自己的过期时间。
    一纳秒:@勿忘初心_3bd7 说下我的理解,第1个问题,锁的过期时间lockExpireTime在之前就设置了,即便在设置key过期时间前宕机也只是key过期时间没有设置而已,其他线程也可以通过判断锁超时时间来重新获得锁(当然这时也会设置key的过期时间)
    c9c0dac9dc84:@杜琪
    1.if (executeResult == Success) {
    //在expire操作前宕机会导致死锁, 因为这个操作不具有原子性
    expire操作
    }
    3.String valueFromRedis2 = redisTemplateForGeneralize.opsForValue().getAndSet(key, String.valueOf(lockExpireTime));
    若有两个线程同时向redis发送这个请求,
    if(currentExpireTime == oldExpireTime)
    if条件虽然能确保,getAndSet最先发送到redis的线程获得锁,但是第二个线程还是
    还getAndSet设置自己的lockExpireTime,虽然这个两个线程的时间差不大。
    程序熊大:@勿忘初心_3bd7 👍感谢
    (1)关于第一个问题,确实会,所以我设置了一个最终的超时时间,在这个超时时间过后,不会死锁;
    (2)是的,需要所有集群的机器的时间保持一致;
    (3)没太看懂这个点哦
  • 程序熊大:来自网友的建议:一般这样做,set key 时 value 弄个随机串,获取的时候获得这个随机串,unlock 的时候用 key 和这个随机串去 处理,key 的 value 不一致表示锁的状态不对,不允许删除
  • sunny_Lee:杜老师厉害!
  • Anoyi:杜老师好厉害:stuck_out_tongue_winking_eye:

本文标题:实践基于Redis的分布式锁

本文链接:https://www.haomeiwen.com/subject/qbagnxtx.html