Handwritten Redis distributed lock (2)
1. Analysis of locking and unlocking ideas
In “Handwritten Redis Distributed Lock (1)”, we have already obtained the lock through “while judgment and spin retry + setnx with expiration time + Lua script to delete the lock”, but we have not considered the reentrancy of the lock Sex.
? A reliable distributed lock needs to meet the following conditions:
- exclusivity
- high availability
- anti-deadlock
- No looting
- Reentrancy
? So what is a reentrant lock? Reentrant lock, also known as recursive lock, means that when the same thread acquires the lock in the outer method, the inner method that enters the thread will automatically acquire the lock (the premise is that the lock object must be the same object). It has been acquired before and has not been released yet blocked. Both ReentrantLock and synchronized in Java are reentrant locks. One advantage of reentrant locks is that deadlocks can be avoided to a certain extent.
? Synchronized reentrant implementation principle:
? 1. Each lock object has a lock counter and a pointer to the thread holding the lock.
-
When monitorenter is executed, if the counter of the target lock object is zero, it means that it is not held by other threads, and the Java virtual machine sets the holding thread of the lock object as the current thread, and adds 1 to its counter.
-
If the counter of the target lock object is not zero, if the thread holding the lock object is the current thread, then the Java virtual machine can add 1 to the counter, otherwise it needs to wait until the thread holding the lock releases the lock.
-
When executing monitorexit, the Java virtual machine needs to decrement the counter of the lock object by 1. A counter of zero indicates that the lock has been released.
? Considering the principle of synchronized reentrancy implementation, we can use the Hash data structure in Redis to realize the reentrancy of locks. Locking and unlocking are realized through HINCRBY
s.
? Locking idea:
- First judge whether the key of the Redis distributed lock exists (
EXISTS key
), and the judgment statement returns 0 to indicate that it does not exist, andhset
creates a new lock that belongs to the current thread (HSET key UUID:ThreadID
), and set the expiration time, and then return 1, indicating that the lock is successful; - If the judgment statement returns 1, it means that there is already a lock, and it is necessary to further judge whether the lock belongs to the current thread itself. (
HEXISTS key UUID:ThreadID
), the judgment statement returns 1, indicating that it is its own lock, and auto-increment means reentry (HINCRBY key UUID:ThreadID
), then return 1, indicating Locking succeeded. Return 0 to indicate that it is not your own, and then return 0 to indicate that the lock failed.
? Translate the above ideas into Lua scripts:
if redis.call('exists','key') == 0 then redis.call('hset','key','uuid:threadid',1) redis.call('expire','key',30) return 1 elseif redis.call('hexists','key','uuid:threadid') == 1 then redis.call('hincrby','key','uuid:threadid',1) redis.call('expire','key',30) return 1 else return 0 end
The merge operation is:
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then redis.call('hincrby','key','uuid:threadid',1) redis.call('expire','key',30) return 1 else return 0 end
The final Lua script is:
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis. call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end
Description, if there is no lock, or there is a lock and it is held by the current thread lock. Then perform a + 1 operation on the lock count and refresh the expiration time.
Unlocking idea: Judging “there is a lock and it is still the lock held by the current thread” (HEXISTS key uuid:ThreadID
), if the judgment statement returns 0, it means that there is no lock at all lock, return nil. If the return of the judgment statement is not 0, then it means that there is a lock and it is your own lock. Directly perform HINCRBY -1
to decrement the lock count by one (unlock once). If the lock count becomes 0, delete it the lock.
Translate to Lua script as:
if redis.call('HEXISTS',lock,uuid:threadID) == 0 then return nil elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then return redis. call('del', lock) else return 0 end
The final Lua script is:
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end
2. Implementation of locking and unlocking
- Meet the AQS interface specification definition for Lock in JUC to implement the code, create a new RedisDistributedLock class and implement the Lock interface in JUC.
package com.hyw.redislock.mylock; import cn.hutool.core.util.IdUtil; import lombok. SneakyThrows; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; public class RedisDistributedLock implements Lock {<!-- --> private StringRedisTemplate stringRedisTemplate; private String lockName; private String uuidValue; private long expireTime; public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue) {<!-- --> this.stringRedisTemplate = stringRedisTemplate; this. lockName = lockName; this.uuidValue = uuidValue + ":" + Thread.currentThread().getId(); this. expireTime = 30L; } @Override public void lock() {<!-- --> this. tryLock(); } @Override public boolean tryLock() {<!-- --> try {<!-- --> return this. tryLock(-1L, TimeUnit. SECONDS); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); } return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {<!-- --> if(time != -1L) {<!-- --> expireTime = unit.toSeconds(time); } String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " + "redis. call('hincrby',KEYS[1],ARGV[1],1)" + "redis. call('expire',KEYS[1],ARGV[2])" + "return 1 " + "else " + "return 0 " + "end"; System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue); while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {<!-- --> try {<!-- --> TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); } } return true; } @Override public void unlock() {<!-- --> String script = "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " + "return nil" + "elseif redis. call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " + "return redis. call('del',KEYS[1]) " + "else " + "return 0 " + "end"; System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue); Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)); if(flag == null) {<!-- --> throw new RuntimeException("No such lock, no HEXISTS query"); } } //================================================== ========= @Override public void lockInterruptibly() throws InterruptedException {<!-- --> } @Override public Condition newCondition() {<!-- --> return null; } }
- In order to facilitate future expansion, the factory model is introduced.
?
package com.hyw.redislock.mylock; import cn.hutool.core.util.IdUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.locks.Lock; @Component public class DistributedLockFactory {<!-- --> @Autowired private StringRedisTemplate stringRedisTemplate; private String lockName; private String uuidValue; public DistributedLockFactory() {<!-- --> this.uuidValue = IdUtil.simpleUUID();//UUID } public Lock getDistributedLock(String lockType) {<!-- --> if(lockType == null) return null; if(lockType.equalsIgnoreCase("REDIS")){<!-- --> lockName = "RedisLock"; return new RedisDistributedLock(stringRedisTemplate, lockName, uuidValue); } else if(lockType. equalsIgnoreCase("ZOOKEEPER")){<!-- --> //TODO zookeeper version distributed lock implementation //return new ZookeeperDistributedLock(); return null; } else if(lockType. equalsIgnoreCase("MYSQL")){<!-- --> //TODO mysql version distributed lock implementation return null; } return null; } }
-
Then InventoryService can use the lock we wrote and test the reentrancy of the lock.
package com.hyw.redislock.service; import cn.hutool.core.util.IdUtil; import com.hyw.redislock.mylock.DistributedLockFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service @Slf4j public class InventoryService {<!-- --> @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String port; @Autowired private DistributedLockFactory distributedLockFactory; public String sale() {<!-- --> String retMessage = ""; Lock redisLock = distributedLockFactory. getDistributedLock("redis"); // lock redisLock. lock(); try {<!-- --> //1 Query inventory information String result = stringRedisTemplate.opsForValue().get("inventory001"); //2 Determine whether the inventory is sufficient Integer inventoryNumber = result == null ? 0 : Integer. parseInt(result); //3 Deduct inventory if(inventoryNumber > 0) {<!-- --> stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber)); retMessage = "Successfully sold a product, remaining inventory: " + inventoryNumber; System.out.println(retMessage); this.testReEnter(); }else{<!-- --> retMessage = "The product is sold out, o(╥﹏╥)o"; } }catch (Exception e){<!-- --> e.printStackTrace(); }finally {<!-- --> // unlock redisLock. unlock(); } return retMessage + "\t" + "Service port number:" + port; } private void testReEnter() {<!-- --> Lock redisLock = distributedLockFactory. getDistributedLock("redis"); redisLock. lock(); try {<!-- --> System.out.println("################# test reentrant lock ####################### ################"); }finally {<!-- --> redisLock. unlock(); } } }
3. Automatic renewal
? Consider that if the business has not been executed after the lock expires, then the lock needs to be automatically renewed.
? Automatic renewal idea: If the lock still exists, it means that the business has not been executed, and then reset the expiration time of the lock.
? The Lua script is:
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then return redis. call('expire', KEYS[1], ARGV[2]) else return 0 end
And add the business of regularly scanning whether the business is completed in RedisDistributedLock
private void renewExpire() {<!-- --> String script = "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " + "return redis. call('expire',KEYS[1],ARGV[2])" + "else " + "return 0 " + "end"; // Scan every third of the expiration time. If the renewal is successful this time, it means that the business has not been completed, and the scan is performed again recursively. new Timer().schedule(new TimerTask() {<!-- --> @Override public void run() {<!-- --> if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {<!-- --> renewExpire(); } } },(this. expireTime * 1000)/3); }
Then RedisDistributedLock is modified to:
package com.hyw.redislock.mylock; import cn.hutool.core.util.IdUtil; import lombok. SneakyThrows; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; public class RedisDistributedLock implements Lock {<!-- --> private StringRedisTemplate stringRedisTemplate; private String lockName; private String uuidValue; private long expireTime; public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue) {<!-- --> this.stringRedisTemplate = stringRedisTemplate; this. lockName = lockName; this.uuidValue = uuidValue + ":" + Thread.currentThread().getId(); this. expireTime = 30L; } @Override public void lock() {<!-- --> this. tryLock(); } @Override public boolean tryLock() {<!-- --> try {<!-- --> return this. tryLock(-1L, TimeUnit. SECONDS); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); } return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {<!-- --> if(time != -1L) {<!-- --> expireTime = unit.toSeconds(time); } String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " + "redis. call('hincrby',KEYS[1],ARGV[1],1)" + "redis. call('expire',KEYS[1],ARGV[2])" + "return 1 " + "else " + "return 0 " + "end"; System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue); while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {<!-- --> try {<!-- --> TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); } } // auto-renew this.renewExpire(); return true; } @Override public void unlock() {<!-- --> String script = "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " + "return nil" + "elseif redis. call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " + "return redis. call('del',KEYS[1]) " + "else " + "return 0 " + "end"; System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue); Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)); if(flag == null) {<!-- --> throw new RuntimeException("No such lock, no HEXISTS query"); } } private void renewExpire() {<!-- --> String script = "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " + "return redis. call('expire',KEYS[1],ARGV[2])" + "else " + "return 0 " + "end"; // Scan every third of the expiration time. If the renewal is successful this time, it means that the business has not been completed, and the scan is performed again recursively. new Timer().schedule(new TimerTask() {<!-- --> @Override public void run() {<!-- --> if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {<!-- --> renewExpire(); } } },(this. expireTime * 1000)/3); } //================================================== ========= @Override public void lockInterruptibly() throws InterruptedException {<!-- --> } @Override public Condition newCondition() {<!-- --> return null; } }
Four. Summary
So far, our handwritten Redis distributed lock has exclusivity, high availability, anti-deadlock, no random grabbing, and reentrancy.
But what if the Redis we use for lock and unlock operations hangs up? Redisson distributed locks can solve this problem.