Redis Series: Redis-based distributed locks

Source: Internet
Author: User
Tags lua redis uuid zookeeper
First, Introduction

This blog post shows you how to build a Redis-based distributed lock step at a pace. Will start with the most original version, then adjust according to the problem, and finally complete a more reasonable distributed lock.

This article divides the implementation of the distributed lock into two parts, one is the single-machine environment and the other is the Redis lock implementation in the cluster environment. Before introducing the implementation of distributed locks, let's look at some information about distributed locks.

II. Distributed Lock 2.1 What is a distributed lock?

Distributed locks are a kind of lock implementation that controls the shared resources between distributed systems or different systems, and if a resource is shared between different systems or different hosts of the same system, mutual exclusion is often required to prevent mutual interference to ensure consistency.

2.2 What conditions are required for distributed locks
    1. Mutex: At any one time, only one client holds the lock.
    2. No deadlock: Locks can still be acquired even if the client holding the lock crashes or other unexpected events.
    3. Fault tolerance: Clients can acquire and release locks as long as most redis nodes are alive
2.4 What are the implementations of distributed locks?
    1. Database
    2. Memcached (add command)
    3. Redis (setnx command)
    4. Zookeeper (Temporary node)
    5. Wait a minute
Three, single-machine Redis distributed lock 3.1 preparation work
    • Defining a constant class
public class LockConstants {    public static final String OK = "OK";    /** NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. **/    public static final String NOT_EXIST = "NX";    public static final String EXIST = "XX";    /** expx EX|PX, expire time units: EX = seconds; PX = milliseconds **/    public static final String SECONDS = "EX";    public static final String MILLISECONDS = "PX";    private LockConstants() {}}
    • Abstract class for defining locks

The abstract class Redislock implements the lock interface under the Java.util.concurrent package, and then provides the default implementation for some methods, which only need to implement the lock method and the Unlock method. The code is as follows

public abstract class RedisLock implements Lock {    protected Jedis jedis;    protected String lockKey;    public RedisLock(Jedis jedis,String lockKey) {        this(jedis, lockKey);    }    public void sleepBySencond(int sencond){        try {            Thread.sleep(sencond*1000);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    @Override    public void lockInterruptibly(){}    @Override    public Condition newCondition() {        return null;    }    @Override    public boolean tryLock() {        return false;    }    @Override    public boolean tryLock(long time, TimeUnit unit){        return false;    }}
3.2 Most basic version 1

First, the most basic version, the code is as follows

public class LockCase1 extends RedisLock {    public LockCase1(Jedis jedis, String name) {        super(jedis, name);    }    @Override    public void lock() {        while(true){            String result = jedis.set(lockKey, "value", NOT_EXIST);            if(OK.equals(result)){                System.out.println(Thread.currentThread().getId()+"加锁成功!");                break;            }        }    }    @Override    public void unlock() {        jedis.del(lockKey);    }}

The LockCase1 class provides the lock and unlock methods.
Where the lock method is the Reids client executes the following command

SET lockKey value NX

The Unlock method is to call the del command to delete the key.
All right, here's how it's done. Now, think about what the problem is.
Assume that there are two clients A and b,a acquire a distributed lock. A was executed for a while, suddenly a server was powered down (or something else), that is, client a hangs. At this point, there is a problem, the lock is always present and will not be released, and the other clients will never get the lock. As follows

You can fix the problem by setting the expiration time.

3.3 Version 2-set the expiration time of the lock
public void lock() {    while(true){        String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30);        if(OK.equals(result)){            System.out.println(Thread.currentThread().getId()+"加锁成功!");            break;        }    }}

Similar Redis commands are the following

SET lockKey value NX EX 30

Note: To ensure that setting the expiration time and setting the lock is atomic

At this point, a problem arises, and the steps are as follows

    1. Client A acquires a lock successfully with an expiration time of 30 seconds.
    2. Client A was blocked for 50 seconds on an operation.
    3. 30 seconds, the lock is automatically released.
    4. Client B obtains a lock that corresponds to the same resource.
    5. Client a recovers from the blocking and releases the lock held by client B.

As follows

Then there are two problems.

    1. How does the expiration time guarantee be greater than the business execution time?
    2. How do I ensure that the lock is not deleted by mistake?

First of all to resolve how to ensure that the lock will not be deleted by mistake.
This problem can be achieved by setting value to a random string generated by the current client, and is guaranteed to be unique within a long enough period of all requests to acquire locks for all clients.

Full code for version 2: GitHub address

3.4 Version 3-Set the value of the lock

Abstract class Redislock adds the Lockvalue field, the default value for the Lockvalue field is the UUID random value assuming the current thread ID.

public abstract class RedisLock implements Lock {    //...    protected String lockValue;    public RedisLock(Jedis jedis,String lockKey) {        this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());    }    public RedisLock(Jedis jedis, String lockKey, String lockValue) {        this.jedis = jedis;        this.lockKey = lockKey;        this.lockValue = lockValue;    }    //...}

Add lock Code

public void lock() {    while(true){        String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECONDS,30);        if(OK.equals(result)){            System.out.println(Thread.currentThread().getId()+"加锁成功!");            break;        }    }}

Unlock Code

public void unlock() {    String lockValue = jedis.get(lockKey);    if (lockValue.equals(lockValue)){        jedis.del(lockKey);    }}

Then look at the lock code, as if there is no problem ah.
Then take a look at the unlocked code, where the unlock operation consists of three steps: getting the value, judging and deleting the lock. Did you ever think of operating in a multithreaded environment i++ ?

3.4.1 i++ Problems

i++The operation can also be divided into three steps: Read the value of I, i+1, set the value of I.
If two threads perform i++ operations on I, the following conditions can occur

    1. I set the value to 0
    2. Thread A reads the value of I to 0
    3. Thread B also reads the value of I to 0
    4. Thread A performs a +1 operation that writes the result value 1 to the memory
    5. Thread B performs +1 operations, writes the result value 1 to the memory
    6. I performed two i++ operations at this time, but the result was 1.

In a multi-threaded environment, what is the way to avoid this kind of situation?
There are many ways to solve this problem, such as Atomicinteger, CAS, synchronized and so on.
The purpose of these solutions is to ensure i++ the atomicity of the operation. So looking back at unlocking, we're also going to ensure the atomicity of the unlock. We can use Redis's LUA script to achieve the atomicity of unlocking operations.

Full code for Version 3: GitHub address

3.5 version 4-Atomic release lock

The Lua script content is as follows

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

This LUA script takes the value of Lockvalue as argv[1] in execution and passes Lockkey as the value of keys[1]. Now look at the unlocked Java code

public void unlock() {    // 使用lua脚本进行原子删除操作    String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +                                "return redis.call('del', KEYS[1]) " +                                "else " +                                "return 0 " +                                "end";    jedis.eval(checkAndDelScript, 1, lockKey, lockValue);}

OK, the unlock operation also ensures atomicity, so is it a single-machine Redis environment where the distributed lock is done?
Don't forget. Version 2-Setting the expiration time of the lock also has one, how the expiration time is guaranteed to be greater than the business execution time issue is not resolved.

Full code for version 4: GitHub address

3.6 Version 5-Ensure that the expiration time is greater than the business execution time

Abstract class Redislock adds a Boolean property Isopenexpirationrenewal that identifies whether the timed refresh expiration time is turned on.
An Scheduleexpirationrenewal method is added to enable the thread to open the refresh expiration time.

  Public abstract class Redislock implements Lock {//... protected volatile Boolean Isopenexpirationrenewa    L = true; /** * Turn on timed refresh */protected void Scheduleexpirationrenewal () {Thread renewalthread = new Thread (New Expira        Tionrenewal ());    Renewalthread.start (); }/** * Refresh key Expiration Time */Private class Expirationrenewal implements runnable{@Override public vo                ID run () {while (Isopenexpirationrenewal) {System.out.println ("execution delay expires in ..."); String checkandexpirescript = "If Redis.call (' Get ', keys[1]) = = Argv[1] Then" + "return Redis.call (' expire ', keys[1],argv[2])                "+" Else "+" return 0 end ";                Jedis.eval (Checkandexpirescript, 1, Lockkey, Lockvalue, "30");            Sleep 10 seconds Sleepbysencond (10); }        }    }}

The lock code resets the Isopenexpirationrenewal to true after the lock is acquired, and calls the Scheduleexpirationrenewal method to turn on the thread that refreshed the expiration time.

public void lock() {    while (true) {        String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 30);        if (OK.equals(result)) {            System.out.println("线程id:"+Thread.currentThread().getId() + "加锁成功!时间:"+LocalTime.now());            //开启定时刷新过期时间            isOpenExpirationRenewal = true;            scheduleExpirationRenewal();            break;        }        System.out.println("线程id:"+Thread.currentThread().getId() + "获取锁失败,休眠10秒!时间:"+LocalTime.now());        //休眠10秒        sleepBySencond(10);    }}

The unlock code adds a line of code, the Isopenexpirationrenewal property to False, and the thread polling to stop flushing the expiration time.

public void unlock() {    //...    isOpenExpirationRenewal = false;}

Full code for version 5: GitHub address

3.7 Testing

The test code is as follows

public void testLockCase5() {    //定义线程池    ThreadPoolExecutor pool = new ThreadPoolExecutor(0, 10,                                                    1, TimeUnit.SECONDS,                                                    new SynchronousQueue<>());    //添加10个线程获取锁    for (int i = 0; i < 10; i++) {        pool.submit(() -> {            try {                Jedis jedis = new Jedis("localhost");                LockCase5 lock = new LockCase5(jedis, lockName);                lock.lock();                //模拟业务执行15秒                lock.sleepBySencond(15);                lock.unlock();            } catch (Exception e){                e.printStackTrace();            }        });    }    //当线程池中的线程数为0时,退出    while (pool.getPoolSize() != 0) {}}

Test results

Perhaps here is the introduction based on the distributed single-machine Redis environment. But did the students who use Java have found an important feature of a lock

That is the re-entry of the lock, then how to implement the re-entry of the distributed lock? We're going to leave a hole here.

Iv. distributed locks for clustered Redis

In a Redis distributed environment, the Redis author provides a redlock algorithm to implement a distributed lock.

4.1 Plus lock

Redlock algorithm lock procedure is as follows

    1. Gets the current Unix time, in milliseconds.
    2. Try to get the lock from N instances, using the same key and random values. In step 2, when you set a lock to Redis, the client should set a network connection and response time-out, which should be less than the lock's expiration time. For example, if your lock has an automatic expiration time of 10 seconds, the time-out should be between 5-50 milliseconds. This avoids the case where the server-side Redis has been hung, and the client is still waiting for the result to be answered. If the server side does not respond within the specified time, the client should try another Redis instance as soon as possible.
    3. The client uses the current time minus the start acquisition lock Time (the time recorded in step 1) to obtain the time the lock was used. The lock is successful only if and only if the Redis node from most (here is 3 nodes) is taken to the lock and the time used is less than the lock failure time.
    4. If a lock is taken, the true effective time of the key is equal to the effective time minus the time used to acquire the lock (the result of step 3).
    5. If, for some reason, the acquisition lock fails ( without at least n/2+1 Redis instances taking the lock or the lock time has exceeded the effective time), the client should be unlocked on all Redis instances (even if some redis instances are not locked successfully at all).
4.2 Unlocking

Send a Release lock command to all Redis instances, regardless of whether you have successfully acquired the lock from the Redis instance before.

As for the Redlock algorithm, there is also an episode in which Martin Kleppmann and redlock author Antirez the redlock algorithm. The official website is as follows

Martin Kleppmann analyzed redlock here. I disagree with the posted my reply

More about the Redlock algorithm here is not the description, interested can go to the official website to read the relevant articles.

V. Summary

This article describes a Redis-based distributed lock authoring process and how to solve the problem, but the distributed locks implemented in this article are not suitable for production environments. Java environments have Redisson available for production environments, but distributed locks or zookeeper can be better (see Martin Kleppmann and Redlock analysis).

Martin Kleppmann's analysis of Redlock: http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

Redlock author Antirez's response: http://antirez.com/news/101

The entire project address is stored on GitHub, and there's a need to look at it: GitHub address

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.