In the previous article on Redis distributed lock security (Part 1), we mentioned that Redis has a hidden danger in single-node failover. Antirez, the author of Redis, proposed the Redlock algorithm. We can learn about the algorithm description on the official website of Redis. Redlock is also the official guidance specification for the realization of distributed lock.

The Antirez Redlock

1.1 Obtaining and Releasing locks

Antirez proposed the distributed lock algorithm Redlock:

  • Based on N Redis nodes, which are independent (example sets N to 5)
  • Locks are acquired and released in the same way as for a single node

To acquire the lock, the client does the following:

  1. The value is obtained in millisecondsThe current time
  2. Try to acquire the lock in all N instances using the same key name and random value in all instances in sequence. In step 2, when you set a lock in each instance, set one more locktimeout.timeoutLess thanThe total automatic release time of a lock. For example, if the automatic release time is 10 seconds, the timeout may be between 5 and 50 milliseconds. This prevents the client from communicating with the failed Redis node for long periods of time: if one instance is unavailable, we should try to communicate with the next instance as soon as possible.
  3. The client passes through theThe current timeSubtract the timestamp obtained in Step 1 to calculate the time required to acquire the lock. A lock is obtained if and only if the client can obtain the lock in most instances (at least three), and the total time taken to obtain the lock is less than the lock validity time.
  4. If a lock is acquired, it isValid timeIs the initial valid time minus the elapsed time, as calculated in Step 3.
  5. If the client fails to acquire the lock for some reason (unable to lock the N / 2 + 1 instance or with a negative validity time), it will attempt to unlock all instances (even if it thinks it is not the instance that can be locked).

The lock release process is simple: the client initiates the lock release operation to all Redis nodes, regardless of whether they successfully acquired the lock or not. This is because when the client obtains the lock from Redis, redis has already executed successfully, but the data is lost when it is returned to the client. In the eyes of the client, the communication of Redis is faulty, but redis actually locks successfully

1.2 Martin’s analysis

In this article, Martin talked about many basic issues of distributed system (especially the asynchronous model of distributed computing). This article is roughly divided into two parts, which can be summarized as follows:

  • Distributed locks with automatic expiration must provide some fencing mechanism to ensure true mutually exclusive protection of shared resources. Redlock does not provide such a mechanism.
  • Redlock is built on an insecure system model. It has strong requirements on timing assumptions of the system, and these requirements cannot be guaranteed in real systems.

A good distributed algorithm should be based on the asynchronous model, and the security of the algorithm should not depend on any timing assumptions. In an asynchronous model: processes can be paused for any length of time, messages can be delayed in the network for any length of time, or even lost, and the system clock can fail in any way. For a good distributed algorithm, these factors should not affect its safety property, but only its liVENESS property, which means that even in very extreme cases (such as a serious system clock error), At best, the algorithm cannot give a result in a limited time, and should not give a wrong result. Such algorithms exist in the real world, such as the well-known Paxos, or Raft. But clearly Redlock’s level of security is not up to par by this standard.

In addition, Martin believes that if distributed locks are used for efficiency and locks are allowed to fail occasionally, the locking scheme using single Redis nodes is sufficient, simple and efficient. Redlock is a double feature. He describes Redlock’s algorithm as neither fish nor fowl.

1.3 Antirez’s rebuttal

About fencing. Antirez questions Martin’s argument: why use a distributed lock and require it to be so secure when there is already a fencing mechanism that can maintain mutually exclusive access to resources even if the lock fails? Even though Redlock doesn’t offer the incremental fencing token Martin mentioned, the random string (my_random_value) generated by Redlock can achieve the same effect. This random string is not incremented, but it is unique and can be called a unique token.

Personally, I also feel that Martin’s first point is indeed a little far-fetched. After the client completes the lock acquisition request to each Redis node, it will calculate the time consumed in the process and then check whether the lock validity time has been exceeded. After the client recovers from the GC Pause, it will use this check to discover that the lock has expired and will no longer assume that it successfully acquired the lock.

Antirez also focuses on the rebuttal that the clock jumps, which Redlock doesn’t work properly once they happen. Redlock, he said, doesn’t require a clock to be perfectly accurate, it just needs it to be nearly accurate

  • Manually changing the clock for human reasons, just don’t do it. Otherwise, if someone manually changes the Raft protocol’s persistence log, even Raft won’t work.
  • Using an NTPD program that does not “jump” to adjust the system clock (possibly with proper configuration), the clock changes are made with many small adjustments.

At this point in the discussion, it doesn’t really matter who’s right or wrong between Martin and Antirez. As long as we have a good understanding of the level of security that Redlock (or any other distributed lock) can provide, we can make our own choices. There was a lot of discussion on Hacker News about Martin’s and Antirez’s blogs. For those of you who are interested, check out Martin’s blog discussion and Antirez’s blog discussion.

Elegant Reddison

Redisson is the official Redis recommended Java version of Redis client. It provides a lot of functionality and is very powerful, and we will only discuss its distributed locking capabilities and Springboot integration here.

2.1 Basic Usage

2.1.1 pom. XML

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.14.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.redisson</groupId>
                    <artifactId>redisson-spring-data-23</artifactId>
                </exclusion>
            </exclusions>

        </dependency>
        
        <! Redisson-spring-data-23 = redisson-spring-data-23 = redisson-spring-data-23 = redisson-spring-data-23
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-data-22</artifactId>
            <version>3.14.1</version>
        </dependency>
Copy the code

2.1.2 application. Yml

spring:
  profiles:
    active: dev
  redis:
    redisson:
      file: classpath:redisson.yml
Copy the code

2.1.3 redisson. Yml

#singleServerConfig: single-node use
clusterServersConfig:
  Connection idle timeout, in milliseconds
  idleConnectionTimeout: 10000
  Connection timed out in milliseconds
  connectTimeout: 60000
  Command wait timeout, in milliseconds
  timeout: 3000
  # Number of command failed attempts. An error is thrown if the retryAttempts cannot be sent to a specified node.
  # If the attempt to send within this limit succeeds, start the timeout timer.
  retryAttempts: 3
  # Command retry sending interval, in milliseconds
  retryInterval: 1500
  # your password
  password: xxx
  # Maximum number of subscriptions per connection
  subscriptionsPerConnection: 5
  # Client name
  clientName: xxx
  nodeAddresses:
  - "Redis: / / 127.0.0.1:7004"
  - "Redis: / / 127.0.0.1:7001"
  - "Redis: / / 127.0.0.1:7000"
  # Minimum number of free connections for publish and subscribe connections
  subscriptionConnectionMinimumIdleSize: 1
  Publish and subscribe connection pool size
  subscriptionConnectionPoolSize: 50
  # Minimum number of free connections
  connectionMinimumIdleSize: 32
  # connection pool size
  connectionPoolSize: 64
  # database no.
  # database: single node
  # node address
  #address: single node
  DNS monitoring interval, in milliseconds
  dnsMonitoringInterval: 5000
# number of thread pools, default: number of cores currently processed * 2
  threads: 16
# number of Netty thread pools, default: number of current processing cores * 2
  nettyThreads: 32
# Automatic topology refresh time
  scanInterval: 1000
# code
codec: ! 
       {}
# Transfer mode
transportMode : "NIO"
Copy the code

2.2 Application Scenarios

2.2.1 acquiring a lock

RedissonClient has the following lock implementation. We need to obtain different locks according to different usage scenarios. Typically, we use getLock () to implement distributed locks.

 /** * Returns Lock instance by name. * <p> * Implements a <b>non-fair</b> locking so doesn't guarantees an acquire order  by threads. * <p> * To increase reliability during failover, all operations wait for propagation to all Redis slaves. * *@param name - name of object
     * @return Lock object
     */
     // Unfair lock
    RLock getLock(String name);

    /**
     * Returns MultiLock instance associated with specified <code>locks</code>
     * 
     * @param locks - collection of locks
     * @return MultiLock object
     */
     / / joint lock
    RLock getMultiLock(RLock... locks);
    
    /* * Use getLock method instead. Returned instance uses Redis Slave synchronization */
    @Deprecated
    / / has been abandoned
    RLock getRedLock(RLock... locks);
    
    /**
     * Returns Lock instance by name.
     * <p>
     * Implements a <b>fair</b> locking so it guarantees an acquire order by threads.
     * <p>
     * To increase reliability during failover, all operations wait for propagation to all Redis slaves.
     * 
     * @param name - name of object
     * @return Lock object
     */
     // Fair lock: When multiple Redisson clients request the lock at the same time, priority is assigned to the thread that made the request first
    RLock getFairLock(String name);
    
    /**
     * Returns ReadWriteLock instance by name.
     * <p>
     * To increase reliability during failover, all operations wait for propagation to all Redis slaves.
     *
     * @param name - name of object
     * @return Lock object
     */
     // Read/write lock (read/write mutually exclusive, read not mutually exclusive)
    RReadWriteLock getReadWriteLock(String name);
Copy the code

2.2.2 Common Locking

 RLock lock = redissonClient.getLock(lockKey);
 // Threads that have not acquired the lock will block here
 lock.lock();
Copy the code

2.2.3 Try Lock (Common)

 RLock lock = redissonClient.getLock(lockKey);
        try {

            if (lock.tryLock(30L, TimeUnit.SECONDS)) {
                // Get the lock
            } else {
                // No lock was obtained}}finally {
            lock.unlock();
        }
Copy the code

Lock. tryLock() will return the lock directly. This method can also pass in a waitTime timeout. LeaseTime will automatically release the timeout if leaseTime is not passed

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
Copy the code

2.3 Redisson implements Redlock

In the master-slave mode, for example, Redlock needs at least three nodes, and Redisson also needs to create at least three instances. Then we cannot create RedissonClient directly with SpringBoot auto-loading configuration

2.3.1 Manually Creating a RedissonClient


    @Bean(name ="redissonClient1", destroyMethod ="shutdown")
    public RedissonClient redissonClient(RedisProperties redisProperties){

        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://xxx"+":"+redisProperties.getPort())
                .setPassword(redisProperties.getPassword())
                .setConnectionPoolSize(100)
                .setConnectionMinimumIdleSize(10);
        return Redisson.create(config);
    }

    @Bean(name ="redissonClient2", destroyMethod ="shutdown")
    public RedissonClient redissonClient2(RedisProperties redisProperties){

        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://xxx"+":"+redisProperties.getPort())
                .setPassword(redisProperties.getPassword())
                .setConnectionPoolSize(100)
                .setConnectionMinimumIdleSize(10);
        return Redisson.create(config);
    }

    @Bean(name ="redissonClient3", destroyMethod ="shutdown")
    public RedissonClient redissonClient3(RedisProperties redisProperties){

        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://xxx"+":"+redisProperties.getPort())
                .setPassword(redisProperties.getPassword())
                .setConnectionPoolSize(100)
                .setConnectionMinimumIdleSize(10);
        return Redisson.create(config);
    }
Copy the code

2.3.2 lock

RLock lock = redissonClient1.getLock(lockKey);
        RLock lock2 = redissonClient2.getLock(lockKey);
        RLock lock3 = redissonClient3.getLock(lockKey);

        final RedissonRedLock redissonRedLock = new RedissonRedLock(lock, lock2, lock3);
        try {
            if(redissonRedLock.tryLock(30L, TimeUnit.SECONDS)){
                // Get the lock
            }else{}}finally {
            redissonRedLock.unlock();
        }
Copy the code

What about sentinel mode or Cluster mode? We must have three sentinel clusters or clusters, clusters, not nodes. So while maintaining high availability, Redlock’s hardware requirements are also quite high. In most cases, trylock() with getLock will suffice for distributed locking. How trylock() is implemented will be broken down next time.