Home > Backend Development > PHP Tutorial > How to implement distributed locks based on Redis

How to implement distributed locks based on Redis

无忌哥哥
Release: 2023-04-03 09:34:01
Original
1549 people have browsed it

Preface

Distributed locks are widely used in distributed applications. If you want to understand a new thing, you must first understand its origin, so that you can understand it better and even draw inferences.

First of all, when talking about distributed locks, we naturally think of distributed applications.

In the stand-alone system before we split the application into distributed applications, when reading public resources in some concurrent scenarios, such as deducting inventory and selling tickets, we can simply use synchronization or locking. It can be achieved.

But after the application is distributed, the system changes from the previous single-process and multi-threaded program to a multi-process and multi-threaded program. At this time, the above solution is obviously not enough.

Therefore, a commonly used solution in the industry is usually to rely on a third-party component and use its own exclusivity to achieve mutual exclusion of multiple processes. For example:

  • Unique index based on DB.

  • Temporary ordered node based on ZK.

  • Based on Redis’s NX EX parameters.

The discussion here is mainly based on Redis.

Implementation

Since Redis is chosen, it must be exclusive. At the same time, it is best to have some basic characteristics of locks:

  • High performance (high performance when adding and unlocking)

  • You can use blocking locks with Non-blocking lock.

  • No deadlock can occur.

  • Availability (the lock cannot fail after the node is down).

Here, a NX parameter used in Redis set key can ensure successful writing even if the key does not exist. And adding the EX parameter allows the key to be automatically deleted after timeout.

So using the above two features can ensure that only one process will obtain the lock at the same time, and no deadlock will occur (the worst case is that the key will be automatically deleted after timeout).

Lock

The implementation code is as follows:

    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    
    public  boolean tryLock(String key, String request) {
        String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);

        if (LOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }
Copy after login

Note the

String set(String key, String value, String nxxx, String expx, long time);
Copy after login

api of jedis used here.

This command can ensure the atomicity of NX EX.

Be sure not to execute the two commands (NX EX) separately. If there is a problem with the program after NX, a deadlock may occur.

Blocking lock

At the same time, a blocking lock can also be implemented:

    //一直阻塞
    public void lock(String key, String request) throws InterruptedException {

        for (;;){
            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                break ;
            }
                
              //防止一直消耗 CPU  
            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }

    }
    
     //自定义阻塞时间
     public boolean lock(String key, String request,int blockTime) throws InterruptedException {

        while (blockTime >= 0){

            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                return true ;
            }
            blockTime -= DEFAULT_SLEEP_TIME ;

            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }
        return false ;
    }
Copy after login

Unlocking

Unlocking is also very simple. In fact, just delete the key and everything will be fine. For example, use the del key command.

But reality is often not that easy.

If process A acquires the lock and sets a timeout, but due to the long execution cycle, the lock is automatically released after the timeout. At this time, process B acquires the lock and releases the lock soon. In this way, process B will release the lock of process A.

So the best way is to determine whether the lock belongs to you every time you unlock it.

At this time, it needs to be implemented in conjunction with the locking mechanism.

When locking, you need to pass a parameter, and use this parameter as the value of this key, so that you can judge whether the values ​​are equal each time you unlock.

So the unlock code cannot be simply del.

    public  boolean unlock(String key,String request){
        //lua script
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        Object result = null ;
        if (jedis instanceof Jedis){
            result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else if (jedis instanceof JedisCluster){
            result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else {
            //throw new RuntimeException("instance is error") ;
            return false ;
        }

        if (UNLOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }
Copy after login

A lua script is used here to determine whether the values ​​are equal, and the del command will be executed only if they are equal.

Using lua can also ensure the atomicity of the two operations here.

Therefore, the four basic features mentioned above can also be satisfied:

  • Using Redis can ensure performance.

  • See above for blocking locks and non-blocking locks.

  • Use the timeout mechanism to solve the deadlock.

  • Redis supports cluster deployment to improve availability.

Usage

I have a complete implementation myself, and it has been used in production. Friends who are interested can use it out of the box:

maven dependency:

<dependency>
    <groupId>top.crossoverjie.opensource</groupId>
    <artifactId>distributed-redis-lock</artifactId>
    <version>1.0.0</version>
</dependency>
Copy after login

Configuration bean:

@Configuration
public class RedisLockConfig {

    @Bean
    public RedisLock build(){
        RedisLock redisLock = new RedisLock() ;
        HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ;
        JedisCluster jedisCluster = new JedisCluster(hostAndPort) ;
        // Jedis 或 JedisCluster 都可以
        redisLock.setJedisCluster(jedisCluster) ;
        return redisLock ;
    }

}
Copy after login

Usage:

    @Autowired
    private RedisLock redisLock ;

    public void use() {
        String key = "key";
        String request = UUID.randomUUID().toString();
        try {
            boolean locktest = redisLock.tryLock(key, request);
            if (!locktest) {
                System.out.println("locked error");
                return;
            }


            //do something

        } finally {
            redisLock.unlock(key,request) ;
        }

    }
Copy after login

It is very simple to use. The main purpose here is to use Spring to help us manage the RedisLock singleton bean, so when releasing the lock, we need to manually pass in the key and request (because the entire context has only one RedisLock instance) (the API does not look particularly elegant).

You can also create a new RedisLock and pass in the key and request every time you use the lock, which is very convenient when unlocking. But you need to manage the RedisLock instance yourself. Each has its own pros and cons.

Single Test

While working on this project, I have to mention Single Test.

Because this application is strongly dependent on third-party components (Redis), but we need to exclude this dependency in the single test. For example, another partner forked the project and wanted to run a single test locally, but the result failed to run:

  1. It may be that the IP and port of Redis are inconsistent with those in the single test.

  2. Redis itself may also have problems.

  3. It is also possible that the student does not have Redis in his environment.

So it is best to eliminate these external unstable factors and only test the code we have written.

So you can introduce the single test tool Mock.

The idea is very simple, that is, to block all the external resources you rely on. Such as: database, external interface, external files, etc.

使用方式也挺简单,可以参考该项目的单测:

    @Test
    public void tryLock() throws Exception {
        String key = "test";
        String request = UUID.randomUUID().toString();
        Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");

        boolean locktest = redisLock.tryLock(key, request);
        System.out.println("locktest=" + locktest);

        Assert.assertTrue(locktest);

        //check
        Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong());
    }
Copy after login

这里只是简单演示下,可以的话下次仔细分析分析。

它的原理其实也挺简单,debug 的话可以很直接的看出来:

How to implement distributed locks based on Redis

这里我们所依赖的 JedisCluster 其实是一个 cglib 代理对象。所以也不难想到它是如何工作的。

比如这里我们需要用到 JedisCluster 的 set 函数并需要它的返回值。

Mock 就将该对象代理了,并在实际执行 set 方法后给你返回了一个你自定义的值。

这样我们就可以随心所欲的测试了,完全把外部依赖所屏蔽了

总结

至此一个基于 Redis 的分布式锁完成,但是依然有些问题。

  • 如在 key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。

  • 就算 Redis 是集群部署的,如果每个节点都只是 master 没有 slave,那么 master 宕机时该节点上的所有 key 在那一时刻都相当于是释放锁了,这样也会出现并发问题。就算是有 slave 节点,但如果在数据同步到 salve 之前 master 宕机也是会出现上面的问题。

感兴趣的朋友还可以参考 Redisson 的实现。

The above is the detailed content of How to implement distributed locks based on Redis. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template