线上redis分布式锁使用不当导致单号重复问题分析

背景

   前天,同事反馈说之前redis分布式锁有问题导致单号重复了,后面换成redission后就一切正常。基于这样的现象,对此进行分析

问题驱动

  1. 下面的代码经分析,发现了三个问题
    1.1 过期处理
    1.2 不可重入
    1.3 应用宕机处理
    @Aspect
    @Component("redisLockAspect")
    public class RedisLockAspectBack {
    	//获取P4jSyn注解
        P4jSyn lockInfo = getLockInfo(pjp);
        String synKey = getSynKey(pjp, lockInfo.synKey());
        boolean lock = false;  //标志物,true表示获取了到了该锁
        try {
        	while (!lock) {
        		//持锁时间,系统当前时间再往后加20秒
                long keepMills = System.currentTimeMillis() + lockInfo.keepMills();
                //为key“synKey”设置值keepMills,如果设置成功,则返回true
                lock = setIfAbsent(synKey, keepMills);
                //lock为true表示得到了锁,没有人加过相同的锁
                if(lock){
                 	//如果获得了该锁,则调用目标方法,执行业务逻辑任务
                     obj = pjp.proceed();
                 }
                 // 已过期,并且getAndSet后旧的时间戳依然是过期的,可以认为获取到了锁(问题点1)
    			else if (System.currentTimeMillis() > (getLock(synKey)) && (System.currentTimeMillis() > (getSet(synKey, keepMills)))) {
                     lock = true; 			//lock一定要设置成true,不然不能主动释放锁
                     obj = pjp.proceed();
                 } else {
                   // 如果超过最大等待时间抛出异常
                   if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
                         throw new TimeoutException("获取锁资源等待超时");
                     }
                     //只要当前时间没有大于超时时间,则继续等待10毫秒,以便继续尝试去获取锁
                     TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
                 }
        	}
        }finally {
          // 如果获取到了锁,释放锁(问题点2)
            if (lock) {
                releaseLock(synKey);
            }
        }
    }
    
  2. 首先对三个问题进行复现,然后验证redission是否已解决

过期释放锁异常

  1. 复现描述:
    1.1 假设有3个线程(A、B、C)
    1.2 A线程获取到锁后,业务阻塞一直没有释放锁
    1.3 B线程一直循环等待,直到超过锁持有时间,重新设置锁的持有时间,进入锁保护的区域
    1.4 在C获取锁之间,A线程业务执行完毕释放了锁,这时C线程就可以进来了(异常现象:没锁住)
  2. redission怎样解决问题?
    2.1 关于锁释放,官方文档有下面这段描述
    RLock object behaves according to the Java Lock specification. It means only lock owner thread can unlock it otherwise 
    IllegalMonitorStateException would be thrown. Otherwise consider to use RSemaphore object.
    
    google机翻
    RLock对象的行为根据Java锁规范。这意味着只有锁所有者线程才能解锁它,
    否则将抛出IllegalMonitorStateException异常。否则考虑使用RSemaphore对象
    
    2.2 测试用例演示
    (1)用IDEA线程模式打断点
    (2)切换到线程2,下一步,让其执行lock.lock()获取锁
    (3)切换到线程1,此时线程2的锁还未释放,这时下一步执行 lock.unlock()释放锁,这时会抛出IllegalMonitorStateException异常
    @Test
    public void testUnlock() throws InterruptedException {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://"+host+":"+port)
                .setTimeout(timeout)
                .setConnectionPoolSize(maxTotal)
                .setConnectionMinimumIdleSize(minIdle)
                .setPassword(password);
        RedissonClient redisson =  Redisson.create(config);
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                RLock lock = redisson.getLock("test"); // 断点1
                lock.unlock();
            }
        });
        thread1.start();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                RLock lock = redisson.getLock("test"); //断点2
                lock.lock();
                System.out.println("业务处理");
                lock.unlock();
            }
        });
        thread2.start();
        // 阻塞主线程
        CountDownLatch countDownLatch = new CountDownLatch(1);
        countDownLatch.await();
    }
    

线上redis分布式锁使用不当导致单号重复问题分析

应用宕机异常

  1. 复现描述:
    1.1 在线程A获取锁后,在释放锁之前系统异常停机会导致redis上的key一直存在
    1.2 超过锁持有时间之后,应用重启,线程B就获取不到锁,因为key存在,setnx时返回false
  2. redission怎样解决问题?
    2.1 通过redission官方文档描述,可知Redisson维护锁的watchdog
    If Redisson instance which acquired lock crashes then such lock could hang forever in acquired state. To avoid this Redisson maintains lock 
    watchdog, it prolongs lock expiration while lock holder Redisson instance is alive. By default lock watchdog timeout is 30 seconds and can be 
    changed through Config.lockWatchdogTimeout setting.
    
    google机翻
    如果获得锁的Redisson实例崩溃,那么该锁将永远挂起。
    为了避免这种Redisson维护锁看门狗,它会在锁持有者Redisson实例活着时延长锁过期时间。
    锁看门狗超时时间默认为30秒,可以通过Config修改。lockWatchdogTimeout设置
    

重入异常

  1. 复现描述
    1.1 线程A调用redis锁方法1,方法1调用同一把锁(key相同)的方法2,这是就会拿不到一直等待锁释放
  2. redission怎样解决问题?
    2.1 官方文档明确指出是支持可重入的
    Redis based distributed reentrant Lock object for Java and implements Lock interface
    
    google 翻译
    基于Redis的分布式可重入Java锁对象,实现了锁接口
    
    2.2 测试代码验证(断点调式方式和释放锁调式一样)
    @Test
    public void testReentrant() throws InterruptedException {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://"+host+":"+port)
                .setTimeout(timeout)
                .setConnectionPoolSize(maxTotal)
                .setConnectionMinimumIdleSize(minIdle)
                .setPassword(password);
        RedissonClient redisson =  Redisson.create(config);
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                RLock lock = redisson.getLock("test"); 
                // 同一个线程加锁两次,不会阻塞
                lock.lock();
                lock.lock();
                // 加两次锁就必须要释放两次,不然线程2在获取锁时就会阻塞
                lock.unlock();
                lock.unlock();
            }
        });
        thread1.start();
    
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                RLock lock = redisson.getLock("test");
                lock.lock();
                System.out.println("模拟执行业务逻辑");
                lock.unlock();
            }
        });
        thread2.start();
    
        CountDownLatch countDownLatch = new CountDownLatch(1);
        countDownLatch.await();
    }
    
上一篇:项目中分布式锁的实现方式(技术篇)


下一篇:【Redisson】五.Redisson RedLock算法的实现