Redis implements concurrent blocking lock scheme
Because users access the online order interface at the same time, an exception occurs when deducting inventory. This is a typical concurrency problem. This article was born to solve the concurrency problem. The technology used is Redis lock mechanism + multi-threading The blocking wake-up method.
Before implementing the Redis lock mechanism, we need to understand the prerequisite knowledge.
1. Preliminary knowledge
1. Multithreading
It is slightly inappropriate to classify wait() and notifyAll() as multi-threaded methods. These two methods are methods in Object.
① When the wait() method is called, let the current thread enter the waiting state, and let the current thread release the object lock, wait for the blocking state, and wait for the wake-up of the notifyAll() method.
The wait() method and the sleep() method have some similarities, both block the current thread, but they actually have some differences.
The lock needs to be requested before executing the wait() method, the lock will be released when the wait() method is executed, and the lock will be competed when waiting for wake-up.
sleep() just puts the current thread to sleep for a period of time, ignoring the existence of the lock.
wait() is a method of Object class sleep() is a static method of Thread
② notifyAll() method is to wake up the thread in wait().
Both notifyAll() and notify() methods can wake up the blocked thread that calls the wait() method.
But notify() is to randomly wake up a random thread in this blocking queue, and notifyAll() is to wake up the thread that has been blocked by calling the wait() method, and let them preempt the object lock by themselves.
notifyAll() and notify() must also be called in the locked synchronization code block. They play the role of waking up, not releasing the lock. They are only used when the program in the current synchronization code block is executed. When the object lock is naturally released, the notifyAll() and notify() methods will work to wake up the thread.
The wait() method is generally used in conjunction with the notify() or notifyAll() methods.
2. Redis
The process of locking is essentially to set the value in Redis. When other processes also come to set the value and find that there is already a value in it, they can only give up the acquisition and try again later.
Redis provides a natural way to implement locking mechanisms.
在The command for the Redis client is setnx(set if not exists)
The method used in integrating Springboot is:
redisTemplate.opsForValue().setIfAbsent(key, value);
If the set value inside is successful, it will return True, and if there is a value in it, it will return False.
When we actually use it, the setIfAbsent() method does not always return True and False.
If a transaction is added to our business, the method will return null. I don't know if this is a bug or what. This is a huge pit of Redis. It took a long time to find this problem. If you solve this problem, you can jump to chapter four.
Second, the realization principle
In essence, the goal of distributed locks is to occupy a position in Redis. When other processes want to occupy it, they find that someone has already occupied it, so they have to give up or try again later. Occupancy is generally using the setnx (set if not exists) command, which is only allowed to be occupied by one client. Come first, occupy first, and then call the del command to release the hut after the work is done.
Among them, it is found that there is already a value in Redis. Whether the current thread directly gives up or tries again later represents, respectively, non-blocking lock and blocking lock.
In our business scenario, we must try again later (blocking lock). If we give up directly (non-blocking lock), we can do it directly at the database level, and we don't need to spend a lot of time in the code.
Non-blocking locks can only save the correctness of data. In the case of high concurrency, a large number of exceptions will be thrown. When a hundred concurrent requests arrive, only one request is successful, and the others will throw exceptions.
Redis non-blocking locks and MySQL's optimistic locks have the same final effect. Optimistic locks use the idea of CAS.
Optimistic locking method: add a version number to the table field, or other fields! Add the version number to know the control order! When updating, you can add version= oldVersion after where. Database, in any case of concurrency, update success is 1, failure is 0. You can do corresponding processing according to the returned 1 and 0!
We recommend that you use blocking locks.
When the lock cannot be obtained, we let the current thread wake up using the wait() method, and when the thread holding the lock is finished, call notifyAll() to wake up all waiting methods.
Third, the specific implementation
The following code is the implementation of blocking lock.
Business Layer:
public String test() throws InterruptedException { lock("lockKey"); System.out.println("11"); System.out.println("22"); System.out.println(Thread.currentThread().getName()+"***********"); Thread.sleep(2000); System.out.println("33"); System.out.println("44"); System.out.println("55"); unlock("lockKey"); return "String"; }
Lock tool class:
There are mainly two methods of locking and unlocking.
//Each redis key corresponds to a blocking object private static HashMap<String, Object> blockers = new HashMap<>(); //The thread that currently acquires the lock private static Thread curThread; public static RedisTemplate redisTemplate = (RedisTemplate) SpringUtils.getBean("redisTemplate") ; /** * lock * @param key * @throws InterruptedException */ public static void lock(String key) { //The loop judges whether the key can be created, if not, wait directly to release the CPU execution right //If you can't put the finger, it means that the lock is being occupied System.out.println(key+"**"); while (!RedisUtil.setLock(key,"1",3)){ synchronized (key) { blockers.put(key, key); //wait releases CPU execution rights try { key.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } blockers.put(key, key); / / Can be successfully created, the lock acquisition successfully records the current acquisition lock thread curThread = Thread.currentThread(); } /** * Unlock * @param key */ public static void unlock(String key) { //Determine whether the unlocking is performed by the locked thread, if not, ignore it directly if( curThread == Thread.currentThread()) { RedisUtil.delete(key); //After deleting the key, it is necessary to notifyAll all applications, so here we use to send subscription messages to all applications // RedisUtil.publish("lock", key); //notifllall other threads Object lock = blockers.get(key); if(lock != null) { synchronized (lock) { lock.notifyAll(); } } } }
When we use the interface test tool to test without locking, 12345 cannot all be executed sequentially, which will cause inconsistent output order. If it is in our actual scenario, it is the select and update that the input is replaced by the database. , it is normal for the data to be corrupted.
When we added the lock, 12345 were output sequentially, and the concurrency problem was solved smoothly.
4. Appendix
1. Bugs in Redis
Originally, the lock() method is to directly call the "Redis.setIfAbsent()" method, but it keeps reporting a null pointer exception when using it. The final positioning problem is that there is a problem with the Redis.setIfAbsent() method.
In my actual business, the method of placing an order uses @Transflastion to increase the transaction, which causes the method to return null. We write a function to implement setIfAbsent().
/** * Set the value only if the key does not exist, return true, otherwise return false * * @param key key cannot be null * @param value value cannot be null * @param timeout expiration time, the unit is wonderful * @return */ public static Boolean setLock(String key,String value, long timeout) { SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() { List<Object> exec = null; @Override @SuppressWarnings("unchecked") public Boolean execute(RedisOperations operations) throws DataAccessException { operations.multi(); redisTemplate.opsForValue().setIfAbsent(key, value); redisTemplate.expire(key,timeout, TimeUnit.SECONDS); exec = operations.exec(); if(exec.size() > 0) { return (Boolean) exec.get(0); } return false; } }; return (Boolean) redisTemplate.execute(sessionCallback); }
For the convenience of comparison, the original setIfAbsent() method is pasted below.
/** * Set the value only when the key does not exist, return true, otherwise return false [Warning: An error will be reported in the case of a transaction or pipeline - use the setLock method] * * @param key key cannot be null * @param value value cannot be null * @param timeout expiration time, the unit is wonderful * @return */ @Deprecated public static <T> Boolean setIfAbsent(String key, T value, long timeout) { // redisTemplate.multi(); ValueOperations<String, T> valueOperations = redisTemplate.opsForValue(); Boolean aBoolean = valueOperations.setIfAbsent(key, value, timeout, TimeUnit.SECONDS); // redisTemplate.exec(); return aBoolean; }
2, MySQL lock mechanism
In a concurrent scenario, MySQL will report an error, and the error message is as follows:
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction ; SQL []; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
The reason for the problem is that a certain table is frequently locked, causing another transaction to time out. The reason for the problem is the mechanism of MySQL.
When MySQL updates, if there is an index in the where field, a row lock will be used, otherwise a table lock will be used.
We used navichat to add an index to the where field, and the problem was solved smoothly.
0 Comments