Implementation scheme of lock under monolithic architecture
1. Global lock of ReentrantLock
ReentrantLock (reentrant lock), refers to a situation where a thread's reentrant request for a lock it already holds on a critical resource will succeed.
Compared with the commonly used Synchronized:
ReentrantLock | Synchronized | |
Lock Implementation Mechanism | Depends on AQS | Monitor Pattern |
Flexibility | Supports response timeout, interruption, and attempt to acquire lock | Inflexible |
Release Form | Must explicitly call unlock () to release the lock | Automatic release of the monitor |
Lock Type | Fair Lock & Non-fair Lock | Non-fair Lock |
Condition Queue | Can associate multiple condition queues | Associate a condition queue |
Reentrancy | Reentrant | Reentrant |
AQS Mechanism:If the requested shared resource is idle, then the current thread requesting the resource is set to an effective working thread, and the shared resource is passed through CAScompareAndSetState
Set to the locked state; if the shared resource is occupied, it uses a certain blocking waiting and waking mechanism (CLH variant FIFO double-ended queue) to ensure lock allocation.
Reentrancy:Whether it is a fair lock or a non-fair lock, the locking process will use a state value
private volatile int state
- The state value is initialized to 0, indicating that no thread holds the lock.
- When a thread requests the lock, the state value will increment by 1, and if the same thread acquires the lock multiple times, it will increment by 1 each time, which is the concept of reentrancy.
- Unlocking is also a decrement of the state value until 0, indicating that this thread has released the lock.
public class LockExample {
static int count = 0;
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
// Lock
lock.lock();
for (int i = 0; i < 10000; i++) {
count++;
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
// Unlock, placed in the finally clause to ensure the release of the lock
lock.unlock();
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count: " + count);
}
}
/**
* Output
* count: 20000
*/
2. MySQL row lock, optimistic lock
Optimistic lock is the lock-free idea, which is generally implemented based on CAS (Compare-And-Swap) idea, and in MySQL, optimistic lock is implemented through version number + CAS lock-free form; for example, when T1 and T2 two transactions are executed concurrently, when T2 transaction is successfully committed, it will increment version+1, so the version condition of T1 transaction execution cannot be established.
Locking the SQL statement and performing state machine operations can also avoid data inconsistency problems caused by concurrent access to the count value by different threads.
// Optimistic lock + state machine
update
table_name
set
version = version + 1,
count = count + 1
where
id = id AND version = version AND count = [The previous count value];
// Row lock + state machine
update
table_name
set
count = count + 1
where
id = id AND count = [The previous count value]
for update;
3. Fine-grained ReetrantLock lock
If we directly use ReentrantLock global locking, then in this case, a thread acquires the lock, and all threads in the entire program will be blocked here; but in our project, we want to implement mutual exclusion logic for each user when operating, so we need a more fine-grained lock.
public class LockExample {
private static Map<String, Lock> lockMap = new ConcurrentHashMap<>();
public static void lock(String userId) {
// Add fine-grained lock resources to the Map
lockMap.putIfAbsent(userId, new ReentrantLock());
// Get the lock from the container and implement locking
lockMap.get(userId).lock();
}
public static void unlock(String userId) {
// First, get the lock from the container to ensure the existence of the lock
Lock locak = lockMap.get(userId);
// Release lock
lock.unlock();
}
}
Disadvantages:If each user requests shared resources, the lock will be acquired once, and after that, the user has not logged in to the platform, but the lock object will always exist in memory, which is equivalent to a memory leak, so the lock timeout and淘汰mechanism need to be implemented.
4. Fine-grained Synchronized Global Lock
The above locking mechanism uses a lock containerConcurrentHashMap
, which is easy for thread safety, but the underlying implementation will still useSynchronized
mechanism, so in some cases, using lockMap needs to add two layers of locks.
Then can we directly useSynchronized
to implement a fine-grained locking mechanism
public class LockExample {
public static void syncFunc1(Long accountId) {
String lock = new String(accountId + "").intern();
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
// Simulate business processing time
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " released the lock");
}
}
public static void syncFunc2(Long accountId) {
String lock = new String(accountId + "").intern();
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
// Simulate business processing time
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " released the lock");
}
}
// Use Synchronized to implement finer-grained locking
public static void main(String[] args) {
new Thread(()-> syncFunc1(123456L), "Thread-1").start();
new Thread(()-> syncFunc2(123456L), "Thread-2").start();
}
}
/**
* Print
* Thread-1 acquired the lock
* Thread-1 released the lock
* Thread-2 acquired the lock
* Thread-2 released the lock
*/
- From the code, we find that the object that implements locking is actually a string object related to the user ID. There may be some doubts here, as each new thread that comes in creates a new string object, but the content of the string is the same. How can we ensure that the shared resource can be locked safely;
- This actually needs to be attributed to the following
intern()
The function's functionality; intern()
The function is used to add a string to the string constant pool in the heap space at runtime, if the string already exists, return the reference in the string constant pool.
Lock implementation in a distributed architecture
Core issue:We need to find a visible area among all threads in multiple processes to define this mutual exclusion variable.
An excellent distributed lock implementation should meet the following characteristics:
- In a distributed environment, it can ensure mutual exclusion between threads of different processes
- At the same time, only one thread can successfully acquire the lock resource at a time
- Ensure high availability where mutual exclusion is required
- It needs to ensure that locks and releases can be obtained and released with high performance
- It can support reentrancy of the same thread's lock
- It has a reasonable blocking mechanism, and threads that fail to compete for the lock should have a corresponding solution
- Supports non-blocking lock acquisition. Threads that fail to acquire the lock can directly return
- It has a reasonable lock expiration mechanism, such as timeout expiration, which can ensure that deadlocks do not occur
Redis implements distributed locking
- Redis is a middleware that can be independently deployed;
- It is visible to different Java processes, and the performance is also very可观
- depending on the commands provided by Redis itself
setnx key value
to implement distributed locking; it is different from the ordinaryset
The instruction is to set successfully only when the key does not exist, and return failure to set when the key exists
Code example:
// Inventory deduction interface
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
// Acquire lock
String lockKey = "lock-" + inventory.getInventoryId();
int timeOut = 100;
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, "Bamboo-Panda",timeOut,TimeUnit.SECONDS);
// Adding an expiration time can ensure that deadlocks are also released within a certain period of time
stringRedisTemplate.expire(lockKey,timeOut,TimeUnit.SECONDS);
if(!flag){
// Non-blocking implementation
return "Server busy...Please try again later!!!";
}
// ----Only after successfully obtaining the lock can the following inventory reduction business be executed----
try{
// Query inventory information
Inventory inventoryResult =
inventoryService.selectByPrimaryKey(inventory.getInventoryId());
if (inventoryResult.getShopCount() <= 0) {
return "Insufficient inventory, please contact the seller....";
}
// Deduct inventory
inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
} catch (Exception e) { // Ensure that the lock can be released even if the business throws an exception, to avoid deadlock
// Release lock
stringRedisTemplate.delete(lockKey);
}
if (n > 0)
return "Port-" + port + ", inventory deduction successful!!!";
return "Port-" + port + ", inventory deduction failed!!!";
}
Author: Bamboo loves panda
Link: https://juejin.cn/post/7038473714970656775
Analysis of the rationality of expiration time:
Because for different businesses, the length of the expiration time we set will be different, too long is not appropriate, and too short is also not appropriate;
Therefore, the solution we thought of is to set up a child thread to extend the life of the current lock resource. Specifically, the child thread queries whether the key has expired every 2-3 seconds, and if it has not expired, it means that the business thread is still executing the business, so the expiration time of the key is extended by 5 seconds.
However, in order to avoid the situation where the child thread continues to extend the life of the main thread after the main thread unexpectedly dies, causing the phenomenon of 'immortal lock', the child thread is changed to a守护线程 of the main (business) thread, so that the child thread will die along with the main thread.
// Life extension child thread
public class GuardThread extends Thread {
private static boolean flag = true;
public GuardThread(String lockKey,
int timeOut, StringRedisTemplate stringRedisTemplate){
……
}
@Override
public void run() {
// Start the loop to renew
while (flag){
try {
// Sleep for half the time first
Thread.sleep(timeOut / 2 * 1000);
}
e.printStackTrace();
}
// Go to renew after half the time has passed
// First check if the key has expired
Long expire = stringRedisTemplate.getExpire(
lockKey, TimeUnit.SECONDS);
// If it has expired, it means the main thread has released the lock
if (expire <= 0){
// Stop the loop
flag = false;
}
// If it has not expired
// Then renew it for half the time
stringRedisTemplate.expire(lockKey,expire
+ timeOut/2,TimeUnit.SECONDS);
}
}
}
// Create a child thread to keep the lock alive
GuardThread guardThread = new GuardThread(lockKey,timeOut,stringRedisTemplate);
// Set as the daemon thread of the current business thread
guardThread.setDaemon(true);
guardThread.start();
Author: Bamboo loves panda
Link: https://juejin.cn/post/7038473714970656775
The issue of lock expiration under Redis master-slave architecture
To ensure high availability of Redis during the development process, a master-slave replication architecture is adopted for read-write separation, thereby improving the throughput and availability of Redis. However, if a thread successfully acquires a lock on the Redis master node, but the master node has not yet had time to replicate it to the slave node before crashing, the other thread accessing Redis will access the slave node and also successfully acquire the lock. In this case, the access to the critical resource will present a security issue.
Solution:
- Redlock Algorithm (official solution): Write data to multiple independent Redis instances at the same time. If more than half of the machines write successfully within the lock timeout period, the lock acquisition is successful and the locks on the successful machines are released if the operation fails. However, the drawback of this approach is that it is expensive and requires the independent deployment of multiple Redis nodes.
- Additional lock status recording: an additional lock status is recorded through other independently deployed middleware (such as DB), and before a new thread acquires the lock, it needs to first query the lock holding record in the DB, and only when the lock status is not held should it try to acquire the distributed lock.HoweverThe disadvantages of this situation are obvious, the process of obtaining the lock is complex, and the performance overhead is also very large; in addition, it is also necessary to cooperate with the timer function to update the lock status in the DB to ensure the reasonable failure mechanism of the lock.
- Implementation using Zookeeper
Zookeeper Implementation of Distributed Lock
The data of Zookeeper is different from that of redis, the data is real-time synchronized, and more than half of the nodes need to write after the master node writes in order to return success. Therefore, if projects such as e-commerce and education pursue high performance, they can give up some stability and recommend using redis to implement; for example, projects such as finance, banking, and government, which pursue high stability, can sacrifice some performance and recommend using Zookeeper to implement.
Distributed Lock Performance Optimization
The locking above indeed solves the problem of thread safety under concurrent conditions, but how should we deal with the scenario where 1 million users are competing to buy 1000 products at the same time?
- It is also possible to preheat the shared resources and store a segmented and dispersed copy. The flash sale time is at 15:00 in the afternoon, and the product quantity is divided into 10 parts around 14:30 in advance, and each piece of data is separately locked to prevent concurrent exceptions.
- In addition, it is also necessary to write 10 keys in redis, and each new thread that enters first randomly allocates a lock, then performs the subsequent inventory reduction logic, releases the lock after completion, so that it can be used by subsequent threads.
- The idea of this distributed lock is that, in order to improve the access speed of multi-threading under instantaneous conditions, it is also necessary to ensure a way of implementation under the condition of concurrent safety for the function of multi-threaded synchronization access to shared resources that can be realized with a single lock.
Reference Articles:
1.https://juejin.cn/post/7236213437800890423
2.https://juejin.cn/post/7038473714970656775
3.https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
Author: JD Technology, Jiao Zebin
Source: JD Cloud Developer Community. Please indicate the source when转载.

评论已关闭