Comprehensive analysis of redis distributed locks

Preface

Let’s learn about distributed locks today. Distributed locks are common in the cluster environment. They are used to solve problems that cannot be solved by single-machine locks, such as the scenario of deducting inventory. If the business machine deducting inventory is deployed in multiple units, it will appear. Oversold phenomenon (the common lock and Synchronized in JAVA are both stand-alone locks), at this time distributed locks need to be introduced.

There are many implementations of distributed locks. The most common ones are through redis and zookeeper. Today, let's implement distributed locks through redis.

Distributed lock

Redis distributed lock related video explanation: learning video

Redis implements distributed locks, so we might as well think about how to implement it with redis. The condition is: this operation is mutually exclusive, and there is exactly one instruction in redis that can achieve the effect of mutual exclusion, namely the setnx(k) instruction, the meaning of this instruction That is, if the specified k does not exist, the execution is successful. If the specified k already exists in redis, it returns false, which can perfectly achieve mutual exclusion between multiple machines (eg: existing 5 machines, the first machine After executing setnx("lock"), other machines will fail to execute setnx("lock") until the first machine releases this key).

Code demo

Simply simulate a scenario where an order is created to deduct inventory

    @Autowired
    private RedisTemplate redisTemplate;
    
    private final String LOCKKEY="DistributedLock";
    
    @PostMapping("/createOrder")
    public String createOrder(String goodsId){
        //尝试创建锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value");
        if (flag){
            if (!StringUtils.isEmpty(goodsId)){
                Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
                if (stock>0){
                    stock--;
                    redisTemplate.opsForValue().set(goodsId,stock);
                    //......创建订单
                    //释放锁
                    redisTemplate.delete(LOCKKEY);
                    return "创建订单成功";
                }else {
                    redisTemplate.delete(goodsId);
                    //释放锁
                    redisTemplate.delete(LOCKKEY);
                    return "库存不足";
                }
            }else {
                //释放锁
                redisTemplate.delete(LOCKKEY);
                return "库存不足";
            }
        }else {
            //对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
            return "系统繁忙,请稍后重试";
        }
    }

Now we have implemented the simplest version of distributed locks, but there are still many problems in it. Below we will solve these problems step by step and optimize them.

Solve the problem

Problem 1: The lock caused by the program exception is not released

Problem description: It is very likely that an abnormal situation occurred during the execution of the business, resulting in the code not being executed until the lock is released. For this situation, we need to improve the code, the code is as follows:

@SuppressWarnings("all")
@Controller
public class Lock {
    @Autowired
    private RedisTemplate redisTemplate;
    private final String LOCKKEY="DistributedLock";
    @PostMapping("/createOrder")
    public String createOrder(String goodsId){
        //尝试创建锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value");
        if (flag){
            try {
                if (!StringUtils.isEmpty(goodsId)){
                    Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
                    if (stock>0){
                        stock--;
                        redisTemplate.opsForValue().set(goodsId,stock);
                        //......创建订单
                        return "创建订单成功";
                    }else {
                        redisTemplate.delete(goodsId);
                        return "库存不足";
                    }
                }else {
                    return "库存不足";
                }
            }finally {
                //释放锁,防止出现异常导致的锁无法释放场景
                redisTemplate.delete(LOCKKEY);
            }

        }else {
            //对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
            return "系统繁忙,请稍后重试";
        }
    }
}

Share more information on Linux back-end network infrastructure development to enhance the knowledge of the principles of learning Click on learning materials acquisition, improve technology stack, content knowledge, including Linux, Nginx, ZeroMQ, MySQL, Redis, thread pool, MongoDB, ZK, Linux kernel, CDN, P2P , Epoll, Docker, TCP/IP, coroutine, DPDK, etc.

Problem 2: Locks caused by downtime cannot be released

When the lock is successfully added, the server goes down and the lock cannot be released at this time. Therefore, we need to add an expiration time to the lock so that the lock will eventually be released even if it goes down.

  • Note: The operation of adding the lock expiration time must be an atomic operation with adding the lock, otherwise the server will be down when the lock is added and the expiration time is about to be added.

It happens that this atomic operation is supported in redisTemplate, an example is as follows:

Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value",20L, TimeUnit.SECONDS);

Problem 3: A thread releases the lock of B thread

This situation is also possible. After the A thread adds the lock, the business code has not been executed for some reasons. At this time, the lock is automatically released, and the B thread successfully acquires the lock. Before the B thread finishes executing, the A thread When the finally code block is executed, the lock of the B thread is released directly.

  • Solution: We can randomly generate a different value for each thread, and compare whether the value is the same before releasing
    @PostMapping("/createOrder")
    public String createOrder(String goodsId){
        //生成value
        String value = UUID.randomUUID().toString().replace("-", "");
        //尝试创建锁,并设置过期时间
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, value,20L, TimeUnit.SECONDS);
        if (flag){
            try {
                if (!StringUtils.isEmpty(goodsId)){
                    Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
                    if (stock>0){
                        stock--;
                        redisTemplate.opsForValue().set(goodsId,stock);
                        //......创建订单
                        return "创建订单成功";
                    }else {
                        redisTemplate.delete(goodsId);
                        return "库存不足";
                    }
                }else {
                    return "库存不足";
                }
            }finally {
                //释放锁,防止出现异常导致的锁无法释放场景
                Object o = redisTemplate.opsForValue().get(LOCKKEY);
                if (o!=null){
                    String redisValue=(String)o;
                    //当且仅当redis中的value和当前线程value相同时,释放锁
                    if (value.equals(redisValue)){
                        redisTemplate.delete(LOCKKEY);
                    }
                }
            }

        }else {
            //对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
            return "系统繁忙,请稍后重试";
        }
    }

Problem 4: The operation of releasing the lock is not atomic

We can see that in the code just now, the release of the lock operation is not atomic, it is an operation of querying and then deleting. There will also be problems in this, that is, the A thread queries and obtains the value of the lock. At this time, the value is also the same as A. The value of the thread is exactly the same. When the lock is about to be released, the lock automatically expires, the B thread successfully locks, and A releases B's lock again.

Solution:

  • The first way: through the Lua script to make the operation of releasing the lock atomic.
 		//释放锁,防止出现异常导致的锁无法释放场景
                //lua脚本保证原子性
                String script="if redis.call(‘get’,KEYS[1]) == ARGV[1]" +
                      "then" +
                      "    return redis.call(‘del’,KEYS[1])" +
                      "else" +
                      "    return 0" +
                      "end";
                Object eval = jedis.eval(script, Collections.singletonList(LOCKKEY),
                        Collections.singletonList(value));
                if ("0".equals(eval.toString())){
                    System.out.println("删除失败");
                }else {
                    System.out.println("删除锁成功");
                }
                jedis.close();
  • The second way: Introduce the transaction in redis. There is a watch command, which can monitor a key and start the transaction. If the monitored key is operated by other threads, then the submission is unsuccessful, because we have the key Performed a watch operation
//释放锁,防止出现异常导致的锁无法释放场景
//开启手动提交事务
redisTemplate.setEnableTransactionSupport(true);
//监听锁
redisTemplate.watch(LOCKKEY);
//开启事务
redisTemplate.multi();
if (value.equals(redisTemplate.opsForValue().get(LOCKKEY))){
          redisTemplate.delete(LOCKKEY);
}
//提交事务(如果在监听到现在提交这段时间,锁被其他线程占用,那么删除锁操作不会执行,否则删除锁成功)
redisTemplate.exec();   
//取消监听
redisTemplate.unwatch();

Question 5: Automatic lock failure and problems under the master-slave architecture

  • Automatic lock failure problem

You can review the question 3 and question 4 just now. In the final analysis, the reason is that the lock automatically expires, causing other threads to occupy the lock.

The automatic lock expiration is simply because the lock’s survival time is less than the business execution time. How to solve it? In fact, we can start another thread and let it increase the survival time for the lock regularly, and stop the thread when the lock is deleted.

  • Problems under the master-slave architecture

This problem is mainly reflected in the assumption that there are two master and slave libraries, and the lock is added to the master library successfully, and the data has not been synchronized to the slave library. At this time, the master library is down, and the slave library becomes the new master library, and a thread comes. In order to add a lock to the new main library, the addition is successful. At this time, two threads have successfully added the lock at the same time. How to solve it? You can set the delayed restart of the slave library, and let the slave library become the new master library when the lock expires.

to sum up

It can be seen that there are many points to pay attention to when implementing distributed locks through redis, and I have not written all of the points of attention, so it is still very difficult to implement a distributed lock by yourself, so we can actually It is enough to use distributed locks that have been packaged on the market. For example, the well-known Redission framework helps us realize redis distributed locks. The method of use is also very simple, just getlock directly, then lock and unlock.

Guess you like

Origin blog.csdn.net/Linuxhus/article/details/115144974