REDIS10_Redission's entry case, multi master case building, locking and unlocking distributed locks, and underlying source code analysis

①. How to develop based on the official website

  1. Automatic lock renewal. If the business is too long, the lock will be automatically renewed for a new 30s during operation. There is no need to worry about the long business time
  2. If the business goes down, the default expiration time is 30s, avoiding deadlock
  3. As long as the business of locking is completed, the current lock will not be renewed. Even if it is not unlocked manually, the lock will be automatically deleted after 30s by default

②. Introduction case based on Redisson

  • ①. Import pom
	<!--introduce redisson Distributed lock-->
	<dependency>
		<groupId>org.redisson</groupId>
		<artifactId>redisson</artifactId>
		<version>3.13.4</version>
	</dependency>
  • ②. Create a configuration class, refer to the documentation
//2. Create configuration class
@Configuration
public class MyRedisConfig {
    /**
     * All use of Redisson
     * @return
     * @throws IOException
     */
    //https://github.com/redisson/redisson/wiki/14.-%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6%E6%95%B4%E5%90%88
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        // Create configuration for singleton mode
        //config.useSingleServer().setAddress("redis://" + ipAddr + ":6379");
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        return Redisson.create(config);
    }
}

  • ③. Conduct unit tests
@Autowired
//RedissonClient redissonClient;
RedissonClient redisson;
@Test
public void redission(){
	System.out.println(redissonClient);
	RLock lock = redisson.getLock("lock");
}

③. What are the shortcomings of setnx's distributed locks

  • ①. What are the disadvantages of setnx based distributed locks?
  1. Thread 1 first obtains the lock successfully and writes the key value pair to the master node of redis
  2. Before redis synchronizes the key value pair to the slave node, the master fails
  3. redis triggers failover, and one slave is upgraded to a new master
  4. At this time, the new master does not contain the key value pair written by thread 1, so thread 2 can successfully get the lock even if it tries to get the lock
  5. This is equivalent to two threads acquiring locks, which may lead to various unexpected situations, such as the most common dirty data.
    We add an exclusive lock. Only one redis lock can be successfully created and held at the same time. It is strictly prohibited for more than two request threads to get the lock.
  • ②. The father of redis proposed the Redlock algorithm to solve this problem
    (Redis also provides a Redlock algorithm to implement distributed locks based on multiple instances. Lock variables are maintained by multiple instances. Even if an instance fails, the lock variable still exists, and the client can still complete the lock operation. The Redlock algorithm is an effective solution to realize highly reliable distributed locks, which can be used in practical development.)

  • ③. AP of Redis cluster (lock loss caused by Redis asynchronous replication, for example, if the master node does not come and gives the data just set in to the slave node, it hangs)

  • ④. redis distributed lock, multi master cluster mode, need to calculate the fault tolerance rate (N=2X+1)
    For example, if one machine dies in the network, my request is OK and can be used. How many machines can be deployed in the main cluster at most?
    N=2*1+1=3

  • ⑤. Why odd? N = 2X + 1 (N is the number of finally deployed machines and X is the number of fault-tolerant machines)
    The least machine, the most output effect
    In the cluster environment, 1 redis failed, acceptable. 2N+2= 2 * 1+2 =4, 4 sets deployed
    In the cluster environment, 2 redis failed, acceptable. 2N+2 = 2 * 2+2 =6, 6 sets deployed

  • ⑥. So what is fault tolerance?

  1. I can tolerate the failure of many machine instances. The so-called tolerance means that the data consistency can be Ok and the CP data consistency can be satisfied
  2. In the cluster environment, 1 redis failed, acceptable. 2X+1 = 2 * 1+1 =3, deploy 3, one is dead, and the remaining 2 can work normally, then deploy 3
  3. In the cluster environment, 2 redis failed, acceptable. 2X+1 = 2 * 2+1 =5, deploy 5, 2 dead, and the remaining 3 can work normally, then deploy 5

④. Construction of three host cases

  • ①. Install three machines on docker
 docker run -p 6381:6379 --name redis-master-1 -d redis:6.08
docker run -p 6382:6379 --name redis-master-2 -d redis:6.0.8
docker run -p 6383:6379 --name redis-master-3 -d redis:6.0.8

  • ②. Build Module(redis_redlock) and change POM xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.xiaozhi.redis.redlock</groupId>
    <artifactId>redis_redlock</artifactId>
    <version>0.0.1-SNAPSHOT</version>


    <properties>
        <java.version>1.8</java.version>
    </properties>

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

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

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <!--<version>3.12.0</version>-->
            <version>3.13.4</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--swagger-ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • ③. Each configuration file is as follows
spring.application.name=spring-boot-redis
server.port=9090

spring.swagger2.enabled=true

spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
#sentinel/cluster/single
spring.redis.mode=single

spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10

spring.redis.single.address1=192.168.111.147:6381
spring.redis.single.address2=192.168.111.147:6382
spring.redis.single.address3=192.168.111.147:6383
//CacheConfiguration
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {

    @Autowired
    RedisProperties redisProperties;

    @Bean
    RedissonClient redissonClient1() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient2() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient3() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    /**
     * stand-alone
     * @return
     */
    /*@Bean
    public Redisson redisson()
    {
        Config config = new Config();

        config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);

        return (Redisson) Redisson.create(config);
    }*/
}
//RedisPoolProperties
@Data
public class RedisPoolProperties {
    private int maxIdle;
    private int minIdle;
    private int maxActive;
    private int maxWait;
    private int connTimeout;
    private int soTimeout;
    /**
     * Pool size
     */
    private  int size;
}
//RedisProperties
@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperties {
    private int database;
    /**
     * Time to wait for the node to reply to the command. The time starts when the command is sent successfully
     */
    private int timeout;
    private String password
    private String mode;
    /**
     * Pool configuration
     */
    private RedisPoolProperties pool;
    /**
     * Stand alone information configuration
     */
    private RedisSingleProperties single;
}
//RedisSingleProperties
@Data
public class RedisSingleProperties {
    private  String address1;
    private  String address2;
    private  String address3;
}
  • ④. controller code display
@RestController
@Slf4j
public class RedLockController {
    public static final String CACHE_KEY_REDLOCK = "TANGZHI_REDLOCK";
    @Autowired
    RedissonClient redissonClient1;
    @Autowired
    RedissonClient redissonClient2;
    @Autowired
    RedissonClient redissonClient3;
    @GetMapping(value = "/redlock")
    public void getlock() {
        //CACHE_KEY_REDLOCK is the key of redis distributed lock
        RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
        RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
        RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        boolean isLockBoolean;
        try {
            //waitTime the waiting time to grab the lock. Normally, wait for 3 seconds
            //leaseTime is the expiration time of the redis key. Normally, wait for 5 minutes and 300 seconds.
            isLockBoolean = redLock.tryLock(3, 300, TimeUnit.SECONDS);
            log.info("thread {},Did you get the lock:{} ",Thread.currentThread().getName(),isLockBoolean);
            if (isLockBoolean) {
                System.out.println(Thread.currentThread().getName()+"\t"+"---come in biz");
                //Business logic, busy for 10 minutes
                try { TimeUnit.MINUTES.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        } catch (Exception e) {
            log.error("redlock exception ",e);
        } finally {
            // In any case, unlock it in the end
            redLock.unlock();
        }
    }
}
  • ⑤. Test: http://localhost:9090/redlock
[root@TANG2021 ~]# docker exec -it redis-master-1 redis-cli
127.0.0.1:6379> keys *
1) "TANGZHI_REDLOCK"
127.0.0.1:6379> type TANGZHI_REDLOCK
hash
127.0.0.1:6379> hgetall TANGZHI_REDLOCK
1) "ca512c4b-f05f-4578-9b8f-45e2e35aa0d7:50"
2) "1"

⑤. Redisson source code analysis

  • ①. Test code display
public class WatchDogDemo {
    public static final String LOCKKEY = "DEBUG_YUANMA";

    private static Config config;
    private static Redisson redisson;

    static {
        config = new Config();
        config.useSingleServer().setAddress("redis://"+"192.168.68.143"+":6379").setDatabase(0);
        redisson = (Redisson)Redisson.create(config);
    }

    public static void main(String[] args) {
        RLock redissonLock = redisson.getLock(LOCKKEY);
        redissonLock.lock();
        try {
            System.out.println("1111-------biz");
            //Pause the thread for a few seconds
            try { TimeUnit.SECONDS.sleep(25); } catch (InterruptedException e) { e.printStackTrace(); }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
                redissonLock.unlock();
            }
        }
        System.out.println(Thread.currentThread().getName() + " main ------ ends.");
        //Pause the thread for a few seconds
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        redisson.shutdown();
    }
}
  • ②. The Redis distributed lock has expired, but the business logic has not finished processing it. What should I do? Introduce cache life extension
  1. Start an additional thread, regularly check whether the thread still holds the lock, and if so, extend the expiration time.
  2. This scheme is implemented in Redisson. The "watchdog" is used to check regularly (once every 1 / 3 of the lock time). If the thread still holds the lock, the expiration time will be refreshed
  3. After obtaining the lock successfully, add a watchdog to the lock. The watchdog will start a scheduled task and renew the lock when it is not released and is about to expire
  • ③. Detailed analysis of cache life extension source code 1: the lock key created through redisson is 30 seconds by default
  • ④. Detailed cache life extension source code analysis II
  • ⑤. Detailed cache life extension source code analysis III
  1. A timer is initialized. The delay time is internalLockLeaseTime/3
  2. In Redisson, the internalLockLeaseTime is 30s, that is, it is renewed every 10s for 30s each time

  • ⑥. Automatic delay mechanism of watch dog
    (if client A locks successfully, it will start A watch dog. As A background thread, it will check every 10 seconds. If client A still holds the lock key, it will continue to prolong the lifetime of the lock key. By default, each renewal starts from 30 seconds.)
  • ⑦. Detailed analysis of cache life extension source code IV: lock logic (for those expired within the default time of 30s) lock()
  1. No lock, lock (unique id), start the scheduled task, and set the expiration time of 30s
  2. With lock: unique ID + 1 (reentrant lock)
  3. Non locked thread, returns the expiration time of the current locked thread
  • ⑧. Detailed explanation of cache life extension source code analysis 5: Unlocking logic
  1. If the thread of the released lock and the thread of the existing lock are not the same thread, null is returned
  2. By hincrby decrementing 1, the lock is released once first. If the remaining number of times is greater than 0, it proves that the current lock is a reentrant lock and the expiration time is refreshed
  3. If the remaining times are less than 0, delete the key and release the lock, and the unlocking is successful

  • ⑨. Detailed explanation of cache life extension source code analysis 6: locking logic
    (lock. Lock (10, timeunit. Seconds): it will be unlocked automatically for 10s and will not be renewed automatically)
    Underlying principle: if we pass the lock timeout, we will send it to redis to execute the lua script to occupy the lock. The default timeout is the time we specify
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE,
 command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
    }

Keywords: Database Redis memcached

Added by kellydigital on Fri, 17 Dec 2021 15:43:24 +0200