Dark Horse Review: Distributed Lock

distributed lock

Comparison of basic principles and implementation methods

Distributed lock: A lock that is visible and mutually exclusive to multiple processes in a distributed system or cluster mode.

The core idea of ​​distributed lock is to let everyone use the same lock. As long as everyone uses the same lock, then we can lock the thread, prevent the thread from proceeding, and let the program execute serially. This is the core of distributed lock train of thought

image-20230202114754537

So what kind of conditions should the distributed lock meet?

Visibility: Multiple threads can see the same results. Note: The visibility mentioned here is not the memory visibility referred to in concurrent programming, but the meaning that changes can be perceived between multiple processes

Mutual exclusion: Mutual exclusion is the most basic condition of distributed locks, making programs execute serially

High availability: the program is not easy to crash, and high availability is guaranteed at all times

High performance: Since locking itself reduces performance, all distributed locks require higher locking performance and lock release performance

Security: Security is also an integral part of the program

image-20230202114839947

There are three common distributed locks

Mysql: mysql itself has a lock mechanism, but because of the general performance of mysql, it is rare to use mysql as a distributed lock when using distributed locks

Redis: Redis as a distributed lock is a very common way to use it. Now enterprise-level development basically uses redis or zookeeper as a distributed lock. Using the setnx method, if the key is inserted successfully, it means that the lock has been obtained. If someone If the insertion is successful, if other people fail to insert, it means that the lock cannot be obtained. Use this logic to implement distributed locks

Zookeeper: Zookeeper is also a better solution for implementing distributed locks in enterprise-level development. Since this set of videos does not explain the principle of zookeeper and the implementation of distributed locks, it will not be elaborated.

image-20230202114922236

The core idea of ​​Redis distributed lock implementation

There are two basic methods that need to be implemented when implementing distributed locks:

  • Acquire the lock:

    • Mutual exclusion: ensures that only one thread can acquire the lock
    • Non-blocking: try once, return true on success, false on failure
  • Release the lock:

    • manual release
    • Timeout release: add a timeout when acquiring a lock

Core ideas:

We use the setNx method of redis. When multiple threads enter, we use this method. When the first thread enters, there is this key in redis, and it returns 1. If the result is 1, it means that he has grabbed the key. lock, then he goes to execute the business, then deletes the lock, exits the lock logic, if the buddy who has not grabbed the lock, just wait for a certain period of time and try again

image-20230202144409873

Summarize:

image-20230202144525590

Implement the primary version of distributed lock

Code

lock interface

The basic interface of the lock : ILock

package com.hmdp.utils;

public interface ILock {
    
    

    /**
     * 尝试获取锁
     * @param timeoutSec    锁持有的超市时间,过期后自动释放
     * @return  TRUE 代表获取锁成功
     */
    boolean tryLock(Long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

Lock implementation class

Lock interface implementation class : SimpleRedisLock

  • Use the setnx method to lock and increase the expiration time to prevent deadlock. This method can ensure that locking and increasing the expiration time are atomic

  • Release locks to prevent deletion of other people's locks

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{
    
    

    private String name;

    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
    
    
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(Long timeoutSec) {
    
    
        //获取线程标识
        long threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        //自动拆箱,防止空指针
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
    
    
        //通过del删除锁
        stringRedisTemplate.delete(KEY_PREFIX+name);
    }
}

Modify business code

Modify: VoucherOrderServiceImpl

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result seckillVoucher(Long voucherId) {
    
    

        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            return Result.fail("库存不足!");
        }

        Long userId = UserHolder.getUser().getId();
        //创建锁对象
        //"order:" 所有用户下单都会锁
        //"order:" + userId 锁的范围使用户,同一个用户才加锁
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        boolean isLock = lock.tryLock(10L);
        if (!isLock) {
    
    
            return Result.fail("不允许重复下单");
        }
        try {
    
    
            // 获取代理对象 事务
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
    
    
            //释放锁
            lock.unlock();
        }

    }

test

  1. build environment

    • idea: two services running at the same time

      image-20230202155212644

    • Nginx up and running

    • jmeter starts (because authorization has been configured before, it is convenient to use it for testing)

    • Data recovery

      • Inventory: 100
      • Orders: 0
  2. start jmeter test

    • Same as the previous configuration, the port and path are unchanged
  3. success

    • jmeter

      image-20230202155659913

    • mysql

      image-20230202155825581

Explanation of Redis Distributed Lock Misdeletion

Logical explanation:

The thread holding the lock is blocked inside the lock, causing its lock to be released automatically. At this time, other threads, thread 2, try to acquire the lock and get the lock. Then thread 2 is holding the lock during execution. Thread 1 reacts and continues to execute. During the execution of thread 1, it reaches the logic of deleting locks. At this time, it will delete the lock that should belong to thread 2. This is the description of the situation where someone else's lock is accidentally deleted.

solution:

The solution is to judge whether the current lock belongs to itself when each thread releases the lock. If it belongs to itself, the lock will not be deleted. Assuming the above situation is still the case, thread 1 is stuck, the lock is automatically released, and the thread 2 Enter the internal execution logic of the lock. At this time, thread 1 reacts and deletes the lock. However, thread 1 sees that the current lock does not belong to itself, so it does not perform the lock deletion logic. When thread 2 reaches the lock deletion logic , if the time point of automatically releasing the lock has not been stuck, then it is judged that the current lock belongs to itself, so delete the lock.

image-20230202162258484

Solve the problem of mistaken deletion of Redis distributed lock

Requirement: Modify the previous distributed lock implementation to meet the requirements: when acquiring the lock, store the thread ID (which can be represented by UUID); when
releasing the lock, first obtain the thread ID in the lock, and judge whether it is consistent with the current thread ID

  • Release lock if consistent
  • If inconsistent, the lock is not released

Core logic: When storing the lock, put the ID of your own thread. When deleting the lock, judge whether the current lock ID is stored by yourself. If it is, delete it. If not, don’t delete it.

image-20230202164243907

Code

lock

    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(Long timeoutSec) {
    
    
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        //自动拆箱,防止空指针
        return Boolean.TRUE.equals(success);
    }

release lock

@Override
    public void unlock() {
    
    
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的线程标识
        String lockId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //判断表示是否一致
        if (threadId.equals(lockId)) {
    
    
            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }

test

Instructions for code operation:

After we modify the code here, we restart the project, and then start two threads. After the first thread holds the lock, it manually releases the lock. The second thread enters the lock at this time, and then releases the first thread. At this time, the first thread cannot release the lock because the value of the lock is not its own, so it cannot delete other people's locks. At this time, the second thread can release the lock correctly. This case initially shows that we have solved the problem of accidental lock deletion. The problem.

  1. build environment

    • idea: break point, modify expiration time (longer, convenient for testing)

      image-20230202170420506

    • postman:request、authorization

      image-20230202170905543

    • Redis client

    • mysql: restore data

  2. 8081 Successfully acquired the lock, manually delete and release the lock

    image-20230202171500526

  3. 8083 Successfully acquired the lock

    image-20230202171824283

  4. 8081 Failed to release lock

    image-20230202172232544

  5. 8083 successfully released the lock

    image-20230202172610634

  6. test passed

Atomicity of distributed locks

More extreme mistaken deletion logic description:

After thread 1 now holds the lock, in the process of executing business logic, he is preparing to delete the lock, and has reached the process of conditional judgment. For example, he has obtained the current lock that really belongs to him and is preparing to delete it Lock, but at this time his lock expires, then thread 2 comes in at this time, but thread 1 will continue to execute later, when he is stuck, he will directly execute the line of code that deletes the lock, which is equivalent to the condition The judgment did not work. This is the atomicity problem when deleting locks. The reason for this problem is that thread 1's lock acquisition, lock comparison, and lock deletion are actually not atomic. We need to prevent the situation occurs,

Graphic:

image-20230203112515288

Lua script solves the atomicity problem of multiple commands

Redis provides the Lua scripting function to write multiple Redis commands in one script to ensure the atomicity of multiple commands execution. Lua is a programming language. For its basic grammar, you can refer to the website: https://www.runoob.com/lua/lua-tutorial.html. Here we will focus on the calling functions provided by Redis. We can use lua to operate redis , and can guarantee its atomicity, so that it is an atomic action to take a lock than to delete a lock. As a Java programmer, there is no simple requirement, and you don’t need to be too proficient. You only need to know that he has Whatever works.

Here we focus on the call function provided by Redis, the syntax is as follows:

redis.call('命令名称', 'key', '其它参数', ...)

For example, if we want to execute set name jack, the script is as follows:

# 执行 set name jack
redis.call('set', 'name', 'jack')

For example, we need to execute set name Rose first, and then execute get name, the script is as follows:

# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

After writing the script, you need to use the Redis command to call the script. The common commands for calling the script are as follows:

image-20230203152128607

For example, we want to execute the redis.call('set', 'name', 'jack') script, the syntax is as follows:

image-20230203152826268

If the key and value in the script do not want to be hardcoded, they can be passed as parameters. Key type parameters will be placed in the KEYS array, and other parameters will be placed in the ARGV array. These parameters can be obtained from the KEYS and ARGV arrays in the script:

image-20230203153821471

Next, let's go back and forth about our logic for releasing locks:

The business process for releasing the lock is as follows

1. Obtain the thread mark in the lock

2. Determine whether it is consistent with the specified mark (current thread mark)

3. If consistent, release the lock (delete)

4. If inconsistent, do nothing

If it is represented by Lua script, it is like this:

image-20230203160226696

In the end, the lua script that we operate redis to lock and delete locks will become like this

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

Use Java code to call Lua script to transform distributed lock

The lua script itself does not require you to spend too much time on research, you only need to know how to call it and what it means roughly, so the meaning of these lua expressions will not be explained in detail in the notes.

In our RedisTemplate, we can use the execute method to execute lua scripts, and the parameter correspondence is as shown in the figure below

image-20230203165549974

Code

  1. lua script

    -- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
    -- 获取锁中的标示,判断是否与当前线程标示一致
    if (redis.call('GET', KEYS[1]) == ARGV[1]) then
      -- 一致,则删除锁
      return redis.call('DEL', KEYS[1])
    end
    -- 不一致,则直接返回
    return 0
    

    File location:

    image-20230203171309672

  2. java code

        private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    
        static{
          
          
            UNLOCK_SCRIPT = new DefaultRedisScript<>();
            UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
            UNLOCK_SCRIPT.setResultType(Long.class);
        }   
    	@Override
        public void unlock() {
          
          
            //调用lua脚本
            stringRedisTemplate.execute(
                    UNLOCK_SCRIPT,
                    Collections.singletonList(KEY_PREFIX+name),
                    ID_PREFIX+Thread.currentThread().getId()
            );
        }
    

    analyze:

    image-20230203172148016

test

Test logic:

The first thread comes in, gets the lock, manually deletes the lock, simulates the lock timeout, and other threads will execute lua to grab the lock. When the thread uses lua to delete the lock on the first day, lua can guarantee that he cannot delete his lock. When two threads delete the lock, using lua can also ensure that other people's locks will not be deleted, and at the same time ensure atomicity.

  1. prepare the environment

    • idea
    • postman
    • mysql
    • Redis
    • Nginx (I use the front-end request path to forward to the back-end via Nginx)
  2. 8081 Successfully acquired the lock

    image-20230203200442450

  3. Manually delete the lock of 8081 to simulate failure under special circumstances

  4. 8083 Successfully acquired the lock

    image-20230203200826052

  5. 8081 Failed to delete lock

    image-20230203201142683

  6. 8083 Delete lock successfully

    image-20230203201419773

  7. database is ok

    image-20230203201624951

  8. test passed

Summarize

Redis-based distributed lock implementation ideas:

  • Use set nx ex to acquire the lock, set the expiration time, and save the thread mark
  • When releasing the lock, first judge whether the thread mark is consistent with itself, and delete the lock if it is consistent
    • characteristic:
      • Use set nx to satisfy mutual exclusion
      • Use set ex to ensure that the lock can still be released when a fault occurs, avoiding deadlock and improving security
      • Use Redis cluster to ensure high availability and high concurrency

Small summary:

We have been going all the way, using the addition of expiration time to prevent the occurrence of deadlock problems, but after the expiration time, there may be the problem of accidentally deleting other people's locks. This problem we started by using locks before deletion, comparing locks, and deleting locks This logic is used to solve it, that is, to judge whether the current lock belongs to you before deleting, but there is still an atomicity problem, that is, we cannot guarantee that taking a lock is an atomic action compared to locking and deleting a lock. Finally Solve this problem through lua expressions

But there is still one problem that cannot be locked. What is it that cannot be locked? If you think about it, if the expiration time is up, we can renew it, for example, for 30 seconds, just like Internet cafes. After the fee was paid, he said, come on, the network administrator, give me another 10 yuan, will all the problems behind it not happen? So how to solve the renewal problem? Can rely on us to learn redission next

Distributed lock-redission

Distributed lock-redission function introduction

The distributed lock based on setnx has the following problems:

Reentrancy problem : The reentrancy problem means that the thread that acquires the lock can enter the code block of the same lock again. The significance of reentrant lock is to prevent deadlock. For example, in code such as HashTable, his method is to use synchronized Modified, if he calls another method in one method, then if it is not reentrant at this time, wouldn't it be a deadlock? So the main significance of reentrant locks is to prevent deadlocks. Both our synchronized and Lock locks are reentrant.

Non-retryable : It means that the current distribution can only try once. We think that the reasonable situation is: when the thread fails to acquire the lock, he should be able to try to acquire the lock again.

**Timeout release:** We increase the expiration time when locking, so that we can prevent deadlock, but if the stuck time is too long, although we use lua expressions to prevent deleting others by mistake The lock, but it is not locked after all, there is a security risk

Master-slave consistency: If Redis provides a master-slave cluster, when we write data to the cluster, the host needs to asynchronously synchronize the data to the slave, and if the host crashes before the synchronization passes, a deadlock will occur question.

image-20230204145448694

What is Redission

Redisson is a Java in-memory data grid (In-Memory Data Grid) implemented on the basis of Redis. It not only provides a series of distributed common Java objects, but also provides many distributed services, including the implementation of various distributed locks.

Redission provides a variety of functions for distributed locks

image-20230204145546914

Distributed Lock-Redission Quick Start

Code

  1. Introduce dependencies

    <dependency>
    	<groupId>org.redisson</groupId>
    	<artifactId>redisson</artifactId>
    	<version>3.13.6</version>
    </dependency>
    
  2. Configure the Redisson client

    package com.hmdp.config;
    
    import org.redisson.Redisson;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class RedissonConfig {
          
          
    
        @Bean
        public RedissonClient redissonClient(){
          
          
            //配置
            Config config = new Config();
            //这里由于虚拟机打开耗时且麻烦,使用本地Redis,未设置密码
            config.useSingleServer().setAddress("redis://127.0.0.1:6379");
            //创建RedisClient对象
            return Redisson.create(config);
        }
    }
    
  3. Modify: VoucherOrderServiceImpl

    	//注入RedissonClient
    	@Resource
    	private RedissonClient redissonClient;
    
    	@Override
        public Result seckillVoucher(Long voucherId) {
          
          
    
            // 1.查询优惠券
            SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
            // 2.判断秒杀是否开始
            if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
          
          
                return Result.fail("秒杀尚未开始!");
            }
            // 3.判断秒杀是否结束
            if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
          
          
                return Result.fail("秒杀已经结束!");
            }
            // 4.判断库存是否充足
            if (voucher.getStock() < 1) {
          
          
                return Result.fail("库存不足!");
            }
    
            Long userId = UserHolder.getUser().getId();
            //创建锁对象
            //"order:" 所有用户下单都会锁
            //"order:" + userId 锁的范围使用户,同一个用户才加锁
    //        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
            RLock lock = redissonClient.getLock("lock:order:" + userId);
            boolean isLock = lock.tryLock();
            if (!isLock) {
          
          
                return Result.fail("不允许重复下单");
            }
            try {
          
          
                // 获取代理对象 事务
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            } finally {
          
          
                //释放锁
                lock.unlock();
            }
    
        }
    

How to use Redission's distributed lock

Write a test class:

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    
    
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
    
    
        try{
    
    
            System.out.println("执行业务");          
        }finally{
    
    
            //释放锁
            lock.unlock();
        }
        
    }   
}

test

Possible problems: Distributed lock Redisson entry case error: java.io.IOException: The remote host forcibly closed an existing connection

  1. Environment build

    • idea: start only one service (8081)
    • postman
    • mysql: restore data, stock: 100, order 0
    • Redis
    • jmeter

    image-20230204221713040

  2. start jmeter test

    jmeter

    image-20230204221932017

    mysql

    image-20230204222038202

  3. success

Distributed lock-redission reentrant lock principle

In the Lock lock, he uses a state variable of a voaltile at the bottom to record the state of reentry. For example, if no one currently holds the lock, then state=0, if someone holds the lock, then state =1, if the person who holds this lock holds this lock again, then the state will be +1, if it is for synchronized, he will have a count in the c language code, the principle is similar to state, and it is also important Add 1 for one entry, -1 for one release, until it decreases to 0, indicating that the current lock is not held by anyone.

In redission, we also support reentrant locks

In distributed locks, he uses a hash structure to store locks, where the big key indicates whether the lock exists, and the small key indicates which thread currently holds the lock, so let's analyze the current lock together lua expression

There are 3 parameters in this place

KEYS[1] : lock name

ARGV[1]: lock expiration time

ARGV[2]: id + ":" + threadId; the small key of the lock

exists: Determine whether the data exists name: whether the lock exists, if ==0, it means that the current lock does not exist

redis.call('hset', KEYS[1], ARGV[2], 1); At this point, he starts to write data into redis and write it into a hash structure

Lock{

​ id + “:” + threadId : 1

}

If the current lock exists, the first condition is not satisfied, and then judge

redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1

At this time, you need to use the big key + small key to judge whether the current lock belongs to you. If it is your own, then proceed

redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)

Add +1 to the value of the current lock, redis.call('pexpire', KEYS[1], ARGV[1]); and then set an expiration time for it. If the above two conditions are not met, it means that the current lock Fail to grab the lock, and finally return pttl, which is the expiration time of the current lock

If you guys look at the previous source code, you will find that he will judge whether the return value of the current method is null. If it is null, it corresponds to the conditions corresponding to the first two ifs, and exits the lock grab logic. It is not null, that is, the third branch is taken, and the while(true) spin lock will be performed at the source code.

"if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', 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]);"

image-20230209200049809

Distributed lock-redission lock retry and WatchDog mechanism

Explanation : Since the source code analysis of tryLock and its watchdog principle have been explained in the course, the author will analyze the source code analysis of the lock() method for you here, and hope that you can master more knowledge during the learning process

In the process of grabbing the lock, the current thread is obtained, and the lock is grabbed through tryAcquire. The logic of the lock grabbing is the same as the previous logic

1. First judge whether the current lock exists, if not, insert a lock and return null

2. Determine whether the current lock belongs to the current thread, and if so, return null

So if the return is null, it means that the current buddy has finished grabbing the lock, or the reentry is complete, but if the above two conditions are not met, then enter the third condition, and the return is the expiration time of the lock, classmates We can scroll down a little bit by ourselves, and you can find that there is a while (true) to perform tryAcquire again to grab the lock

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
    
    
    return;
}

Next there will be a conditional branch, because the lock method has overloaded methods, one with parameters and one without parameters, if the value passed in with parameters is -1, if the parameter is passed in, leaseTime is itself, So if the parameter is passed in, leaseTime != -1 will go in and grab the lock at this time. The logic of grabbing the lock is the three logics mentioned before

if (leaseTime != -1) {
    
    
    return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}

If there is no incoming time, the lock will also be grabbed at this time, and the locked time is the default watchdog time commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()

ttlRemainingFuture.onComplete((ttlRemaining, e) This sentence is equivalent to monitoring the above lock grabbing, that is to say, after the above lock grabbing is completed, this method will be called. The specific logic of the call is to open a thread in the background and perform Renewal logic, that is, watchdog thread

RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                        commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                        TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
    
    
    if (e != null) {
    
    
        return;
    }

    // lock acquired
    if (ttlRemaining == null) {
    
    
        scheduleExpirationRenewal(threadId);
    }
});
return ttlRemainingFuture;

This logic is the renewal logic, pay attention to the commandExecutor.getConnectionManager().newTimeout() method

Method( new TimerTask() {}, parameter 2, parameter 3)

It refers to: use parameter 2 and parameter 3 to describe when to do the thing of parameter 1, the current situation is: do the thing of parameter 1 after 10s

Because the expiration time of the lock is 30s, after 10s, the timeTask is triggered at this time, and he will renew the contract, and renew the current lock to 30s. If the operation is successful, then it will call itself recursively at this time. Then set a timeTask() again, so after another 10s, set a timerTask again to complete the non-stop renewal

Then everyone can think about it, if our thread goes down, will he renew the contract? Of course not, because no one will call the renewExpiration method, so it will be released after the time.

private void renewExpiration() {
    
    
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
    
    
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
    
    
        @Override
        public void run(Timeout timeout) throws Exception {
    
    
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
    
    
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
    
    
                return;
            }
            
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
    
    
                if (e != null) {
    
    
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {
    
    
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

MutiLock principle of distributed lock-redission lock

In order to improve the availability of redis, we will build a cluster or master-slave, now take the master-slave as an example

At this time, we write commands on the master, and the master will synchronize the data to the slave, but if the master has not had time to write the data to the slave, the master is down at this time, and the sentinel will find that the master is down machine, and elect a slave to become the master. At this time, there is actually no lock information in the new master, and the lock information has been lost at this time.

Illustration :

image-20230209200257008

In order to solve this problem, redission proposed the MutiLock lock. With this lock, we do not use master-slave. The status of each node is the same. The logic of locking this lock needs to be written to each master cluster node In general, only if all the servers are successfully written, then the locking is successful. Suppose a node is down now, then when he goes to obtain the lock, as long as there is a node that cannot get it, it cannot be regarded as a successful locking. This ensures the reliability of locking.

Illustration :

image-20230209200337370

So what is the principle of MutiLock locking? I drew a picture to illustrate

When we set up multiple locks, redission will add multiple locks to a collection, and then use the while loop to try to get the locks, but there will be a total locking time, which is required to add The number of locks * 1500ms, assuming that there are 3 locks, then the time is 4500ms, assuming that within this 4500ms, all the locks are successfully locked, then the locking is considered successful at this time, if there is a thread that fails to lock within 4500ms, It will try again.

Illustration :

image-20230209200418466

Summarize

  • Distributed lock principle
  • Redis's String structure implements distributed locks
  • The problem of accidental deletion of locks
  • Atomic operation problem of lock
  • Lua script solves atomicity problem
  • Lua syntax
  • Lua script for redis
  • RedisTemplate calls Lua script
  • Redisson distributed lock
  • Hash structure solves the reentrant problem of locks
  • Publish subscription combined with semaphore to solve lock retry problem
  • watchDog solves the problem of lock timeout release

Redis distributed lock :

Guess you like

Origin blog.csdn.net/Htupc/article/details/128961107