当前位置:网站首页>Jedislock redis distributed lock implementation

Jedislock redis distributed lock implementation

2022-06-11 10:21:00 qq_ twenty-eight million nine hundred and seventeen thousand fo

from :https://www.cnblogs.com/0201zcr/p/5942748.html

One 、 Several conditions for using distributed lock :

  1. The system is a distributed system ( The key is distributed , Stand alone can use ReentrantLock perhaps synchronized Code blocks to implement )
  2. Shared resources ( Each system accesses the same resource , The carrier of resources may be traditional relational database or NoSQL)
  3. Synchronous access ( That is, there are many process colleagues accessing the same shared resource . No synchronous access , Who cares if you compete for resources or not )

Two 、 Examples of application scenarios

   Manage the deployment architecture of the background ( More than one tomcat The server +redis【 More than one tomcat The server accesses a redis】+mysql【 More than one tomcat The server accesses... On a server mysql】) The conditions for using distributed locks are met . Multiple servers need to access redis Globally cached resources , If you do not use distributed locks, you will have problems .  Look at the following pseudo code :

long N=0L;
//N from redis Get value 
if(N<5){
N++;
//N Write back to redis
}

The above code mainly realizes the functions :

   from redis Get value N, For the value N Carry out boundary checks , Self adding 1, then N Write back to redis in .  This kind of application scenario is very common , Like a second kill , Global incremental ID、IP Access restrictions, etc . With IP In terms of access restrictions , A malicious attacker may initiate unlimited access , The amount of concurrency is relatively large , In a distributed environment N The boundary check is unreliable , Because from redis Read N It may already be dirty data . The traditional locking method ( Such as java Of synchronized and Lock) It's no use , Because this is a distributed environment , The firefighters of this synchronization problem are also helpless . In this critical autumn , Distributed locks are finally in use .

   Distributed locks can be implemented in many ways , such as zookeeper、redis.... Either way , His The basic principle It is the same. : Use a state value to represent a lock , The occupation and release of locks are identified by status values .

    Here we mainly talk about how to use redis Implement distributed locks .

3、 ... and 、 Use redis Of setNX Command to implement distributed locks   

1、 Implementation principle

  Redis Single process single thread mode , Using queue mode to change concurrent access into serial access , And multi client pairs Redis There's no competition for the connection .redis Of SETNX Command can easily realize distributed locking .

2、 Basic command parsing

1)setNX(SET if Not eXists)

grammar :

SETNX key value

take  key  The value of the set  value , If and only if  key  non-existent .

If a given  key  Already exist , be  SETNX  Don't do anything .

SETNX  yes 『SET if Not eXists』( If it doesn't exist , be SET) Abbreviation

Return value :

   Set up the success , return  1 .

   Setup failed , return  0 .

  Example :

redis> EXISTS job                # job  non-existent 
(integer) 0

redis> SETNX job "programmer"    # job  Set up the success 
(integer) 1

redis> SETNX job "code-farmer"   #  Try to override  job , Failure 
(integer) 0

redis> GET job                   #  Not covered 
"programmer"

  So we use to execute the following command

SETNX lock.foo <current Unix time + lock timeout + 1> 
  • Such as return 1, Then the client gets the lock , hold lock.foo The key value of is set to the time value to indicate that the key has been locked , Finally, the client can use DEL lock.foo To release the lock .

  • Such as return 0, Indicates that the lock has been acquired by another client , At this time, we can go back or try again, wait for the other party to complete or wait for the lock to time out .

2)getSET

grammar :

GETSET key value

   Will be given  key  The value of the set  value , And back to  key  The old value (old value).

   When  key  When it exists but is not a string type , Return an error .

Return value :

   Return to a given  key  The old value .

   When  key  When there is no old value , That is to say , key  When there is no , return  nil .

3)get

grammar :

GET key

  Return value :

   When  key  When there is no , return  nil , otherwise , return  key  Value .

   If  key  Not a string type , So return an error

Four 、 Solve the deadlock

   There's a problem with the lock logic above : If a client holding a lock fails or crashes, the lock cannot be released , How to solve

 We can judge whether this happens by the time stamp corresponding to the key of the lock , If the current time is greater than lock.foo Value , Indicates that the lock has failed , It can be reused . 

   When this happens , You can't just go through DEL To delete the lock , And then again SETNX once ( Be reasonable , The operation of deleting a lock should be performed by the lock owner , Just wait for it to time out ), When multiple clients detect a lock timeout, they will try to release it , There may be a race condition here , Let's simulate the scene : 

C0 The operation timed out , But it still holds the lock ,C1 and C2 Read lock.foo Check the timestamp , I've found that it's time-out . 
C1  send out DEL lock.foo 
C1  send out SETNX lock.foo  And succeeded in . 
C2  send out DEL lock.foo 
C2  send out SETNX lock.foo  And succeeded in . 
 thus ,C1,C2 They all got the locks ! It's a big problem ! 

   Fortunately, this problem can be avoided , Let's see C3 How does this client do it : 

C3 send out SETNX lock.foo  Want to get the lock , because C0 And hold the lock , therefore Redis Return to C3 One 0 
C3 send out GET lock.foo  To check if the lock timed out , If it doesn't time out , Wait or try again . 
 conversely , If it has timed out ,C3 Try to get the lock by doing the following : 
GETSET lock.foo <current Unix time + lock timeout + 1> 
 adopt GETSET,C3 If the time stamp you get is still time-out , That explains. ,C3 I got the lock . 
 If in C3 Before , There's a man named C4 It's better than C3 Quick step to perform the above operation , that C3 The timestamp you get is a value that doesn't time out , At this time ,C3 Didn't get the lock as scheduled , Need to wait or try again . Pay attention , Even though C3 I didn't get the lock , But it rewrites C4 Set the timeout value of the lock , But the impact of this very small error can be ignored . 

   Be careful : In order to make the distributed lock algorithm more stable , The client holding the lock should check again whether its lock has timed out before unlocking , Do it again DEL operation , Because it is possible that the client is suspended due to a time-consuming operation , At the end of the operation, the lock has been acquired by someone else due to timeout , There's no need to unlock .  

5、 ... and 、 Code implementation

  expireMsecs Lock holding timeout , Prevent the thread from entering the lock , Unlimited execution , So that the lock cannot be released  
  timeoutMsecs Lock wait timeout , Prevent thread starvation , There is never a chance to lock in and execute code  

Be careful : The project needs to be built first redis Related configuration of

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis distributed lock implementation.
 *
 * @author zhengcanrui
 */
public class RedisLock {

    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);

    private RedisTemplate redisTemplate;

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    /**
     * Lock key path.
     */
    private String lockKey;

    /**
     *  Lock timeout , Prevent the thread from entering the lock , Infinite execution waiting 
     */
    private int expireMsecs = 60 * 1000;

    /**
     *  Lock wait time , Prevent thread starvation 
     */
    private int timeoutMsecs = 10 * 1000;

    private volatile boolean locked = false;

    /**
     * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.
     *
     * @param lockKey lock key (ex. account:1, ...)
     */
    public RedisLock(RedisTemplate redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * Detailed constructor with default lock expiration of 60000 msecs.
     *
     */
    public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) {
        this(redisTemplate, lockKey);
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     * Detailed constructor.
     *
     */
    public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
        this(redisTemplate, lockKey, timeoutMsecs);
        this.expireMsecs = expireMsecs;
    }

    /**
     * @return lock key
     */
    public String getLockKey() {
        return lockKey;
    }

    private String get(final String key) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.get(serializer.serialize(key));
                    connection.close();
                    if (data == null) {
                        return null;
                    }
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            logger.error("get redis error, key : {}", key);
        }
        return obj != null ? obj.toString() : null;
    }

    private boolean setNX(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            logger.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (Boolean) obj : false;
    }

    private String getSet(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(ret);
                }
            });
        } catch (Exception e) {
            logger.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (String) obj : null;
    }

    /**
     *  get  lock.
     *  Realize the idea :  Mainly used redis  Of setnx command , Cache lock .
     * reids The cache key It's locked key, All the sharing , value It's the expiration time of the lock ( Be careful : Let's put the expiration date here value 了 , There's no time to set its timeout )
     *  Execution process :
     * 1. adopt setnx Try to set something up key Value , success ( There is no such lock at present ) Then return to , Successful lock acquisition 
     * 2. Get the expiration time of the lock if the lock already exists , Compare it to the current time , If the timeout , Then set the new value 
     *
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = String.valueOf(expires); // Lock expiration time 
            if (this.setNX(lockKey, expiresStr)) {
                // lock acquired
                locked = true;
                return true;
            }

            String currentValueStr = this.get(lockKey); //redis Time in 
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                // Determine whether it is null , Not empty , If the value is set by another thread , Then the second condition is not to pass 
                // lock is expired

                String oldValueStr = this.getSet(lockKey, expiresStr);
                // Get last lock expiration time , And set the current lock expiration time ,
                // Only one thread can get the setting time of the previous line , because jedis.getSet It's synchronous 
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    // Prevent false deletion ( Cover , because key It's the same ) Someone else's lock —— It doesn't work here , Here the value will be overridden , But because there is little time difference , So it's acceptable 

                    //[ In the case of distributed ]: If after this time , Multiple threads happen to be here , But only one thread has the same set value as the current one , He has the right to a lock 
                    // lock acquired
                    locked = true;
                    return true;
                }
            }
            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

            /*
                 Delay 100  millisecond ,   It might be better to use random time here , Can prevent the emergence of hunger process , namely , When multiple processes arrive at the same time ,
                 Only one process gets the lock , The others are trying on the same frequency , There's some going on in the back , Apply for locks at the same frequency , This may result in the front lock not being satisfied .
                 Using random waiting time can guarantee fairness to some extent 
             */
            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);

        }
        return false;
    }


    /**
     * Acqurired lock release.
     */
    public synchronized void unlock() {
        if (locked) {
            redisTemplate.delete(lockKey);
            locked = false;
        }
    }

}

  call :

RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000);
 try {
            if(lock.lock()) {
                   // Code that needs to be locked 
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            // In order to make the distributed lock algorithm more stable , The client holding the lock should check again whether its lock has timed out before unlocking , Do it again DEL operation , Because it is possible that the client is suspended due to a time-consuming operation ,
            // At the end of the operation, the lock has been acquired by someone else due to timeout , There's no need to unlock . ———— It's not done here 
            lock.unlock();
        }

6、 ... and 、 Some of the problems

1、 Why not use it directly expire Set timeout , The number of milliseconds of time is taken as value Put it in redis in ?

In the following way , Give the overtime to redis Handle :

lock(key, expireSec){
isSuccess = setnx key
if (isSuccess)
expire key expireSec
}

   There seems to be no problem in this way , But if setnx after ,redis collapsed ,expire There is no implementation , The result is a deadlock . The lock will never time out .

 2、 Why the front lock has timed out , And use it getSet Set the time of the new timestamp to get the old value , Then compare it with the timestamp used to determine the timeout time ?

   Because it is a distributed environment , When the previous lock fails , There are two processes that enter the judgment of lock timeout . Such as :

C0 It's overtime , And hold the lock ,C1/C2 At the same time, the request enters the method

C1/C2 Got it C0 Timeout for

C1 Use getSet Method

C2 It also carried out getSet Method

If we don't add  oldValueStr.equals(currentValueStr) The judgment of the , will C1/C2 Will be locked , After adding , Can guarantee C1 and C2 Only one can get the lock , One can only wait .

Be careful : This may cause the timeout time to be different from its original timeout time ,C1 The timeout of may be C2 covers , But the millisecond difference between them is extremely small , It's ignored here .

原网站

版权声明
本文为[qq_ twenty-eight million nine hundred and seventeen thousand fo]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/162/202206110918381166.html