1. Usage scenarios
Some scenarios in distributed services (such as scheduled tasks and inventory updates) need to consider the possible problems caused by concurrent execution.
If you do not use professional distributed timing tools (such as quartz), but simply rely on timers to do timing tasks, then there will be concurrent execution, which may cause some problems.
For scenarios such as inventory updates, we need to consider updating the same record and record possible problems, such as record confusion caused by uncommitted reading. In order to solve these problems, we can introduce distributed transaction locks. This lock can ensure the correctness and consistency of operations in a distributed system.
2. Problems that may be encountered during the implementation of distributed locks
1. An abnormality in the program causes the lock to not be released, resulting in a deadlock
2. The expiration time of the lock is up, but the business has not been processed, causing other threads to get the lock, that is, the distributed lock cannot be automatically renewed
In order to solve the above problems, it is recommended to use a relatively complete and excellent distributed lock:
- Realize the principle of distributed lock based on Redisson (Redission is an independent redis client, which exists at the same level as Jedis and Lettuce)
Redisson’s locking mechanism is shown in the following figure:
The lock resource is acquired by multiple threads, and if successful, the lua script is executed and the data is saved to the redis database.
If the acquisition fails: always try to acquire the lock through the while loop (the waiting time can be customized, and failure will be returned after timeout).
The distributed lock provided by Redisson supports automatic lock renewal. That is to say, if the lock expiration time is up, but the business has not been processed, then redisson will automatically extend the timeout period for the target key in redis, which is called in Redisson It is Watch Dog watchdog mechanism.
3. Code implementation
1. Dependencies and configuration files
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.15.0</version> </dependency>
Add in configuration file
spring: redis: redisson: file: classpath:redisson.yaml
Then create a new redisson.yaml file and put it in the resource directory
singleServerConfig: idleConnectionTimeout: 10000 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500 password: 123456 subscriptionsPerConnection: 5 clientName: null address: "redis://localhost:6379" subscriptionConnectionMinimumIdleSize: 1 subscriptionConnectionPoolSize: 50 connectionMinimumIdleSize: 32 connectionPoolSize: 64 database: 1 dnsMonitoringInterval: 5000 threads: 0 nettyThreads: 0 codec: !<org.redisson.codec.JsonJacksonCodec> {} transportMode: "NIO"
2. Redis tool class
(1) Redisson operation class, this case only uses tryLock() and unLock() methods, other methods can be flexibly selected according to the business
package com.sync.task.core.redislock; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; /** * redisson operation class */ @Component public class RedisDistributedLocker { @Autowired private RedissonClient redissonClient; public RLock lock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); lock. lock(); return lock; } public RLock lock(String lockKey, int leaseTime) { RLock lock = redissonClient.getLock(lockKey); lock. lock(leaseTime, TimeUnit. SECONDS); return lock; } public RLock lock(String lockKey, TimeUnit unit ,int timeout) { RLock lock = redissonClient.getLock(lockKey); lock. lock(timeout, unit); return lock; } public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) { RLock lock = redissonClient.getLock(lockKey); try { return lock. tryLock(waitTime, leaseTime, unit); } catch (InterruptedException e) { return false; } } public boolean tryLock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); return lock. tryLock(); } public void unlock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); // Check if it is the lock you added, release it if it is your own lock if (lock. isHeldByCurrentThread()) { lock. unlock(); } } public void unlock(RLock lock) { lock. unlock(); } }
(2) Distributed lock tools
package com.sync.task.core.redislock; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.concurrent.TimeUnit; /** * redis distributed lock tool class */ @Component public class RedisLockUtils { @Autowired private RedisDistributedLocker locker; private static RedisDistributedLocker distributedLocker; @PostConstruct private void init() { distributedLocker = locker; } /** * lock * @param lockKey * @return */ public static RLock lock(String lockKey) { return distributedLocker. lock(lockKey); } /** * release lock * @param lockKey */ public static void unlock(String lockKey) { distributedLocker. unlock(lockKey); } /** * release lock * @param lock */ public static void unlock(RLock lock) { distributedLocker. unlock(lock); } /** * Lock with timeout * @param lockKey * @param timeout timeout time unit: second */ public static RLock lock(String lockKey, int timeout) { return distributedLocker. lock(lockKey, timeout); } /** * Lock with timeout * @param lockKey * @param unit time unit * @param timeout timeout time */ public static RLock lock(String lockKey, int timeout, TimeUnit unit ) { return distributedLocker. lock(lockKey, unit, timeout); } /** * Try to acquire the lock * @param lockKey * @param waitTime the maximum waiting time * @param leaseTime Automatically release the lock time after locking * @return */ public static boolean tryLock(String lockKey, int waitTime, int leaseTime) { return distributedLocker. tryLock(lockKey, TimeUnit. SECONDS, waitTime, leaseTime); } /** * Try to acquire the lock * @param lockKey * @param unit time unit * @param waitTime the maximum waiting time * @param leaseTime Automatically release the lock time after locking * @return */ public static boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) { return distributedLocker. tryLock(lockKey, unit, waitTime, leaseTime); } /** * Try to acquire the lock, do not set the waiting and expiration time, the watchdog can really take effect, automatic renewal * @param lockKey * @return */ public static boolean tryLock(String lockKey) { return distributedLocker. tryLock(lockKey); } }
3. Business implementation class
Saving product information to Redis will not be repeated here. If you need to know more, see my other article:
https://mp.csdn.net/mp_blog/creation/editor/130848593
The following is the product information saved to Redis:
Submit a product order:
/** * Submit Order * @param productId product id * @param count purchase quantity * @return */ @Override @Transactional(rollbackFor = Exception. class) public Result<String> submitOrder(String productId, long count) { String productKey = PRODUCT + productId; // PRODUCT_N001 String stockKey = PRODUCT_STOCK + productId; // PRODUCT_STOCK_N001 String stockLockKey = PRODUCT_STOCK_LOCK + productId; // PRODUCT_STOCK_LOCK_N001 // Acquire the inventory lock, it will not automatically renew, wait for up to 3 seconds, if it exceeds, give up acquiring the lock, and the lock will be released automatically after 20 seconds // boolean res = RedisLockUtils. tryLock(stockLockKey, TimeUnit. SECONDS, 3, 20); // Obtain inventory lock, auto-renew boolean res = RedisLockUtils. tryLock(stockLockKey); if (!res) { // Exceeded the waiting time, give up acquiring the lock log.info("Thread{} failed to acquire lock", stockLockKey); return ResponseData.error("The system is busy, please try again later"); } log.info("Thread {} acquired the lock successfully", stockLockKey); try { // Query inventory Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey); // There is no product inventory in the cache, query the database if (stock == null) { Product product = productMapper. getById(productId); if (Objects. isNull(product)) { return ResponseData.error("The order submission failed, the product has been removed"); } redisTemplate.opsForValue().set(productKey, JSON.toJSONString(product)); // The inventory information of the database should be obtained according to the actual business. If the product has specification information, the inventory should be obtained according to the specification stock = product. getStack(); redisTemplate.opsForValue().set(stockKey, stock); return ResponseData.error("Order submission failed, insufficient inventory"); } // check if stock is sufficient if (stock < count) { return ResponseData.error("Order submission failed, insufficient inventory"); } // update the inventory data in the database int row = productMapper. updateStock(productId, stock - count); if (row > 0) { // Update the product inventory data cached in Redis redisTemplate.opsForValue().decrement(stockKey, count); } // Inventory deducted, create an order Order order = Order. builder() .id(IdUtil.simpleUUID()).count(count).productId(productId).status(1) .build(); String orderId = orderService. createOrder(order); return ResponseData.success(orderId, "order created successfully"); } catch (OptimisticLockingFailureException e) { // If an optimistic lock exception occurs, release the Redis lock and return an error message log.error("Failed to update inventory, product {} may have been updated", productId); } catch (Exception e) { log.error("The system has deserted~"); } finally { RedisLockUtils.unlock(stockLockKey); log.info("thread{} release lock", stockLockKey); } return ResponseData.error("The system is busy, please try again later"); }