springboot integrates redisson to realize distributed locks, and realizes the function of deduction of commodity orders and inventory

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");
    }