1. An overview of the

In distributed systems, the use of distributed locks is often involved when operations on the same resources are involved. Redis is a single-process single-thread mode. Through the Redis command SETNX, GET can facilitate the implementation of distributed locks. This paper realizes distributed lock by redis command first, introduces the main business logic and points out its shortcomings. Then through lua script to realize distributed lock, make up for its shortcomings. Finally, the pressure test of the lock is carried out by AB to compare the performance of the two.

2. Run the redis command to implement distributed locks

2.1. SETNX

Syntax: SETNX key value

  • If the key does not exist, the value (key:value) is stored and 1 is returned
  • If the key does not exist, 0 is returned

Because of the nature of this command, only one thread can change the key value when multiple threads are competing. This can be used to achieve mutually exclusive lock function.

2.2. ILock and DistributeLock

Define locks: There are two main methods: lock and unlock

@author hry 3. */ public interface ILock {/** * @param lock lock name */ void lock(String lock); /** * @param lock lock name */ void unlock(String lock); }Copy the code

ILock implements the DistributeLock class:

  1. ThreadLocal threadId: Stores the UUID value of each thread lock by threadId, which is used to distinguish whether the current lock is owned by the thread, and the value of the lock also stores this value
  2. The lock logic is as follows: setIfAbsent on BoundValueOperations sets the lockKey value (setIfAbsent encapsulates SETNX). If true is returned, the lock has been obtained. If false is returned, wait is entered
  3. Unlock logic: Release the lock by redistemplate.delete. Before releasing the lock, check that the current lock is owned by the current thread. If so, release the lock. Otherwise, do not release the lock
  4. Deadlock avoidance: If thread A acquires the lock and suddenly dies before releasing it, no other thread can acquire the lock again, resulting in A deadlock. To avoid deadlocks, once we acquire the lock, we need to set an expiration date for the lock so that the lock can be released automatically even if the owner of the lock dies
  5. The lock can be reentrant: thread A can acquire the lock again if it executes the lock again, instead of appearing in the queue waiting for the lock. If the current thread has already acquired the lock, it must be able to obtain the lock again. Otherwise, it will wait for itself to release the lock, resulting in deadlock

See the code for detailed implementation:

@author hry ** / public class DistributeLock implements ILock {private static final Logger = LoggerFactory.getLogger(DistributeLock.class); private static final int LOCK_MAX_EXIST_TIME = 5; Private static final String LOCK_PREX = "lock_"; private static final String LOCK_PREX = "lock_"; Private StringRedisTemplate redisTemplate; private String lockPrex; Private int lockMaxExistTime; Private ThreadLocal<String> threadId = new ThreadLocal<String>(); Public DistributeLock(StringRedisTemplate redisTemplate){this(redisTemplate, LOCK_PREX, LOCK_MAX_EXIST_TIME); } public DistributeLock(StringRedisTemplate redisTemplate, String lockPrex, int lockMaxExistTime){ this.redisTemplate = redisTemplate; this.lockPrex = lockPrex; this.lockMaxExistTime = lockMaxExistTime; } @Override public void lock(String lock){ Assert.notNull(lock, "lock can't be null!" ); String lockKey = getLockKey(lock); BoundValueOperations<String,String> keyBoundValueOperations = redisTemplate.boundValueOps(lockKey); While (true) {/ / if the last time to get the lock is their own, this can also get a lock that realize the reentrant String value = keyBoundValueOperations. The get (); If (value! = null && value equals (String. The valueOf (threadId. The get ()))) {/ / reset the expiration time keyBoundValueOperations. Expire (lockMaxExistTime, TimeUnit.SECONDS); break; } the if (keyBoundValueOperations setIfAbsent (lockKey)) {/ / every time acquiring a lock, You must regenerate the id value String keyUniqueId = uuID.randomuuid ().toString(); // Generate a unique value for key threadid.set (keyUniqueId); / / set the value, and then set the expiration date, expiration date otherwise invalid keyBoundValueOperations. Set (String) the valueOf (keyUniqueId)); // To prevent a user from getting the lock and not releasing it normally, set a default expiration. Can cause a deadlock keyBoundValueOperations. Expire (lockMaxExistTime, TimeUnit. SECONDS); // Get the lock, break the loop; Thread.sleep(10, (int)(math.random () * 500)); } catch (InterruptedException e) { break; }}}} /** * Release the lock and consider whether the current lock is owned by the current thread. The following conditions can cause the current thread to lose the lock: The thread executes for longer than the timeout period, causing the lock to be taken by another thread; * a. The lock is owned by this thread, but before the deletion, the lock timeout is released and another thread acquires the lock. * * Final solution * A. Public void unlock(final String lock) {final String lockKey = getLockKey(lock); BoundValueOperations<String,String> keyBoundValueOperations = redisTemplate.boundValueOps(lockKey); String lockValue = keyBoundValueOperations.get(); if(! StringUtils.isEmpty(lockValue) && lockValue.equals(threadId.get())){ redisTemplate.delete(lockKey); }else{logger.warn("key=[{}] has been released and will not be released this time. [{}] ", lock, lockValue); Private String getLockKey(String Lock){StringBuilder sb = new StringBuilder(); sb.append(lockPrex).append(lock); return sb.toString(); }}Copy the code

2.3. ILockManager and SimpleRedisLockManager

ILockManager: Encapsulates distributed lock usage

Public interface ILockManager {/** * * @param lockKeyName Key name * @param callback */ void lockCallBack(String lockKeyName, SimpleCallBack callback); /** * Secure execution of program by locking, @param lockKeyName * @param callback * @return */ <T> T lockCallBackWithRtn(String lockKeyName, ReturnCallBack<T> callback); }Copy the code

SimpleRedisLockManager An implementation class of ILockManager that initializes the lock implemented above; This class encapsulates common code for using locks, simplifying the use of distributed locks. Two callback methods are defined for the user’s actual business logic implementation

  1. SimpleCallBack: Callback function with no return value
  2. ReturnCallBack: a callback function that returns data
@Component public class SimpleRedisLockManager implements ILockManager { @Autowired protected StringRedisTemplate redisTemplate; protected ILock distributeLock; // distributeLock = new distributeLock (redisTemplate, "mylock_", 5); // distributeLock = new distributeLock (redisTemplate, "mylock_", 5); } @Override public void lockCallBack(String lockKeyName, SimpleCallBack callback){Assert. NotNull ("lockKeyName","lockKeyName cannot be null "); Assert. NotNull ("callback","callback cannot be null "); Try {// Obtain the lock distributelock. lock(lockKeyName); callback.execute(); }finally{// Must release the lock distributelock. unlock(lockKeyName); } } @Override public <T> T lockCallBackWithRtn(String lockKeyName, ReturnCallBack<T> callback){Assert. NotNull ("lockKeyName","lockKeyName cannot be null "); Assert. NotNull ("callback","callback cannot be null "); Try {// Obtain the lock distributelock. lock(lockKeyName); return callback.execute(); }finally{// Must release the lock distributelock. unlock(lockKeyName); @author hry ** / public interface SimpleCallBack {void execute(); ** @author hry ** @param <T> */ public interface callback <T> {T execute(); }Copy the code

2.4. The code TestCtrl that actually uses the lock

Very simple to use

@Autowired private SimpleRedisLockManager simpleRedisLockManager; simpleRedisLockManager.lockCallBack("distributeLock" + ThreadLocalRandom.current().nextInt(1000), new SimpleCallBack() { @Override public void execute() { System.out.println("lockCallBack"); }});Copy the code

2.5. The above lock implementation still has shortcomings

  1. If thread A holds the lock longer than the specified time, redis will release the lock automatically. If thread B gets the lock, thread A and thread B get the lock at the same time. This can be resolved by setting a reasonable timeout.
  2. If the amount of concurrency is high, multiple threads may have locks at the same time. This is because both the Lock and UNLOCK methods in DistributeLock execute multiple statements that are not transactional. For example, when thread A is unlocking, it knows that it owns the lock through A get method, and then it releases the lock. Between these two operations, Redis finds that the lock has expired and automatically deletes the lock, at which point thread B applies for and gets the lock. Only then can thread A remove the lock, and thread C can also acquire the lock, and thread B and thread C simultaneously acquire the lock. This situation can be resolved by the Lua method below

3. Use the Lua script to implement distributed locks

The problem with the above lock implementation is that multiple statements are not executed in a transaction. Lua, introduced in this section, solves this problem. Lua scripting is supported in redis versions after 2.6.0. Lua is used in Redis in detail here. When executing a Lua script in Redis, redis will execute the whole script as a whole, and will not be inserted by other commands, solving the problem of multiple command objects.

3.1. Lua lock script

The lock script: lock. Lua

-- Set a lock 1 local key = KEYS[1] local content = KEYS[2] local TTL = ARGV[1] local lockSet = redis. Call ('setnx', key, Content) if lockSet == 1 then redis. Call ('pexpire', key, TTL) -- redis. Call ('incr', "count") else -- if value is the same, Local value = redis. Call ('get', key) if(value == content) then lockSet = 1; redis.call('pexpire', key, ttl) end end return lockSetCopy the code

Unlock script: unlock. Lua

-- unlock key
local key     = KEYS[1]
local content = KEYS[2]
local value = redis.call('get', key)
if value == content then
--  redis.call('decr', "count")
  return redis.call('del', key);
end
return 0Copy the code

3.2. LuaDistributeLock

LuaDistributeLock implements the same service logic as DistributeLock. When LuaDistributeLock is created, the init method is used to initialize lock and UNLOCK scripts and generate DefaultRedisScript objects. These two objects can be reused without the need to initialize an object each time you execute lock/ UNLOCK

public class LuaDistributeLock implements ILock { private static final int LOCK_MAX_EXIST_TIME = 5; Private static final String LOCK_PREX = "lock_"; private static final String LOCK_PREX = "lock_"; Private StringRedisTemplate redisTemplate; private String lockPrex; Private int lockMaxExistTime; Private DefaultRedisScript<Long> lockScript; private DefaultRedisScript<Long> lockScript; Private DefaultRedisScript<Long> unlockScript; Private ThreadLocal<String> threadKeyId = new ThreadLocal<String>(){@override protected String initialValue() { return UUID.randomUUID().toString(); }}; public LuaDistributeLock(StringRedisTemplate redisTemplate){ this(redisTemplate, LOCK_PREX, LOCK_MAX_EXIST_TIME); } public LuaDistributeLock(StringRedisTemplate redisTemplate, String lockPrex, int lockMaxExistTime){ this.redisTemplate = redisTemplate; this.lockPrex = lockPrex; this.lockMaxExistTime = lockMaxExistTime; // init init(); } /** * generate */ public void init() {// Lock script lockScript = new DefaultRedisScript<Long>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("com/hry/spring/redis/distributedlock/lock/lock.lua"))); lockScript.setResultType(Long.class); // unlock script unlockScript = new DefaultRedisScript<Long>(); unlockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("com/hry/spring/redis/distributedlock/lock/unlock.lua"))); unlockScript.setResultType(Long.class); } @Override public void lock(String lock2){ Assert.notNull(lock2, "lock2 can't be null!" ); String lockKey = getLockKey(lock2); while(true){ List<String> keyList = new ArrayList<String>(); keyList.add(lockKey); keyList.add(threadKeyId.get()); if(redisTemplate.execute(lockScript, keyList, String.valueOf(lockMaxExistTime * 1000)) > 0){ break; Thread.sleep(10, (int)(math.random () * 500)); } catch (InterruptedException e) { break; }}}} /** * Release the lock and consider whether the current lock is owned by the current thread. The following conditions can cause the current thread to lose the lock: The thread executes for longer than the timeout period, causing the lock to be taken by another thread; */ @override public void unlock(final String lock) {final String lockKey = getLockKey(lock); List<String> keyList = new ArrayList<String>(); keyList.add(lockKey); keyList.add(threadKeyId.get()); redisTemplate.execute(unlockScript, keyList); Private String getLockKey(String Lock){StringBuilder sb = new StringBuilder();  sb.append(lockPrex).append(lock); return sb.toString(); }}Copy the code

3.3. LuaLockRedisLockManager

Inherit SimpleRedisLockManager and rewrite init to initialize the lock you just wrote

@Component public class LuaLockRedisLockManager extends SimpleRedisLockManager { @PostConstruct public void init(){ // DistributeLock = new LuaDistributeLock(redisTemplate, "mylock_", 5); }}Copy the code

3.4. The code TestCtrl that actually uses the lock

The usage is the same as SimpleRedisLockManager

@Autowired private LuaLockRedisLockManager luaLockRedisLockManager; luaLockRedisLockManager.lockCallBack("distributeLock2" + ThreadLocalRandom.current().nextInt(1000), new SimpleCallBack() { @Override public void execute() { System.out.println("distributeLock2"); }});Copy the code

4. Performance comparison

Let’s stress test both implementations with the stress test tool AB: 100 concurrent threads sending a total of 1000 requests simpleRedisLockManager: Ab – 1000 – c n 100 http://192.168.188.4:8080/distributeLock2 luaLockRedisLockManager: Ab – n – 1000 c 100 http://192.168.188.4:8080/distributeLock

Detailed data are as follows



Analysis: Lua scripts are much faster than redis implementations, lua scripts are twice as fast as normal commands. Lua performs better in high-stress situations

5. To summarize

To better use locks, you are advised to meet the following conditions

  1. Use Lua for distributed locking
  2. Set a proper timeout period based on the service logic
  3. The granularity of locks should be as small as possible to reduce conflicts

6. Code

See code for details