Principle of redisson MultiLock and application of distributed lock

Posted by JustinM01 on Tue, 04 Jan 2022 15:25:42 +0100

1, Foreword

Redisson distributed interlocking RedissonMultiLock objects based on Redis can associate multiple RLock objects into an interlocking, and each RLock object instance can come from different redisson instances. Of course, this is the introduction of the official website. What is it? Let's take a look at the use and source code of interlock MultiLock!

2, MultiLock use

According to the official documents, the Redisson client here can not be the same. Of course, it is not necessary to use a client in general work.

3, Lock

Source entry: org redisson. Redissonmultilock #lock(), the default timeout leaseTime is not set, so it is - 1.

  public void lock(long leaseTime, TimeUnit unit) {
        try {
            lockInterruptibly(leaseTime, unit);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }


This method is too long. Let's split it up and read it.

  1. Basic waiting time baseWaitTime = number of locks * 1500, which is 4500 milliseconds here;
  2. leaseTime == -1, so waitTime = baseWaitTime, that is 4500;
  3. while (true) call tryLock to lock until it succeeds.

Call the tryLock method, where the parameters waitTime = 4500, leaseTime = -1, unit = MILLISECONDS.

Let's take a look at the logic in tryLock?


leaseTime != - If 1 is not satisfied, skip this part directly.


waitTime != -1. If the conditions are met, remainTime = 4500, lockWaitTime = 4500.


Therefore, the failedLocksLimit() method directly returns 0, which means that all locks must be successful.

Here is the point: traverse all locks and lock them in turn. Locking logic is no different from reentrant locking. So Lua script will not be analyzed.

The above is the result of tryLock locking.

If the lock is successfully added, the successful lock will be put into the acquiredLocks set; If locking fails, you need to judge the failedLocksLimit. Because this is 0, it will directly release all locks in the successfully locked collection acquiredLocks, empty the successful collection and recover the iterator.

After each lock is added, the remaining time remainTime of the lock will be updated. If remainTime is less than or equal to 0, it indicates that the lock has timed out and returns false directly. This will execute the external while (true) logic, and then go through RedissonMultiLock#tryLock again.

  • summary
    According to the understanding, the drawing is as follows: in general, it is to put key1, key2, key3... keyN into a List set, and then iterate and cycle locking until all are successful.

  • The difference between lock and tryLock
  1. tryLock() indicates that it is used to attempt to acquire the lock. If the acquisition is successful, it returns true. If the acquisition fails (that is, the lock has been acquired by other threads), it returns false
  2. The tryLock(long time, TimeUnit unit) method is similar to the tryLock() method, except that this method will wait for a certain time when the lock cannot be obtained. If the lock cannot be obtained within the time limit, it will return false. Returns true if the lock is obtained at the beginning or during the waiting period
  3. Trylock (long waittime, long leaseTime, timeunit) is based on 2. If a lock is obtained, the maximum holding time of the lock is leaseTime

4, Lock release

After reading the lock logic, the lock release is easier to understand.

You can directly traverse and release the lock Unlockasync() is the called RedissonBaseLock#unlockAsync() method.

5, Implementing distributed locks using MultiLock

  • Establish a three master and three slave redis cluster, Reference articles

  • Create a springboot project

  • redissonCluster.yml

clusterServersConfig:
  # Connection idle timeout, unit: ms, default: 10000
  idleConnectionTimeout: 10000
  pingConnectionInterval: 1000
  # The wait timeout when establishing a connection with any node. The time unit is milliseconds. The default is 10000
  connectTimeout: 10000
  # Time to wait for the node to reply to the command. The time starts when the command is sent successfully. Default 3000
  timeout: 3000
  # Command failure retries
  retryAttempts: 3
  # Command retry sending interval in milliseconds
  retryInterval: 1500
  # Reconnect interval in milliseconds
  failedSlaveReconnectionInterval: 3000
  # Maximum number of execution failures
  #failedAttempts: 3
  # password
  password:
  # Maximum subscriptions per connection
  subscriptionsPerConnection: 5
  clientName: null
  # Selection of loadBalancer load balancing algorithm class
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  # The minimum number of idle connections of the primary node is 32 by default
  masterConnectionMinimumIdleSize: 32
  # The primary node connection pool size is 64 by default
  masterConnectionPoolSize: 64
  # Load balancing mode of subscription operation
  subscriptionMode: SLAVE
  # Read from server only
  readMode: SLAVE
  # Cluster address
  nodeAddresses:
    - "redis://xxx.xxx.xxx.xxx:9001"
    - "redis://xxx.xxx.xxx.xxx:9002"
    - "redis://xxx.xxx.xxx.xxx:9003"
  # The time interval for status scanning of Redis cluster nodes. The unit is milliseconds. Default 1000
  scanInterval: 1000
  #This number of thread pools is shared by all RTopic object listeners, RRemoteService callers, and RExecutorService tasks. Default 2
threads: 0
#The number of thread pools is the number of threads saved in the thread pool shared by all distributed data types and services created by a Redisson instance and the underlying clients. Default 2
nettyThreads: 0
# The default encoding method is org redisson. codec. JsonJacksonCodec
codec: !<org.redisson.codec.JsonJacksonCodec> {}
#transmission mode 
transportMode: NIO
# The automatic expiration time of the distributed lock prevents deadlock. The unit is milliseconds. The default is 30s. Every 1 / 3 of the lockWatchdogTimeout time. If the game business is not executed, the lock will be automatically renewed
lockWatchdogTimeout: 30000
# This parameter is used to modify whether messages are sent out according to the receiving order of subscribed and published messages. If no is selected, parallel processing will be implemented for messages. This parameter is only applicable to subscribed and published messages. The default is true
keepPubSubOrder: true
# Used to specify the behavior of high-performance engines. Since the selection of this variable value is closely related to the use scenario (except NORMAL), we recommend trying each parameter value.
#
#This parameter is limited to Redisson PRO version only.
#performanceMode: HIGHER_THROUGHPUT
@Configuration
public class RedissonHttpSessionConfig  {
    //Call shutdown after service is disabled
    @Bean(destroyMethod="shutdown")
    public RedissonClient getRedissonClient() throws IOException {
        ResourceLoader loader = new DefaultResourceLoader();
        Resource resource = loader.getResource("redissonCluster.yml");
        Config config = Config.fromYAML(resource.getInputStream());
        return Redisson.create(config);
    }
}
@Component
public class RedissonMultiLockInit {
    private final ArrayList<RLock> rLockList=new ArrayList<>();
    @Autowired
    RedissonClient redissonClient;
    public  RedissonMultiLock initLock(String... locksName){
        for (String lockName : locksName) {
           rLockList.add(redissonClient.getLock(lockName));
        }
        RLock[] rLocks = rLockList.toArray(new RLock[0]);
        return new RedissonMultiLock(rLocks);
    }
    public List<RLock> getRLocks(){
        return rLockList;
    }
}
@Controller
@RequestMapping("/lock")
public class LockController {
    @Autowired
    RedissonMultiLockInit redissonMultiLockInit;
    @Autowired
    UserMapper userMapper;
    @Autowired
    PlatformTransactionManager transactionManager;
    
    @GetMapping("/get/{waitTime}/{leaseTime}")
    @ResponseBody
    public String getLock(@PathVariable long waitTime, @PathVariable long leaseTime) throws InterruptedException {
        String[] strings={"test1","test2","test3"};
        RedissonMultiLock lock = redissonMultiLockInit.initLock(strings);
        //When transaction management is enabled manually, @ Transitional cannot control the distributed locks of redis
        //Create transaction definition object
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        //Set whether to read only. false supports transactions
        def.setReadOnly(false);
        //Set the transaction isolation level. You can repeatedly read the default level of mysql
        def.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
        //Set transaction propagation behavior
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        //Configure transaction manager
        TransactionStatus status = transactionManager.getTransaction(def);
        if (lock.tryLock(waitTime,leaseTime, TimeUnit.SECONDS)){
            System.out.println(Thread.currentThread().getName()+" waiting time is "+waitTime+"s " +
                    "leaseTime is "+leaseTime+"s "+
                    "execute time is "+(leaseTime+10)+" s" );
            try {
                userMapper.updateById(new User(1L,23,"beijing","myname2"));
                //Simulate execution timeout release lock
                Thread.sleep((leaseTime+10)*1000);
                List<RLock> rLocks = redissonMultiLockInit.getRLocks();
                //Determine whether all locks are still held to prevent lock expiration
                if(rLocks.stream().allMatch(RLock::isLocked)){
                    //Submit business
                    transactionManager.commit(status);
                    //Release the distributed lock after submitting the business
                    lock.unlock();
                    return "unlock success,transition success";
                }
                else {
                    //Rollback business
                    transactionManager.rollback(status);
                    return "lock is expired,transition fail";
                }
            } catch (Exception e) {
                e.printStackTrace();
                return "transition error";
            }
        }
        else {
            return Thread.currentThread().getName()+" can't get the lock,because the waiting time isn't enough. Waiting time is "+waitTime+"s, " +
                    "leaseTime is "+leaseTime+"s ";
        }
    }
}
  • Note: there is a classic problem that @ Transitional and distributed locks are used at the same time. To solve this problem, we manually start the transaction and ensure that the distributed lock is released after the transaction is committed. About this problem, You can refer to this article
  • test
  1. http://localhost:8090/lock/get/6/-1. It means that there is a maximum of 6s waiting time to obtain the lock, and the execution time of the business can be non renewed (enable the watchdog mechanism), then the business will be successful this time

  2. http://localhost:8090/lock/get/6/9 , which means that there is a maximum of 6s waiting time to obtain the lock, and a maximum of 9s business execution time. If it times out, the distributed lock will be released. The business fails. Because I wrote the business in the controller for more than 9s, the business must fail this time.

  3. http://localhost:8090/lock/get/6/-1 and http://localhost:8090/lock/get/2/-1

Since the first service takes 9s of service execution time, the second service cannot obtain the distributed lock within 2s and will exit the service.

Reference articles

Topics: Redis Distribution Cache redisson