Analysis of duplicate order number caused by improper use of online redis distributed lock

background

The day before yesterday, my colleague reported that there was a problem with the redis distributed lock, resulting in duplicate order numbers. Later, everything was normal after it was replaced with reission. Based on this phenomenon, this paper analyzes it

Problem driven

  1. The following code is analyzed and three problems are found
    1.1 expiration treatment
    1.2 non reentrant
    1.3 handling of application downtime
    @Aspect
    @Component("redisLockAspect")
    public class RedisLockAspectBack {
    	//Get P4jSyn annotation
        P4jSyn lockInfo = getLockInfo(pjp);
        String synKey = getSynKey(pjp, lockInfo.synKey());
        boolean lock = false;  //Flag. true indicates that the lock has been obtained
        try {
        	while (!lock) {
        		//Lock holding time, the current time of the system will be increased by 20 seconds
                long keepMills = System.currentTimeMillis() + lockInfo.keepMills();
                //Set the value keepmils for the key "synKey". If the setting is successful, it returns true
                lock = setIfAbsent(synKey, keepMills);
                //If lock is true, it means that a lock has been obtained and no one has added the same lock
                if(lock){
                 	//If the lock is obtained, the target method is called to execute the business logic task
                     obj = pjp.proceed();
                 }
                 // It has expired, and the old timestamp is still expired after getAndSet. It can be considered that the lock has been obtained (problem point 1)
    			else if (System.currentTimeMillis() > (getLock(synKey)) && (System.currentTimeMillis() > (getSet(synKey, keepMills)))) {
                     lock = true; 			//Lock must be set to true, or you cannot actively release the lock
                     obj = pjp.proceed();
                 } else {
                   // If the maximum waiting time is exceeded, an exception is thrown
                   if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
                         throw new TimeoutException("Get lock resource wait timeout");
                     }
                     //As long as the current time is not greater than the timeout, continue to wait for 10 milliseconds to continue trying to acquire the lock
                     TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
                 }
        	}
        }finally {
          // If the lock is acquired, release the lock (problem point 2)
            if (lock) {
                releaseLock(synKey);
            }
        }
    }
    
  2. First, reproduce the three problems, and then verify whether the reission has been solved

Expired release lock exception

  1. Reproduction description:
    1.1 suppose there are three threads (A, B, C)
    1.2 after thread a obtains the lock, the service block does not release the lock
    1.3 thread B waits circularly until the lock holding time is exceeded. Reset the lock holding time and enter the lock protected area
    1.4 when C obtains the lock, thread A releases the lock after the business execution, and then thread C can come in (exception: not locked)
  2. How does reission solve the problem?
    2.1 as for lock release, the following paragraph is described in the official document
    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 Machine turnover
    RLock The object's behavior is based on Java Lock specification. This means that only the lock owner thread can unlock it,
    Otherwise, it will be thrown IllegalMonitorStateException Abnormal. Otherwise, consider using RSemaphore object
    
    2.2 test case demonstration
    (1) Break points with IDEA thread mode
    (2) Switch to thread 2. Next, let it execute lock.lock() to obtain the lock
    (3) Switch to thread 1. At this time, the lock of thread 2 has not been released. At this time, the next step is to execute lock.unlock() to release the lock. At this time, an IllegalMonitorStateException will be thrown
    @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"); // Breakpoint 1
                lock.unlock();
            }
        });
        thread1.start();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                RLock lock = redisson.getLock("test"); //Breakpoint 2
                lock.lock();
                System.out.println("Business processing");
                lock.unlock();
            }
        });
        thread2.start();
        // Block main thread
        CountDownLatch countDownLatch = new CountDownLatch(1);
        countDownLatch.await();
    }
    

Application downtime exception

  1. Reproduction description:
    1.1 after thread A obtains the lock, the system stops abnormally before releasing the lock, which will cause the key on redis to exist all the time
    1.2 after the lock holding time is exceeded, the application restarts, and thread B cannot obtain the lock because the key exists. setnx returns false
  2. How does reission solve the problem?
    2.1 adoption Official document of redistribution The description shows that Redisson maintains the watchdog of the lock
    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 Machine turnover
     If you get a lock Redisson If the instance crashes, the lock will hang forever.
    To avoid this Redisson Maintain the lock watchdog, it will be in the lock holder Redisson Extend the lock expiration time when the instance is alive.
    The watchdog timeout is 30 seconds by default, which can be accessed through Config Modification. lockWatchdogTimeout set up
    

Reentry exception

  1. Recurrence description
    1.1 thread A calls redis lock method 1, and method 1 calls method 2 of the same lock (with the same key), which will not be available until the lock is released
  2. How does reission solve the problem?
    2.1 official documents clearly indicate that reentry is supported
    Redis based distributed reentrant Lock object for Java and implements Lock interface
    
    google translate
     be based on Redis Distributed reentrant Java Lock object, which implements the lock interface
    
    2.2 test code verification (breakpoint mode is the same as lock release mode)
    @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 the same thread twice without blocking
                lock.lock();
                lock.lock();
                // If the lock is added twice, it must be released twice, otherwise thread 2 will block when acquiring the lock
                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("Simulate execution of business logic");
                lock.unlock();
            }
        });
        thread2.start();
    
        CountDownLatch countDownLatch = new CountDownLatch(1);
        countDownLatch.await();
    }
    

Keywords: Database Redis Distribution Cache

Added by bluetonic on Tue, 04 Jan 2022 19:27:01 +0200