SpringBoot integrates Redisson to realize Redis cluster distributed lock

Posted by waq2062 on Thu, 10 Feb 2022 05:51:34 +0100

Distributed lock usage scenario

Protect shared resources with locks, such as generating a unique serial number, ensuring sufficient inventory before placing an order in the e-commerce system, etc.

The core principle of RedLock algorithm:

N Redis master nodes that are completely independent and have no master-slave relationship are used to ensure that they will not go down at the same time in most cases. N is generally odd. A client needs to do the following to obtain a lock:

  1. Gets the current time in milliseconds.
  2. Use the same key and random value to request locks on N nodes in turn. In this step, when the client requests locks on each master, there will be a much smaller timeout than the total lock release time. For example, if the automatic lock release time is 10 seconds, the timeout time of lock request of each node may be in the range of 5-50 milliseconds, which can prevent a client from blocking a failed master node for a long time. If a master node is unavailable, we should try the next master node as soon as possible.
  3. The client calculates the time spent in acquiring the lock in the second step. Only when the client successfully acquires the lock ((N/2) +1) on most master nodes and the total time consumed does not exceed the lock release time, the lock is considered to have been successfully acquired.
  4. If the lock is obtained successfully, the automatic lock release time is the initial lock release time minus the time consumed to obtain the lock before.
  5. If the lock acquisition fails, the client will release the lock on each master node, even those locks that he thinks have not been acquired successfully, whether it is because less than half of the successfully acquired locks (N/2+1) or because the total time consumed exceeds the lock release time.

To put it simply, it means using multiple master nodes to obtain locks at more than half of the master nodes; Otherwise, it will fail and roll back – delete the locks acquired on all nodes before.

Source code

Maven dependency

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson-spring-boot-starter</artifactId>
      <version>3.15.5</version>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.25</version>
    </dependency>

application.yml

server:
  port: 8091
spring:
  redisson:
    address: 127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002 # Redis cluster address
    try-time: 0   #Try time
    lock-time: 4  #Lock time
    password:     # redis password
    scanInterval: 1000 # Scan interval
    retryAttempts: 5   # Command failure retries
    timeout: 3000    # Timeout

Redistribution configuration class

@Configuration
public class RedissonConfig {

    @Value(value = "${spring.redisson.address}")
    private String redissonAddress;

    @Value(value = "${spring.redisson.password}")
    private String redissonPassword;

    @Value(value = "${spring.redisson.scanInterval}")
    private int redissonScanInterval;

    @Value(value = "${spring.redisson.retryAttempts}")
    private int redissonRetryAttempts;

    @Value(value = "${spring.redisson.timeout}")
    private int redissonTimeout;

    @Bean
    public RedissonClient redissonClient() {
        String[] nodes = redissonAddress.split(",");
        for (int i = 0; i < nodes.length; i++) {
            nodes[i] = "redis://" + nodes[i];
        }

        Config config = new Config();
        config.useClusterServers() //This is the cluster server used
                .setScanInterval(redissonScanInterval) //Set cluster status scanning time
                .addNodeAddress(nodes).setRetryAttempts(redissonRetryAttempts).setTimeout(redissonTimeout);
        if (StringUtils.isNotEmpty(redissonPassword)) {
            config.useClusterServers().setPassword(redissonPassword);
        }
        return Redisson.create(config);
    }
}

Controller test class

@RestController
@RequestMapping("api/redisson" )
@Slf4j
public class RedissonLockController {

    /**
     * Lock test shared variable
     */
    private Integer lockCount = 10;

    /**
     * Lock free test shared variable
     */
    private Integer count = 10;

    /**
     * Number of simulated threads
     */

    private static int threadNum = 1000;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * Simulate concurrent testing with and without locks
     * @return
     */
    @GetMapping("/test")
    public void lock(){
        // Counter
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < threadNum; i ++) {
            MyRunnable myRunnable = new MyRunnable(countDownLatch);
            Thread myThread = new Thread(myRunnable);
            myThread.start();
        }
        // Free all threads
        countDownLatch.countDown();
    }

    /**
     * Lock test
     */
    private void testLockCount() {
        RLock lock = redissonClient.getLock("myLock");
        try {
            // Lock, set the timeout of 2s, and unlock automatically 10 seconds after locking
            boolean getLock = lock.tryLock(2000,10000, TimeUnit.MILLISECONDS);
            if (!getLock) {
                log.info("Current thread:[{}]No lock obtained", Thread.currentThread().getName());
                return ;
            }
            lockCount--;
            log.info("lockCount Value:"+lockCount);
        }catch (Exception e){
            log.error(e.getMessage(),e);
        }finally {
            // Release lock
            if(null != lock && lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    /**
     * Lockless test
     */
    private void testCount() {
        count--;
        log.info("count Value:"+count);
    }

    public class MyRunnable implements Runnable {
        /**
         * Counter
         */
        final CountDownLatch countDownLatch;
        public MyRunnable(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                // Block the current thread until the timer value is 0
                countDownLatch.await();
            } catch (InterruptedException e) {
                log.error(e.getMessage(),e);
            }
            // Lockless operation
            //testCount();
            // Locking operation
            testLockCount();
        }
    }
}

RedisLockApplication startup class

@SpringBootApplication
public class RedisLockApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisLockApplication.class, args);
    }
}

test

Access: http://localhost:8091/api/redisson/test , the console outputs the synchronized serial number as follows:

reference resources

https://redis.io/topics/distlock

https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

https://github.com/redisson/redisson/blob/master/redisson-spring-boot-starter/README.md

https://github.com/redis/jedis/wiki/Getting-started

https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/reference/htmlsingle/

Source code

Topics: Spring Boot