Realization and Application of Distributed Lock

Why do you need a lock

Solving data competition problems in concurrent scenarios in a multi-tasking environment

Java common lock

We can group and classify according to whether the lock contains a certain characteristic

  • From whether the thread locks the resource, the lock can be divided into optimistic lock and pessimistic lock
  • Whether the thread is blocked when the slave resource is locked can be divided into spin locks (atomic family under JUC) and blocking locks (synchronized, ReentrantLock)
  • Concurrent access to resources from multiple threads can be divided into lock-free, biased locks, lightweight locks and heavyweight locks (jdk1.6 began to optimize locks)
  • Distinguish from the fairness of locks, divided into fair locks and unfair locks
  • Whether the lock can be acquired repeatedly can be divided into reentrant locks and non-reentrant locks
  • Whether multiple threads can obtain the same lock can be divided into shared locks and exclusive locks

insert image description here

Why do you need distributed locks

In a stand-alone application environment, all threads run in the same jvm process, and using the lock that comes with Java is enough to control concurrency; but in a distributed scenario, multiple threads run on different machines (jvm processes). Distributed locks are needed to solve the problem

What is a distributed lock

A distributed lock is an implementation of a lock that controls concurrent access to shared resources by different processes in a distributed system. If a critical resource (such as data in a database) is shared between different hosts, mutual exclusion is often required to prevent mutual interference and ensure consistency.

Function: When multiple services in a distributed cluster request the same method or the same business operation (such as seckill), the corresponding business logic can only be executed by one thread on one machine to avoid concurrent security issues.

Distributed lock based on database implementation

Use select...for updatedatabase row locks to implement pessimistic locks. Note: If the query condition uses an index/primary key , then select ... for update will lock the row ; if it is an ordinary field (without index/primary key), then select ... for update will lock the table.

Pessimistic lock implementation

The acquisition lock method needs to declare a transaction, add a data row lock, and release the row lock when the transaction ends. The operation of releasing the lock should be placed in finally

The pseudocode is as follows:

//整体流程
try {
    
    
    if(lock(keyResource)){
    
    //加锁
        process();//业务逻辑处理
     }
} finally {
    
    
    unlock(keyResource);//释放
}

//锁方法实现
//获取锁
public boolean lock(String keyResource){
    
    
    resLock = 'select * from resource_lock where key_resource = '#{
    
    keySource}' for update';
    if(resLock != null && resLock.getLockFlag == 1){
    
    
        return false;
    }
    resLock.setLockFlag(1);//上锁
    insertOrUpdate(resLock);//提交
    return true;
}

//释放锁
public void unlock(String keyResource){
    
    
    resourceLock.setLockFlag(0);//解锁
    update(resourceLock);//提交
}

Optimistic lock implementation

Based on the idea of ​​CAS, add a version field to the database table.

When using, update with conditions (judging version)

mybatis-plus already supports automatic configuration

features

  1. Due to the performance bottleneck of the database itself, the distributed lock based on the database is mainly used in scenarios with low concurrency
  2. The practice method is simple, stable and reliable

Distributed lock based on Redis

original plan

Use SETNXthe command, SETNXnamely SET if N ot e XsetIfAbsent ists (corresponding to the method in Java ), if the key does not exist, the value of the key will be set and 1 will be returned. If the key already exists, SETNXdo nothing and return 0.

expire KEY secondsSet the expiration time of the key, if the key has expired, it will be deleted automatically.

del KEYdelete key

The pseudocode is as follows:

//setnx加锁
if(jedis.setnx(key,lock_value) == 1){
    
    
    //设定锁过期时间
    expire(key,10);
    try{
    
    
        //业务处理
        do();
    } catch(){
    
    
        
    }finally{
    
    
        //释放锁
        jedis.del(key);
    }
}

In this original scheme, setnx and expire are two separate operations rather than atomic operations . If after executing the setnx operation, the process hangs up before executing expire to set the expiration time, then the lock cannot be released, and other threads cannot acquire the lock.

SET extension command (after Redis2.6.12 version)

Use redis extension commandSET key value[EX seconds][PX milliseconds][NX|XX]

The meaning of each parameter is as follows:

  • key: The key name to set.
  • value: The value to set.
  • EX seconds: An optional parameter indicating the expiration time of the set key in seconds.
  • PX milliseconds: An optional parameter indicating the expiration time of the set key in milliseconds.
  • NX: Optional parameter, which means set the value only if the key does not exist.
  • XX: Optional parameter, which means set the value only if the key already exists.

Examples are as follows:

  • Set a key-value pair with no options:
SET username alice

This will set the key named "username" to the value "alice".

  • Set a key-value pair with an expiration time:
SET session_token 123456 EX 3600

This will set the value of the key named "session_token" to "123456" and the key will expire after 3600 seconds (1 hour).

  • Set a key-value pair with an expiration time in milliseconds:
SET cache_key data123 PX 5000

This will set the value of the key named "cache_key" to "data123" and the key will expire after 5000 milliseconds (5 seconds).

  • Only set the value if the key doesn't exist:
SET order_status pending NX

If the key "order_status" does not exist, it will be set to "pending". If the key already exists, do nothing.

  • Only set the value if the key already exists:
SET login_attempts 3 XX

If the key "login_attempts" already exists, its value will be set to "3". If the key name does not exist, do nothing.

After understanding this command, we can use it to build a distributed lock

The pseudocode is as follows:

if(jedis.set(key,lock_value,"NX","EX",10s) == 1){
    
    
    try{
    
    
        do();
    }catch(){
    
    
  
    }finally{
    
    
        jedis.del(key);
    }
}

This operation guarantees the atomicity of set and expire, but there are still other problems:

  1. The lock expired and released, but the business has not been executed yet (the solution will be mentioned later: watchdog mechanism)
  2. The lock is accidentally deleted by other threads (the solution will be mentioned later: Lua script): After the lock of thread 1 expires and is released, it is acquired by other threads (thread 2), but the previous thread (thread 1) is del again after execution Lock (that is, the lock of thread 2 is released), in the case of high concurrency, this scenario is equivalent to no lock

The problem of accidental deletion of locks

Some students will ask, since the lock can be accidentally deleted by other threads, can we add a unique identifier to it? Generally speaking, there is no problem in thinking, but it cannot be simply processed in Java

If we judge in Java, the pseudocode is as follows:

//加锁时设置一个随机id来作为标识,如果释放锁时还是这个id即证明释放了自己的锁(实际上是有逻辑错误的)
if(jedis.set(key,randomId,"NX","EX",10s) == 1){
    
    
    try{
    
    
        do();
    }catch(){
    
    
  
    }finally{
    
    
        //从redis获取randomId,如果是期望值则释放
        if(randomId.equals(jedis.get(key))){
    
    
        	jedis.del(key);
        }
    }
}

It seems that there is no problem, but here there will be a problem that is not caused by atomic operations : if the lock expires after the randomId is judged to be the expected value, the second thread creates its own lock, because the first thread has already If the judgment of randomId is passed, it will still release the lock just created by thread 2, and the problem of accidental lock deletion still exists...

The good news is, we have other solutions.

After redis version 2.6, developers are allowed to use Lua to write scripts to pass to redis for execution. The benefits of doing so are as follows:

  1. Reduce network overhead: The operation of multiple network requests can be completed with one request, and the logic of the original multiple requests is completed on the redis server. Using scripts reduces the network round-trip delay;
  2. Atomic operation: Redis will execute the entire script as a whole, and will not be inserted/interrupted by other commands in the middle;
  3. Replace the transaction function of redis: the Lua script of Redis almost realizes the conventional transaction function, and supports error reporting and rollback operations. It is officially recommended that if you want to use the redis transaction function, you can use the redis lua script instead.

The basic syntax of the Redis Eval command is as follows

EVAL script numkeys key [key ...] arg [arg ...] 

#实例			   eval  引号中是脚本内容                         key的个数 key[...]  arg[...]  
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 20
1) "username"
2) "age"
3) "jack"
4) "20"

At this time, we can use Lua scripts to ensure the atomicity of operations.

lua script:

if redis.call('get',KEYS[1])==ARGV[1] then
    return redis.call('del',KEYS[1])
else
    return 0
end;

In redis:

EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 key value

In Java:

String key = "key";
String value = "value";
// 定义 Lua 脚本
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 执行 Lua 脚本
Object result = jedis.eval(luaScript, 1, key, value);

At this point, we can solve the problem of accidentally deleting the lock, but there is another problem that has not been solved. What should I do if the lock expires and is released but the business has not been executed yet?

Redisson's watchdog mechanism

Redisson is similar to Jedis. It is a client for Java to operate Redis. It is more useful than Jedis in solving distributed scenarios. It provides various distributed objects, distributed locks, distributed synchronizers, distributed services, etc.

The implementation process of Redission distributed lock is as follows

The source code of Redisson's realization of automatic renewal is as follows:

private void renewExpiration() {
    
    
  // 获取当前锁的过期时间续约条目
  RedissonBaseLock.ExpirationEntry ee = (RedissonBaseLock.ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(this.getEntryName());

  // 如果存在续约条目
  if (ee != null) {
    
    
    // 创建定时任务,定时执行续约操作
    Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
    
    
      public void run(Timeout timeout) throws Exception {
    
    
        // 获取续约条目
        RedissonBaseLock.ExpirationEntry ent = (RedissonBaseLock.ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());

        // 如果续约条目存在
        if (ent != null) {
    
    
          Long threadId = ent.getFirstThreadId();

          // 如果存在线程ID
          if (threadId != null) {
    
    
            // 异步执行续约操作
            CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);

            // 当异步操作完成时
            future.whenComplete((res, e) -> {
    
    
              if (e != null) {
    
    
                // 发生错误时,记录日志并移除续约条目
                RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
                RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
              } else {
    
    
                // 如果续约成功,递归调用续约操作
                if (res) {
    
    
                  RedissonBaseLock.this.renewExpiration();
                } else {
    
    
                  // 如果无法续约,取消续约操作
                  RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
                }
              }
            });
          }
        }
      }
    }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);

    // 设置定时任务到续约条目
    ee.setTimeout(task);
  }
}

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    
    
  // 使用 evalWriteAsync 方法执行 Lua 脚本
  // 这个脚本会检查锁是否仍然由给定的线程持有,如果是则更新锁的过期时间
  return this.evalWriteAsync(
    this.getRawName(),                       // 锁的键名
    LongCodec.INSTANCE,                      // 键的编码器
    RedisCommands.EVAL_BOOLEAN,              // 使用 EVAL 命令并返回布尔值
    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;",
    Collections.singletonList(this.getRawName()),     // 键名作为 KEYS[1]
    this.internalLockLeaseTime,              // 锁的过期时间(毫秒)
    this.getLockName(threadId)               // 获取锁的名称,用于验证锁的持有者
  );
}

Summarize:

  1. The watchdog is only used if no lock timeout is specified
  2. If the Redisson instance hangs up, the watchdog will also crash, then the key that has reached the expiration time will be cleared by redis, and the lock will be released, and there will be no problem that the lock is permanently occupied.

Redisson's RLock interface inherits JUC's lock interface, so it conforms to the Lock interface specification in Java. At the same time, Redisson also provides a variety of distributed lock implementation classes (for example: RedissonFairLock, RedissonRedLock, etc.) for you to choose

Redis cluster data inconsistency problem

When deploying redis, in order to avoid single-point problems, we usually deploy in a cluster mode. Since the data synchronization of the redis cluster is an asynchronous operation, it will return the lock success after the master node is locked; if a thread gets the lock on the master node When the lock is reached, but the locked key has not been synchronized to the slave node, the master node fails . A slave node will be upgraded to a master node, and other threads can also acquire the lock of the same key, which is equivalent to not adding Lock

The author of redis proposed an advanced distributed lock algorithm: Redlock, to solve this problem

Redlock core idea

Do multiple Redis master deployments to ensure that they don't go down at the same time. And these master nodes are completely independent of each other , and there is no data synchronization between them. At the same time, you need to ensure that on these multiple master instances, you use the same method as the Redis single instance to acquire and release locks.

insert image description here

Redlock process steps

1. Request locks from multiple master nodes (as shown in the above figure 5) in sequence

2. Judging according to the set timeout period, whether to skip the master node;

3. If more than half of the nodes are successfully locked (the three in the right picture are sufficient), and the use time is less than the validity period of the lock (set a single node timeout), the lock can be deemed successful;

4. If the lock acquisition fails, unlock all master nodes

Distributed lock based on ZooKeeper

I have written a blog about this before, so you can just click on the link to jump~

Distributed lock implementation based on ZooKeeper temporary sequential nodes_❀always❀'s Blog-CSDN Blog

The implementation idea is as follows:

insert image description here

Comparison of distributed lock implementation schemes

plan train of thought advantage shortcoming typical scene
mysql pessimistic lock, optimistic lock Simple, stable and reliable Poor performance, not suitable for high concurrency Distributed timing tasks
redis Guaranteed atomicity of cache operations based on SETNX and Lua scripts Good performance (AP) Relatively complicated to implement, not 100% reliable Flash sales, snap-ups, and large-scale lucky draws
zookeeper ZK-based node characteristics and Watcher mechanism High reliability (CP) The implementation is relatively complex and the performance is slightly worse Flash sales, snap-ups, and large-scale lucky draws

Distributed locks and high concurrency

From a design point of view, distributed locks and high concurrency are contradictory : distributed locks actually serialize parallel codes to solve concurrency problems, which have an impact on performance, but can be optimized.

The main programs are:

  1. Smallest lock granularity: As far as possible, put the code with the smallest granularity that has concurrency security issues inside the lock, and put other codes outside the lock. This is the basic optimization principle of locks
  2. Data sharding: For example, ConcurrentHashMap uses a segmented lock mechanism to improve concurrency, MySQL sub-database and sub-table (distribution of pressure to different DBs), etc.

Application of distributed locks in business scenarios

  • After an event occurs, it is necessary to send a text message to remind the user, and if the event occurs multiple times within two hours, the user is only reminded for the first time

    Implementation idea: Get the distributed lock before sending a text message every time, set the expiration time to 2h, if the event occurs again within 2h, the same distributed lock cannot be obtained, and the process of sending the text message can be skipped automatically

  • Ensure that there is only one piece of data uniquely identified by id+time of day in a table

    Implementation idea: When inserting or updating, first obtain the distributed lock, and unlock it after successful insertion

  • Grab a limited amount of prizes

    Implementation idea: Each thread preempts the distributed lock. After the preemption is successful, it is judged whether the remaining quantity meets the required quantity. If so, the preemption succeeds and the lock is released.

Guess you like

Origin blog.csdn.net/m0_51561690/article/details/132285525