King's Solution in Distributed Locks - Redission



insert image description here

5.1 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 deletion of other people's locks by mistake, but after all Not locked, 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.

insert image description here

So 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

insert image description here


5.2 Distributed Lock - Redission Quick Start

Import dependencies:

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.13.6</version>
</dependency>

Configure the Redisson client:

@Configuration
public class RedissonConfig {
    
    

    @Bean
    public RedissonClient redissonClient(){
    
    
        // 配置
        Config config = new Config();
        //这里添加的单点的地址,也可以使用config.useClusterServer 添加集群地址
        config.useSingleServer().setAddress("redis://192.168.150.101:6379")
            .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

How to use Redission's distributed lock

@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();
        }
        
    }
}

在 VoucherOrderServiceImpl

Inject 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();
        //创建锁对象 这个代码不用了,因为我们现在要使用分布式锁
        //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();
        }
 }

5.3 Principle of distributed lock-redission reentrant lock

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 time, he starts to write data into redis, and writes 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]);"

insert image description here

5.4 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);
}

5.5 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.

insert image description here

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.

insert image description here

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.

insert image description here



insert image description here



Guess you like

Origin blog.csdn.net/m0_60915009/article/details/131979741