Manual implementation of Redis distributed lock

Problems in manually implementing Redis distributed locks

  • What should I do if the lock is released by others?
  • The defined expiration time is too long or too short, how to define it?
  • How to deal with the release of the lock is not an atomic operation?

Small scale chopper

First of all, we need to define the implementation of the lock, that is, the following getLock method, which uses RedisTemplate and does not define tool classes.

The getLock method acquires the lock

The getLock method uses the SETNX command of redis, but since the SETNX command cannot perform atomic operation expiration time, we use the SET command here. Starting from redis2.6.12, the behavior of the SET command can be modified by a series of parameters:

  • EX seconds : Set the key's expiration time to seconds seconds. The effect of executing SET key value EX seconds is equivalent to executing SETEX key seconds value.
  • PX milliseconds : Sets the key's expiration time to milliseconds milliseconds. The effect of executing SET key value PX milliseconds is equivalent to executing PSETEX key milliseconds value.
  • NX : Set the key only if the key does not exist. The effect of executing SET key value NX is equivalent to executing SETNX key value.
  • XX : Set the key only if the key already exists.

The advantage of using the SET LOCK ID EX 30 NX command at the same time is that the unique ID can be added. Will talk about it later.

The release method releases the lock

When acquiring the lock, we set the key, which is the name of the lock, and the ID is the value of the key-value pair. The advantage is that there is a problem when releasing the lock. For example: when thread A acquires the lock to perform business logic, thread B's misoperation directly releases the lock. In order to solve similar abnormal situations, we bind the current thread ID to the lock.
The steps and ideas for designing this method are as follows:

  • First, when releasing the lock, the current thread information needs to be passed in, because the value of the lock is bound to the thread id, and the name of the lock needs to be passed in at the same time.
  • Whether the acquired value of the current lock is consistent with the current ID of the thread that needs to release the lock.
  • If they are consistent, they can be released, but if they are inconsistent, they cannot be released.

The problem is here again, the get+del command here is two commands again, and we have to return to the problem of atomicity

  • Client 1 executes the get command to determine whether the lock is its own
  • Client 2 executes the set command and acquires the lock forcibly
  • Client 1 executes the del command and deletes the lock of client 2

So we used the feature that redis can execute lua scripts, and wrote a script to execute atomically to solve this problem. The specific implementation is shown in the following code:

@Component
public class RedisInfo {
    
    

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    /**
     * Title:getLock <br>
     * Description:获取锁的实现 <br>
     * author:于琦海 <br>
     * date:2022/10/25 10:58 <br>
     * @param key 锁的名称
     * @param time 预估的过期时间
     * @param timeUnit 时间单位
     * @param thread 当前操作获取锁的线程
     * @return Boolean
     */
    public Boolean getLock(String key, Long time, Thread thread, TimeUnit timeUnit) {
    
    
        return redisTemplate.opsForValue().setIfAbsent(key, thread.getId(), time, timeUnit);
    }

    /**
     * Title:release <br>
     * Description:释放锁的原子实现 <br>
     * author:于琦海 <br>
     * date:2022/10/25 11:12 <br>
     * @param key 锁的名称
     * @param thread 当前操作释放锁的线程
     * @return Long
     */
    public Long release(String key, Thread thread) {
    
    
        String lua = " if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setResultType(Long.class);
        script.setScriptText(lua);
        return redisTemplate.execute(script, Arrays.asList(key), thread.getId());
    }

    /**
     * Title:ttl <br>
     * Description:查询key的剩余过期时间 <br>
     * author:于琦海 <br>
     * date:2022/10/25 14:13 <br>
     * @param key 锁的名称
     * @param timeUnit 时间单位
     * @return Long
     */
    public Long ttl(String key, TimeUnit timeUnit) {
    
    
        return redisTemplate.getExpire(key, timeUnit);
    }

    /**
     * Title:expire <br>
     * Description:更新key的过期时间 <br>
     * author:于琦海 <br>
     * date:2022/10/25 14:33 <br>
     * @param key 锁的名称
     * @param time 预估的过期时间
     * @param timeUnit 时间单位
     * @return Boolean
     */
    public Boolean expire(String key, long time, TimeUnit timeUnit) {
    
    
        return redisTemplate.expire(key, time, timeUnit);
    }
}

Renewal issue

Suppose we set the holding time of the lock to 30s, but due to network problems or machine problems, the execution of my business logic exceeds 30s, and the lock has expired at this time, there will be problems in this situation. Then if I set it to a super large time, such as 100s, it will definitely be executed, and the efficiency of the lock will become very low at this time.

According to this situation, we can write a daemon thread to renew the lock according to the remaining time, similar to Redisson's watchdog mechanism.

The idea is as follows:

  • First start a daemon thread.
  • Get the remaining expiration time of the current lock.
  • If the remaining time of the lock is less than or equal to one-third of the lock expiration time I set (I set 30s, divided by 3, which is 10s).
  • Renew the current lock.
@Service
public class RedisLockDemo {
    
    

    @Autowired
    private RedisInfo redisInfo;

    /**
     * Title:businessInfo <br>
     * Description:这里面是业务逻辑伪代码 <br>
     * author:于琦海 <br>
     * date:2022/10/25 11:34 <br>
     */
    public void businessInfo() {
    
    
        // 获取锁
        Boolean lock = redisInfo.getLock("qhyu", 30L, Thread.currentThread(), TimeUnit.SECONDS);
        if (lock) {
    
    
            try {
    
    
                // 开启一个守护线程,去刷新过期时间,实现看门狗的机制
                Runnable thread = () -> {
    
    
                    while (true) {
    
    
                        // 会不会
                        // 获取key的剩余过期时间
                        Long timeLess = redisInfo.ttl("qhyu", TimeUnit.SECONDS);
                        // 当时间小于等于10s的时候,续命
                        if (timeLess <= 10L) {
    
    
                            System.out.println("续命");
                            redisInfo.expire("qhyu", 30L, TimeUnit.SECONDS);
                        }
                    }
                };
                Thread thread1 = new Thread(thread);
                thread1.setDaemon(true);
                thread1.start();
                int i = 0;
                while (true) {
    
    
                    if (i < 50) {
    
    
                        i++;
                        // 执行我们的业务逻辑
                        System.out.println("这里是我的业务逻辑" + i);
                        Thread.sleep(10000L);
                    } else {
    
    
                        break;
                    }
                }
            } catch (Exception e) {
    
    
                throw new RuntimeException(e);
            } finally {
    
    
                redisInfo.release("qhyu", Thread.currentThread());
            }
        }
    }
}

test

According to the above code, I wrote a controller and used it to achieve the purpose of automatic renewal.

@RestController
@RequestMapping(("/qhyu/redis"))
public class RedisLockController {
    
    

    @Autowired
    private RedisLockDemo redisLockDemo;

    @GetMapping("/test")
    public void test(){
    
    
        redisLockDemo.businessInfo();
    }
}

When the clock is normal, we expect that the life will be renewed once without executing the business logic twice. The following results prove this point.
insert image description here
So if it's not a daemon thread, can't you just open a thread directly? It is definitely not possible, because when the thread execution is completed, the newly opened thread will not be destroyed, as shown below, it will keep renewing the lock, and problems will arise.
insert image description here

Guess you like

Origin blog.csdn.net/Tanganling/article/details/127517358