In Java, I think everyone is familiar with locks. In concurrent programming, we use locks to avoid data inconsistency caused by competition. Usually, we use it with synchronized and Lock.
However, locks in Java can only be executed in the same JVM process. What if in a distributed cluster environment?
1, Distributed lock
Distributed lock is an idea, and there are many ways to implement it. For example, if we regard sand beach as a component of distributed lock, it should look like this:
Lock
Step on the beach and leave your footprints, which corresponds to the locking operation. If other processes or threads see footprints on the beach and prove that the lock has been held by others, they wait.
Unlock
Erasing footprints from the beach is the process of unlocking.
Lock timeout
In order to avoid deadlock, we can set a gust of wind to blow after unit time and erase the footprints automatically.
There are many implementations of distributed locks, such as database-based, memcached, Redis, system files, zookeeper, etc. Their core idea is roughly the same as the above process.
2, redis
Let's first look at how to implement a simple distributed lock through single node Redis.
1. Lock
Locking actually means setting a value for the Key in redis to avoid deadlock and give an expiration time.
SET lock_key random_value NX PX 5000
It is worth noting that:
random_value is the only string generated by the client.
NX stands for setting the key only when the key does not exist.
The expiration time of the PX 5000 setting key is 5000 milliseconds.
In this way, if the above command is executed successfully, it proves that the client has obtained the lock.
2. Unlock
The unlocking process is to delete the Key. However, it cannot be deleted randomly. It cannot be said that the request of client 1 deletes the lock of client 2. At this time, random_ The role of value is reflected.
In order to ensure the atomicity of the unlocking operation, we use LUA script to complete this operation. First judge whether the string of the current lock is equal to the incoming value. If yes, delete the Key and unlock it successfully.
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
3. Realize
First, we introduce Jedis into the pom file. Here, the author uses the latest version. Note that the API may be different due to different versions.
redis.clients
jedis
3.0.1
The process of locking is very simple, that is, SET the value through the SET instruction, and return if successful; Otherwise, it will wait circularly. If the lock is not obtained within the timeout time, the acquisition fails.
@Service
public class RedisLock {
Logger logger = LoggerFactory.getLogger(this.getClass()); private String lock_key = "redis_lock"; //Lock key protected long internalLockLeaseTime = 30000;//Lock expiration time private long timeout = 999999; //Timeout to acquire lock //Parameters of the SET command SetParams params = SetParams.setParams().nx().px(internalLockLeaseTime); @Autowired JedisPool jedisPool; /** * Lock * @param id * @return */ public boolean lock(String id){ Jedis jedis = jedisPool.getResource(); Long start = System.currentTimeMillis(); try{ for(;;){ //If the SET command returns OK, the lock acquisition is successful String lock = jedis.set(lock_key, id, params); if("OK".equals(lock)){ return true; } //Otherwise, if the lock is not acquired within the timeout time after the cyclic wait, the acquisition fails long l = System.currentTimeMillis() - start; if (l>=timeout) { return false; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }finally { jedis.close(); } }
}
Unlock, we can execute a LUA through jedis.eval. Pass the Key of the lock and the generated string as parameters.
/**
*Unlock
* @param id
* @return
*/
public boolean unlock(String id){
Jedis jedis = jedisPool.getResource();
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList(lock_key),
Collections.singletonList(id));
if("1".equals(result.toString())){
return true;
}
return false;
}finally {
jedis.close();
}
}
Finally, we can test it in a multithreaded environment. We start 1000 threads to accumulate count. When calling, the key is to generate a unique string. Here, the author uses Snowflake algorithm.
@Controller
public class IndexController {
@Autowired RedisLock redisLock; int count = 0; @RequestMapping("/index") @ResponseBody public String index() throws InterruptedException { int clientcount =1000; CountDownLatch countDownLatch = new CountDownLatch(clientcount); ExecutorService executorService = Executors.newFixedThreadPool(clientcount); long start = System.currentTimeMillis(); for (int i = 0;i<clientcount;i++){ executorService.execute(() -> { //Obtain the unique ID string through Snowflake algorithm String id = IdUtil.getId(); try { redisLock.lock(id); count++; }finally { redisLock.unlock(id); } countDownLatch.countDown(); }); } countDownLatch.await(); long end = System.currentTimeMillis(); logger.info("Number of execution threads:{},Total time:{},count Number is:{}",clientcount,end-start,count); return "Hello"; }
}
So far, the implementation of distributed locks for single node Redis has been completed. It is relatively simple, but the problem is also relatively large. The most important point is that locks are not reentrant.
3, redisson
Redisson Is set up in Redis Based on a Java In memory data grid( In-Memory Data Grid). Make full use of Redis Key value database provides a series of advantages based on Java The common interfaces in the utility toolkit provide users with a series of common tool classes with distributed characteristics. As a result, the toolkit originally used to coordinate single machine multithreaded concurrent programs has the ability to coordinate distributed multi machine multithreaded concurrent systems, which greatly reduces the difficulty of designing and developing large-scale distributed systems. At the same time, combined with various characteristic distributed services, it makes further progress One step simplifies the collaboration between programs in a distributed environment.
Compared with Jedis, Redisson is a powerful group. Of course, its complexity comes with it. It also implements distributed locks and contains many types of locks. For more information, see distributed locks and synchronizers
1. Reentrant lock
The Redis distributed lock we implemented above is not reentrant. Let's take a look at how to call reentrant locks in Redisson.
Here, the author uses its latest version, 3.10.1.
org.redisson
redisson
3.10.1
First, get the instance of RedissonClient client through configuration, and then get the instance of lock through getLock.
public static void main(String[] args) {
Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); config.useSingleServer().setPassword("redis1234"); final RedissonClient client = Redisson.create(config); RLock lock = client.getLock("lock1"); try{ lock.lock(); }finally{ lock.unlock(); }
}
2. Get lock instance
Let's first look at RLock lock = client.getLock("lock1"); this code is to obtain the lock instance, and then we can see that it returns a RedissonLock object.
public RLock getLock(String name) {
return new RedissonLock(connectionManager.getCommandExecutor(), name);
}
The RedissonLock constructor mainly initializes some properties.
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
//Command actuator
this.commandExecutor = commandExecutor;
//UUID string
this.id = commandExecutor.getConnectionManager().getId();
//Internal lock expiration time
this.internalLockLeaseTime = commandExecutor.
getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = id + ":" + name;
}
3. Lock
When we call the lock method, locate lockinterruptible. Here, the logic of locking is completed.
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
//Current thread ID long threadId = Thread.currentThread().getId(); //Attempt to acquire lock Long ttl = tryAcquire(leaseTime, unit, threadId); // If ttl is empty, the lock acquisition is successful if (ttl == null) { return; } //If the lock acquisition fails, subscribe to the channel corresponding to the lock RFuture<RedissonLockEntry> future = subscribe(threadId); commandExecutor.syncSubscription(future); try { while (true) { //Try to acquire the lock again ttl = tryAcquire(leaseTime, unit, threadId); //If ttl is empty, it indicates that the lock has been obtained successfully. Return if (ttl == null) { break; } //If ttl is greater than 0, wait for ttl time and continue to try to get if (ttl >= 0) { getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } else { getEntry(threadId).getLatch().acquire(); } } } finally { //Unsubscribe from channel unsubscribe(future, threadId); } //get(lockAsync(leaseTime, unit));
}
The above code is the whole process of locking. First call tryAcquire to obtain the lock. If the return value ttl is empty, it proves that locking is successful and returns; if it is not empty, it proves that locking fails. At this time, it will subscribe to the Channel of the lock, wait for the lock release message, and then try to obtain the lock again. The process is as follows:
Acquire lock
What is the process of obtaining locks? Next, we will look at the tryAcquire method. Here, it has two processing methods, one is a lock with expiration time, and the other is a lock without expiration time.
private RFuture tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
//If there is an expiration time, the lock is acquired in the normal way if (leaseTime != -1) { return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } //First, execute the method of obtaining the lock according to the expiration time of 30 seconds RFuture<Long> ttlRemainingFuture = tryLockInnerAsync( commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); //If the lock is still held, open the scheduled task to continuously refresh the expiration time of the lock ttlRemainingFuture.addListener(new FutureListener<Long>() { @Override public void operationComplete(Future<Long> future) throws Exception { if (!future.isSuccess()) { return; } Long ttlRemaining = future.getNow(); // lock acquired if (ttlRemaining == null) { scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture;
}
Next, look down. The tryLockInnerAsync method actually executes the logic of obtaining locks. It is a LUA script code. Here, it uses a hash data structure.
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit,
long threadId, RedisStrictCommand command) {
//Expiration time internalLockLeaseTime = unit.toMillis(leaseTime); return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command, //If the lock does not exist, set its value through hset and set the expiration time "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + //If the lock already exists and the lock is the current thread, increment the value by 1 through hincrby "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + //If the lock already exists but is not this thread, the expiration time ttl is returned "return redis.call('pttl', KEYS[1]);", Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); }
This LUA code does not seem complicated. There are three judgments:
adopt exists Judge that if the lock does not exist, set the value and expiration time and lock successfully adopt hexists Judge that if the lock already exists and the lock is the current thread, it is proved that it is a re-entry lock and the locking is successful If the lock already exists, but the lock is not the current thread, it proves that another thread holds the lock. Returns the expiration time of the current lock. Locking failed
After locking is successful, there will be a hash structure in the memory data of redis. Key is the name of the lock; field is random string + thread ID; The value is 1. If the lock method is called multiple times by the same thread, the value is incremented by 1.
127.0.0.1:6379> hgetall lock1
- "b5ae0be4-5623-45a5-8faa-ab7eb167ce87:1"
- "1"
4. Unlock
We unlock it by calling the unlock method.
public RFuture unlockAsync(final long threadId) {
final RPromise result = new RedissonPromise();
//Unlocking method RFuture<Boolean> future = unlockInnerAsync(threadId); future.addListener(new FutureListener<Boolean>() { @Override public void operationComplete(Future<Boolean> future) throws Exception { if (!future.isSuccess()) { cancelExpirationRenewal(threadId); result.tryFailure(future.cause()); return; } //Get return value Boolean opStatus = future.getNow(); //If NULL is returned, it proves that the unlocked thread and the current lock are not the same thread, and an exception is thrown if (opStatus == null) { IllegalMonitorStateException cause = new IllegalMonitorStateException(" attempt to unlock lock, not locked by current thread by node id: " + id + " thread-id: " + threadId); result.tryFailure(cause); return; } //Unlock succeeded. Cancel the scheduled task that refreshes the expiration time if (opStatus) { cancelExpirationRenewal(null); } result.trySuccess(null); } }); return result;
}
Then we'll look at the unlockInnerAsync method. Here is also a LUA script code.
protected RFuture unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, EVAL,
//If the lock no longer exists, release the lock release message "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; " + "end;" + //If the thread that releases the lock and the thread that already has the lock are not the same thread, null is returned "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil;" + "end; " + //Release the lock once by decreasing hincrby by 1 //If the remaining times is greater than 0, refresh the expiration time "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + "if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + //Otherwise, it proves that the lock has been released. Delete the key and publish the lock release message "else " + "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; "+ "end; " + "return nil;", Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
The above code is the logic to release the lock. Similarly, it has three judgments:
If the lock no longer exists, pass publish Release the lock release message. The lock is unlocked successfully If the unlocked thread is different from the thread of the current lock, the unlocking fails and an exception is thrown adopt hincrby Decrement by 1, first release the lock. If the remaining times are greater than 0, it proves that the current lock is a re-entry lock and the expiration time is refreshed; If the remaining times are less than 0, delete key And release the lock release message. The unlock is successful
So far, the logic of reentrant lock in Redisson has been analyzed. However, it should be noted that the above two implementation methods are for single Redis instances. If we have multiple Redis instances, please refer to the redislock algorithm. For the specific content of the algorithm, please refer to http://redis.cn/topics/distlock.html