Java implementation of distributed lock code example based on redis

Why is there such a demand:

For example, a simple user operation, a thread to modify the user's state, first read the user's state in the memory, then modify it in the memory, and then store it in the database. In single thread, this is no problem. However, in multithreading, because reading, modifying, and writing are three operations, not atomic operations (success or failure at the same time), there will be data security issues in multithreading.

In this case, you can use distributed locks to limit the concurrent execution of the program.

Realization ideas:

It means that when other threads come in and operate, they will give up or try again later if they find someone is occupying a seat.

Realization of placeholder:

It is implemented by the setnx command in redis. For the redis command, please refer to my blog https://www.cnblogs.com/javazl/p/12657280.html. The default set command is to save the value. When the key exists, set is It will overwrite the value of the key, but setnx will not. When there is no key, setnx will come in and take the place first. When the key exists, other setnx will not be able to come in. . Wait until the first execution is complete, release the seat in the del command.

Code:

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
      Long setnx = jedis.setnx("k1", "v1");
     //setnx的返回值为long类型
      if (setnx == 1) {
        //没人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        //释放资源
         jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

In the above code, it is a simple implementation of distributed lock, but there is a problem. That is, if it hangs before releasing after occupying. Then this thread will never be released, that is, the del command is not called, all subsequent requests are blocked here, and the lock becomes a deadlock. So here needs to be optimized.

The optimization method is to add an expiration time to ensure that the lock can be released after a certain period of time.

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
      Long setnx = jedis.setnx("k1", "v1");
      if (setnx == 1) {
        //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
        jedis.expire("k1", 5);
        //没人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }

After this treatment, you can ensure that the lock can be released normally. But there will be a new problem, that is, if the server hangs up during lock retrieval and setting expiration time, because lock retrieval, that is, setnx and setting expiration time are two operations, which are not atomic and cannot be completed at the same time. This lock will be occupied forever, cannot be released, and becomes a deadlock. So how to solve it?

After redis2.8, setnx and expireke can be executed together with one command, and the two operations become one, which will solve this problem.

Optimized realization:

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
     //将两个操作合并成一个,nx就是setnx,ex就是expire
      String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
     //操作结果为okhuo或者error
      if (set !=null && "OK".equals(set)) {
     //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
        jedis.expire("k1", 5);
        //没人占位
        jedis.set("name", "zl);
        String name = jedis.get("name");
        System.out.println(name);
      //释放资源
        jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

After optimizing with the expiration time, although the deadlock problem is solved, a new problem arises, that is, the timeout problem:

For example: if the business to be executed is time-consuming, it may be disordered. When a local thread acquires the lock, it starts to execute the business code, but the business code is time-consuming. If the expiration time is 3 seconds, and the business execution requires 5 seconds, in this way, the lock will be released early, and then the second thread acquires the lock and starts execution. When the execution reaches the second second, the first lock is also executed. At this time, the first thread will release the lock of the second thread, and then the third thread will continue to acquire the lock and execute it. When the third second is reached After the second thread is executed, the lock will be released in advance, and the loop will continue to cause thread chaos.

Then there are two main solutions

Try to avoid time-consuming operations.
To deal with the lock, set a random number or random string to the value of the lock, and judge the value of the value whenever it is to be released. If it is, release it, if it is not, it will not release it. For example, suppose the first one When the thread comes in, the value it acquires the lock is 1. If a timeout occurs, it will enter the next thread, and the next thread will acquire the new value as

3. Before releasing the second place, get the value and compare it. If 1 is not equal to three, then the lock is not released.
There is nothing to say about the first type, but there will be a problem with the second type, that is to release the lock to check the value, then compare, and then release. There will be three operations, so there is no atomicity. If this operation is performed, it will appear. Deadlock. Here we can use Lua scripts to process.

Features of Lua script:

1. Easy to use, redis has built-in support for Lua scripts.

2.Lua can execute multiple redis commands atomically on the redis server

3. Due to network reasons, the performance of redis will be affected. Therefore, using Lua can allow multiple commands to be executed at the same time, reducing the performance problems caused by the network to redis.

How to use Lua scripts in redis:

1. Write it on the redis server, and then call the script in the java business

2. You can write directly in java. After you write it, when you need to execute it, send the script to redis for execution every time.

Create a Lua script:

//用redis.call调用一个redis命令,调的是get命令,这个key是从外面传进来的keyif redis.call("get",KEYS[1])==ARGV[1] then//如果相等就去操作释放命令
  return redis.call("del",KEYS[1])
else
 return 0
end

You can find a SHA1 sum for the Lua script:

cat lua/equal.lua | redis-cli -a root script load --pipe
script load This command will cache the Lua script in Redis and return the SHA1 checksum of the script content, and then pass in SHA1 when called in java The checksum is used as a parameter, so that the redis server knows which script to execute.

Next write in java

public static void main(String[] args) {
    Redis redis = new Redis();
    for (int i = 0; i < 2; i++) {
      redis.execute(jedis -> {
        //1.先获取一个随机字符串
        String value = UUID.randomUUID().toString();
        //2.获取锁
        String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
        //3.判断是否成功拿到锁
        if (k1 != null && "OK".equals(k1)) {
          //4. 具体的业务操作
          jedis.set("site", "zl");
          String site = jedis.get("site");
          System.out.println(site);
          //5.释放锁
          jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", 
Arrays.asList("k1"), Arrays.asList(value));
        } else {
          System.out.println("没拿到锁");
        }
      });
    }
  }
}

In this way, the deadlock problem is solved.

Some high-frequency interview questions collected in the latest 2020 (all organized into documents), there are many dry goods, including mysql, netty, spring, thread, spring cloud, jvm, source code, algorithm and other detailed explanations, as well as detailed learning plans, interviews Question sorting, etc. For those who need to obtain these contents, please add Q like: 11604713672

Guess you like

Origin blog.csdn.net/weixin_51495453/article/details/113936698