1 introduction
When we want to ensure the visibility and atomicity of a variable in a program, we can use volatile(read/write atomicity for any single volatile variable, but the compound volatile++ is not atomic), synchronized, optimistic locking, pessimistic locking, and so on. This can be done within a single application, but now, with the development of The Times, most projects have bid farewell to the era of single application and embrace the era of micro services. In this case, many services need to be clustered, and an application needs to be deployed on several machines to do load balancing. It is not feasible to use the above mechanism to guarantee the visibility and atomicity of variables in the case of concurrency (as shown in the following figure), thus many distributed mechanisms (such as distributed transactions, distributed locks, etc.) are generated, which are mainly used to ensure the consistency of data:
As shown above, it is assumed that variable a is remaining stocks, the value is 1, three user in order at this time, just three requests are assigned to three different service node above, three nodes to check remaining inventory, found that there is one, then go to deduct, thus leading to the negative inventory, there are two users aren’t able to send, is commonly known as oversold. This situation is not acceptable, the user will quarrel with the business, the business will quarrel with your leader, and then you pack your bag and go home!
In this scenario, we need a way to solve this problem, and this is the problem that distributed locks solve.
2. Implementation and characteristics of distributed lock
2.1 Implementation of distributed lock
Local lock can be supported by the language itself. To achieve distributed lock, you must rely on middleware, database, Redis, zooKeeper, etc. The main implementation methods are as follows: 1) Memcached: use Memcached add command. This command is atomic, and the add succeeds only if the key does not exist, which means that the thread has acquired the lock. 2) Redis: Similar to Memcached, use Redis setnx. This command is also an atomic operation. The set succeeds only when the key does not exist. 3) Zookeeper: Use the sequential temporary nodes of Zookeeper to realize distributed locks and wait queues. Zookeeper is designed to implement distributed lock services. 4) Chubby: Coarse-grained distributed lock service implemented by Google, which utilizes the Paxos consistency algorithm at the bottom.
2.2 Distributed Lock features
1) In distributed systems, a method can only be executed by one thread on one machine at a time. 2) Highly available lock acquisition and lock release. 3) High-performance lock acquisition and lock release. 4) Reentrant features. 5) With lock failure mechanism to prevent deadlock. 6) It has non-blocking lock feature, that is, if the lock is not obtained, it will directly return the failure to obtain the lock.
3. Redisson realizes Redis distributed lock and its realization principle
3.1 Adding a Dependency
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> The < version > 3.12.4 < / version > < / dependency >Copy the code
3.2 Test View
The number of inventory 100, call once minus 1, less than or equal to 0 return false, indicating that the order failed.
@Component public class RedissonLock { private static Integer inventory = 100; /** * test ** @return true: order successfully false: */ public Boolean redisLockTest(){RLock inventoryLock = redissonservice.getrLock (" inventorynumber "); Try {// add the lock inventorylock. lock(); if (inventory <= 0){ return false; } inventory--; Println (" Thread name: "+ Thread.currentThread().getName() +" + redissonLock. inventory); }catch (Exception e){ e.printStackTrace(); }finally {// Release lock inventorylock. unlock(); } return true; }}Copy the code
Pressure measurement with Jmeter:
Thread group 100 execute for 20 seconds:
The response asserts true for true and false for failure:
Results:
3.3 Obtaining lock instances
RLock inventoryLock = RedissonService.getRLock(“inventory-number”); This is an instance of getting the lock, inventory-number is the name of the lock, and if you go to getLock(String name) you can see that the instance of getting the lock is initializing some properties in the RedissonLock constructor.
public RLock getLock(String name) {
return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
}
Copy the code
Take a look at the RedissonLock constructor:
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) { super(commandExecutor, name); This.mandexecutor = commandExecutor; / / UUID string (MasterSlaveConnectionManager constructor of a class into UUID) enclosing id = commandExecutor. GetConnectionManager (). The getId (); // Internal lock expiration time (to prevent deadlock, The default time is 30 s) enclosing internalLockLeaseTime = commandExecutor. GetConnectionManager () getCfg () getLockWatchdogTimeout (); This. entryName = this.id + ":" + name; / / redis message body enclosing pubSub. = commandExecutor getConnectionManager () getSubscribeService () getLockPubSub (); }Copy the code
Internal lock expiration time (default 30s, if the business code is not executed after this time, then the expiration time will be automatically renewed) :
3.4 lock
inventoryLock.lock(); Lock (); lock(); lock();
public void lock() { try { this.lock(-1L, (TimeUnit)null, false); } catch (InterruptedException var2) { throw new IllegalStateException(); }}Copy the code
Here we set some default values, then continue to call the parameter lock() method, also here, complete the lock logic, source code:
private void lock(long leaseTime, TimeUnit unit, Boolean Interruptibly) throws InterruptedException {// Thread ID long threadId = thread.currentThread ().getid (); Long TTL = this.tryacquire (leaseTime, unit, threadId); // If the expiration time is null, the lock is obtained. If the expiration time is not null, continue with if (TTL! RFuture<RedissonLockEntry> Future = this.subscribe(threadId); If (interruptibly) {/ / interruptible subscribe this.com mandExecutor syncSubscriptionInterrupted (future); } else {/ / do not interrupt. Subscribe to the this.com mandExecutor syncSubscription (future); } try {// loop while(true) {// try to acquire lock TTL = this.tryacquire (leaseTime, unit, threadId); // if TTL (expiration time) is empty, the lock is successfully obtained. If (TTL == null) {return; If (TTL >= 0L) {try {((RedissonLockEntry)future.getNow()).getlatch ().tryacquire (TTL, RedissonLockEntry).getlatch ().tryacquire (TTL, TimeUnit.MILLISECONDS); } catch (InterruptedException var13) { if (interruptibly) { throw var13; } ((RedissonLockEntry)future.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } } else if (interruptibly) { ((RedissonLockEntry)future.getNow()).getLatch().acquire(); } else { ((RedissonLockEntry)future.getNow()).getLatch().acquireUninterruptibly(); }} finally {// Unsubscribe from channel this.unsubscribe(future, threadId); }}}Copy the code
Let’s look at the tryAcquire method for obtaining locks:
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}
Copy the code
Take a look at the tryAcquireAsync method:
Private <T> RFuture<Long> tryAcquireAsync(Long leaseTime, TimeUnit Unit, Long threadId) {// Have set expiration time if (leaseTime! = -1L) { return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } else {// No expiration time RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining == null) { this.scheduleExpirationRenewal(threadId); }}}); return ttlRemainingFuture; }}Copy the code
- The tryLockInnerAsync method, which actually performs the lock acquisition logic, is a LUA script. In this case, it uses a hash data structure.
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); Return this.com mandExecutor. EvalWriteAsync (enclosing getName (), LongCodec. INSTANCE, command, / / if the lock does not exist, by hset set its value, If (redis. Call ('exists', KEYS[1]) == 0) then redis. Call ('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; // If the lock already exists and the current thread is locked, 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; TTL return redis. Call (' PTTL ', KEYS[1]);" , Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); }Copy the code
KEYS[1] represents the key you want to lock, for example: RLock inventoryLock = redissonservice. getRLock(” inventorynumber “); So the lock key that you set yourself is inventory-number. ARGV[1] represents the default lifetime of the lock key. The default lifetime is 30 seconds. ARGV[2] represents the ID of the locked client, similar to 8743C9c0-0795-4907-87FD-6C719a6b4586:1
The above LUA code doesn’t look too complicated, either, with three decisions:
Exists Indicates that the lock does not exist. If the lock does not exist, set the value and expiration time to add the lock successfully. According to hEXISTS, if the lock already exists and the current thread is locked, it is proved to be a reentrant lock. The lock is successfully added, and the value of ARGV[2] +1, which was originally 1, is now changed to 2. If a lock exists, but not by the current thread, then another thread holds the lock. Return the expiration time of the current lock. Lock failed
3.5 the unlock
inventoryLock.unlock(); Unlock (); unlock(); unlock()
public void unlock() { try { this.get(this.unlockAsync(Thread.currentThread().getId())); } catch (RedisException var2) { if (var2.getCause() instanceof IllegalMonitorStateException) { throw (IllegalMonitorStateException)var2.getCause(); } else { throw var2; }}}Copy the code
Go to unlockAsync() and this is how to unlock it:
public RFuture<Void> unlockAsync(long threadId) { RPromise<Void> result = new RedissonPromise(); RFuture<Boolean> Future = this.unlockInnerAsync(threadId); / / add a listener to unlock opStatus: return value future. The onComplete ((opStatus, e) - > {enclosing cancelExpirationRenewal (threadId); if (e ! = null) { result.tryFailure(e); // If null is returned, the unlocked thread and the current lock are not the same thread. An exception is thrown} else if (opStatus = = null) {IllegalMonitorStateException cause = new IllegalMonitorStateException (" attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId); result.tryFailure(cause); } else {// Unlock success result.trySuccess((Object)null); }}); return result; }Copy the code
UnlockInnerAsync () :
protected RFuture<Boolean> unlockInnerAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), Longcodec.instance, RedisCommands.EVAL_BOOLEAN, // If the thread releasing the lock is not the same thread as the thread of the existing lock, Return null "if (redis. Call ('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; Local counter = redis. Call ('hincrby', KEYS[1], ARGV[3], -1); If (counter > 0) then redis. Call ('pexpire', KEYS[1], ARGV[2]); return 0; Redis.call ('del', KEYS[1]); redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;" , Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)}); }Copy the code
The above code is the logic to release the lock. Again, it has three judgments:
If the unlocked thread and the current locked thread are not the same, the unlock fails and an exception is thrown. If the unlocked thread and the current lock thread are the same, the lock is released by hincrby minus 1. If the remaining times are greater than 0, the lock is a reentrant lock and the expiration time is updated again. The lock does not exist. After publishing the release message, the lock is unlocked successfully
Here is the end, eyes over thousands of times as hand over, you try to understand, the boss see here can point a praise, I want to see the horror of the second world, thank you!
If there is a need you can pay attention to my public number, will immediately update the Java related technical articles, public number there are some practical information, such as Java second kill system video tutorial, dark horse 2019 teaching materials (IDEA version), BAT interview summary (classification complete), MAC commonly used installation package (some are taobao to buy, Has PJ’s).