Deep understanding of redis -- redis distributed lock

1. Type of lock

2. Characteristics of a highly robust distributed lock

3. Evolution of a single redis distributed lock

4. Multiple redis distributed locks

5. Summary

1. Type of lock
In our daily development activities, locks are generally divided into two categories:
1) Locks in the same JVM, such as synchronized and Lock, ReentrantLock, etc
2) Distributed locks across JVM s. Because the services are deployed in clusters, the single version locks no longer work, and resources are shared among different servers.

2. Characteristics of a highly robust distributed lock
1) Exclusivity only one thread can hold a lock at any one time
2) High availability in the redis cluster environment, lock failure cannot occur because a node hangs
3) Deadlock prevention shall not have deadlock, but shall have the function of timeout control
4) If you don't rob randomly, you can't unlock other people's locks. Your own locks can only be released by yourself
5) Reentrant: after the same thread of the same node obtains the lock, it can obtain the lock again

3. Evolution of a single redis distributed lock

Version 1: lock of stand-alone version

Let's take a look at the lockless Code:

@RestController
public class GoodController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods()
    {
        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0)
        {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
            return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
        }else{
            System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
        }

        return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
    }
}

The above procedures are not locked when selling goods, which will cause oversold in case of concurrency.

Pressure test with jMeter:

Because in the case of stand-alone version, we can use synchronize or lock to solve it. The above code:

   @GetMapping("/buy_goods")
    public String buy_Goods() {
        synchronized (this) {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece" + "\t Server port:" + serverPort);
                return "At this time, you have successfully killed the remaining goods:" + realNumber + "piece" + "\t Server port:" + serverPort;
            } else {
                System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time" + "\t Server port:" + serverPort);
            }

            return "The goods have been sold out/End of activity/Call timeout, welcome next time" + "\t Server port:" + serverPort;
        }
    }

Operation results:

However, this is only effective for stand-alone programs. We start two microservices and configure load balancing with nginx, which will still oversold:

Server 1:

Server 2:

Look, the 189th stock has been sold twice.

Problem: after distributed deployment, single machine locks are still oversold. Distributed locks are needed at this time!

Version 2: redis distributed locks:

We use redis to lock and prevent oversold

    @GetMapping("/buy_goods/v2")
    public String buy_GoodsV2() {
        String key = "redisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
        //Lock with redis 
        Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if(!flagLock)
        {
            return "Failed to grab lock,o(╥﹏╥)o";
        }

        String result = stringRedisTemplate.opsForValue().get("goods:001");
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);

        if(goodsNumber > 0)
        {
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
            stringRedisTemplate.delete(key);
            System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
            return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
        }else{
            System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
        }

        return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
    }

At this point, let's run the following code:

Service 1:

Service 2:

Question: why can't you sell one? Because we didn't unlock the key s of distributed locks after they were sold out.

Version 3: in finally, release the lock

@GetMapping("/buy_goods/v3")
    public String buy_GoodsV3() {
        String key = "redisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
            if(!flagLock)
            {
                return "Lock grabbing failed,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
                return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
            }else{
                System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
            }
            return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
        } finally {
            stringRedisTemplate.delete(key);
        }
    }

Operation results;
Service 1:

Service 2:

Problem: if the server goes down and the code level doesn't come to finally, there's no way to ensure unlocking. The key hasn't been deleted. We need to add an expiration time to the key!

Version 4: add expiration time to key

 @GetMapping("/buy_goods/v4")
    public String buy_GoodsV4() {
        String key = "redisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
            //Increase expiration time
            stringRedisTemplate.expire(key,10L, TimeUnit.SECONDS);
            if(!flagLock)
            {
                return "Lock grabbing failed,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
                return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
            }else{
                System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
            }
            return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
        } finally {
            stringRedisTemplate.delete(key);
        }
    }

Problem: setting the key and setting the expiration time are not atomic. It is possible that the server may be down during this period.

Version 5: merge the set key and the expiration time of the set key into one line as an atomic operation

 @GetMapping("/buy_goods/v5")
    public String buy_GoodsV5() {
        String key = "redisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
            //Setting the expiration time of key and key is combined into one line, which is an atomic operation, and the bottom layer is setnx command
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);

            if(!flagLock)
            {
                return "Lock grabbing failed,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
                return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
            }else{
                System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
            }
            return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
        } finally {
            stringRedisTemplate.delete(key);
        }
    }

Problem: when deleting a key, it is possible that our lock has expired. What is deleted is the lock of the next thread.

Version 6: when deleting key s, you can only delete your own, not others. Add a layer of judgment

  @GetMapping("/buy_goods/v6")
    public String buy_GoodsV6(){
        String key = "redisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);

            if(!flagLock)
            {
                return "Lock grabbing failed,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
                return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
            }else{
                System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
            }
            return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
        } finally {
            //Make a judgment when deleting a key
            if (stringRedisTemplate.opsForValue().get(key).equals(value)) {
                stringRedisTemplate.delete(key);
            }
        }
    }

Problem: the judgment of finally block + del deletion operation is not atomic. Maybe after the judgment, the lock expires and someone else's lock is deleted.

Version 7: using Lua script will ensure the atomicity of judging and deleting locks

    @GetMapping("/buy_goods/v7")
    public String buy_GoodsV7() throws Exception {
        String key = "redisLock";
        String value = UUID.randomUUID().toString()+Thread.currentThread().getName();

        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key,value,10L,TimeUnit.SECONDS);

            if(!flagLock)
            {
                return "Lock grabbing failed,o(╥﹏╥)o";
            }

            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
                return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
            }else{
                System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
            }
            return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
        } finally {
            Jedis jedis = RedisUtils.getJedis();
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                    "then " +
                    "return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";

            try {
                Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
                if ("1".equals(result.toString())) {
                    System.out.println("------del REDIS_LOCK_KEY success");
                }else{
                    System.out.println("------del REDIS_LOCK_KEY error");
                }
            } finally {
                if(null != jedis) {
                    jedis.close();
                }
            }
        }
    }

Here, our basic redis lock is formed. Generally, the company writes that it is not much different here.

Problem: at this time, we should ensure that the running time of business logic is shorter than the expiration time of our locked key. If the running time of business logic is longer than the expiration time of our lock, the lock will disappear again.

Version 8: using reission can not only solve all the previous problems, but also the watchDog of reission can refresh the expiration time of the lock regularly.

    @Autowired
    private Redisson redisson;


    @GetMapping("/buy_goods/v8")
    public String buy_GoodsV8()
    {
        String key = "redisLock";

        RLock redissonLock = redisson.getLock(key);
        redissonLock.lock();

        try
        {
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if(goodsNumber > 0)
            {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
                System.out.println("You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort);
                return "You have successfully killed the goods, and there are still:" + realNumber + "piece"+"\t Server port:"+serverPort;
            }else{
                System.out.println("The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort);
            }
            return "The goods have been sold out/End of activity/Call timeout, welcome next time"+"\t Server port:"+serverPort;
        }finally {
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
            {
                redissonLock.unlock();
            }
        }
    }

Key logic of lock refresh:

![image.png](/img/bVcXV6s)

Above, we have completed the preparation of stand-alone redis lock.

Problem: now we all use redis with master-slave structure. When the data of the master node has not been synchronized to the slave node, the master node of redis goes down, and the lock will still be lost.

4. Multiple redis distributed locks

Let's repeat the problem described above:

When the user calls the master node of redis and locks successfully, the master node hangs up before the master node can synchronize data to the slave node, resulting in the loss of locks, and subsequent threads start locking again, resulting in dirty data.

Solution: Redlock algorithm!

Locks are maintained by multiple redis (all primary nodes). If one of them fails, other redis can reveal the truth, and the lock still exists. RedLock algorithm is an effective solution to realize highly reliable distributed lock, which can be used in practical development.

@Configuration
public class RedisConfig extends CachingConfigurerSupport {


    /**
     * @param lettuceConnectionFactory
     * @return redis Serialized tool configuration class. Please enable the following configuration
     * 127.0.0.1:6379> keys *
     * 1) "ord:102"  Serialized
     * 2) "\xac\xed\x00\x05t\x00\aord:102"   Wild, not serialized
     */
    @Bean
    public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //Set key serialization method string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //Set the serialization method of value json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }


    @Bean
    public Redisson redisson()
    {
        Config config = new Config();

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

        return (Redisson) Redisson.create(config);
    }


    @Bean
    public Redisson redissonClient1()
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.140:6380").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

    @Bean
    public Redisson redissonClient2()
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.140:6381").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

    @Bean
    public Redisson redissonClient3()
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.140:6382").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

}
public class RedLockController {


    public static final String CACHE_KEY_REDLOCK = "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);
        //The three locks converge into redLock
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        boolean isLock;
        try {
            //The waiting time of the waitTime lock is handled. Normally, wait for 5s
            //leaseTime is the expiration time of the redis key. Normally, wait for 5 minutes.
            isLock = redLock.tryLock(5, 300, TimeUnit.SECONDS);
            if (isLock) {
                //TODO if get lock success, do something;
                //Pause the thread for 20 seconds
                try {
                    TimeUnit.SECONDS.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // In any case, unlock it in the end
            redLock.unlock();
            System.out.println(Thread.currentThread().getName() + "\t" + "redLock.unlock()");
        }
    }

}

5. Summary

This time we talked about the evolution of distributed locks

No lock - > synchronized single machine lock - > single machine redis distributed lock - > multi machine redis distributed lock.

Keywords: Redis Distributed lock

Added by darga333 on Thu, 17 Feb 2022 08:24:09 +0200