I’m participating in nuggets Creators Camp # 4, click here to learn more and learn together!

background

Let’s start with a specific business scenario.

Junfeng’s mall is doing a promotion to discount the latest iphone13, but each person is limited to two units, with a total inventory of 200 units.

Since our mall is relatively large, the server is deployed in a cluster. In order to meet the above purchase restriction conditions and avoid capital loss caused by oversold conditions, we must add locks in the business operation of placing orders, and the locks must be distributed. Without further ado, let’s see how I used to add distributed locks.

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private InventoryService inventoryService;

    @Autowired
    private RedisLockService redisLockService;

    // Get the order number, which is a globally unique distributed ID
    private static Long orderSn = 1L;
    public static String generateOrderSn(a) {
        return String.valueOf(orderSn++);
    }

    /** * create order *@param buyerId
     * @param skuId
     * @return* /` ` `public String create(OrderCreateRequest request) {

    Long buyerId = request.getBuyerId();
    Long skuId = request.getSkuId();
    Integer buyCount = request.getBuyCount();
    String orderSn;
    String key = "order:create:" + buyerId + ":" + skuId;
    try {
        boolean lockResult = redisLockService.tryLock(key, 5);
        Assert.isTrue(lockResult, "System busy");

        // Determine whether the purchase limit has been reached
        int count = orderRepository.count(buyerId, skuId);
        if (count + buyCount > 2) {
            throw new RuntimeException("You have reached the purchase limit.");
        }

        // Query the product information
        GoodsSku sku = skuRepository.get(skuId);

        // Insert order
        orderSn = generateOrderSn();
        orderRepository.insert(new Order(orderSn, buyerId, skuId, 0, sku.getAmount()));

        // Deduct inventory
        inventoryService.deduct(sku.getInventoryId(), buyCount);

    } finally {
        redisLockService.unLock(key);
    }

    return orderSn;
}
Copy the code

This is a simplified version of the order logic code, in which the distributed lock is mainly used to control multiple threads to bypass the judgment condition of if (count > 1) at the same time, so that the buyer who has reached the upper limit may buy more goods, such as two requests issued by two clicks quickly, or log in to multiple devices at the same time to order. Or even make two requests at the same time using tools like Postman.

In the above code, you can see that the business logic is wrapped in the try-finally lock. In fact, it is not only necessary to lock the order to control the concurrency, but also all operations after the order, so there will be this section of lock and lock release logic. We could write this for every piece of code, but it would be ugly to see everyone look the same. Is there any way to optimize it?

In fact, we can find that the lock logic itself is relatively fixed, it can extract a fixed template, as shown below:

public void method(Param param) {
      String keySuffix = param.getKeySuffix();
      String key = "keyPrefix:" + keySuffix;
      try {
          boolean lockResult = redisLockService.trylock(key, 5);
          Assert.isTrue(lockResult, "System busy");
            
        // Business logic
            
      } finally{ redisLockService.unLock(key); }}Copy the code

So how do you get all the business code that needs to use distributed locks to follow this logic? It’s easy to think of a way to do this using a section with annotations. The idea is to use dynamic proxies to dynamically generate a subclass to control access to real objects. Let’s take a look at the optimized code.

To optimize the

Custom annotation

Contains the lock prefix, lock suffixes (calculated using Spring EL expressions), and the time that the lock has been held to prevent a thread from releasing the lock for a long time.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLock {

    String keyPrefix(a); // Prefix of the lock key, mandatory

    String key(a) default ""; // Spring expression language for locking keys (SpEl)

    int second(a) default 5;  // Lock usage time, default 5 seconds
}
Copy the code

Add an annotation @redislock to the method that needs to be locked, passing in the argument.

@RedisLock(keySuffix = "#request.buyerId + ':' + #request.skuId", keyPrefix = "order:create:")
public String create(OrderCreateRequest request) {
    Long buyerId = request.getBuyerId();
    Long skuId = request.getSkuId();
    Integer buyCount = request.getBuyCount();
    // Determine whether the purchase limit has been reached
    int count = orderRepository.count(buyerId, skuId);
    if (count + buyCount > 2) {
        throw new RuntimeException("You have reached the purchase limit.");
    }

    // Query the product information
    GoodsSku sku = skuRepository.get(skuId);

    // Insert order
    String orderSn = generateOrderSn();
    orderRepository.insert(new Order(orderSn, buyerId, skuId, 0, sku.getAmount()));

    // Deduct inventory
    inventoryService.deduct(sku.getInventoryId(), buyCount);

    return orderSn;
}
Copy the code

Define the plane

The create method only keeps the business logic, and the locking logic is maintained in the section. Before locking, Spring EL expression is used to calculate the key suffix and splice the key prefix to obtain a complete key. Of course, more splicing methods can be realized by using the function of EL expression. You can even customize the expression interpreter using the interpreter pattern.

@Aspect
public class RedisLockAspect {

    private RedisLockService redisLockService;

    public RedisLockAspect(RedisLockService redisLockService) {
        this.redisLockService = redisLockService;
    }

    @Around("@annotation(com.example.springdemo...... RedisLock)")	// Change the position of your own annotations
    public Object redisLock(ProceedingJoinPoint joinPoint) throws Throwable {
        RedisLock annotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(RedisLock.class);
        String keySuffix;
        if(! StringUtils.isEmpty(annotation.keySuffix())) { keySuffix = getSpelKey(joinPoint, annotation.keySuffix()); }else {
            throw new UnsupportedOperationException("Spel expression is empty, operation prohibited");
        }
        String lockKey = annotation.keyPrefix() + keySuffix;
        try {
            boolean lockResult = redisLockService.tryLock(lockKey, annotation.second());
            if (lockResult) {
                return joinPoint.proceed();
            } else {
                // Failed to lock, throw an exception
                throw new RuntimeException("System busy"); }}finally{ redisLockService.unLock(lockKey); }}private String getSpelKey(ProceedingJoinPoint joinPoint, String spel) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        Object targetObject = joinPoint.getTarget();
        Object[] args = joinPoint.getArgs();

        // Get a list of intercepted method parameter names (using the Spring support library)
        LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = nameDiscoverer.getParameterNames(targetMethod);
        // Use SPEL to parse keys
        ExpressionParser parser = new SpelExpressionParser();
        / / SPEL context
        StandardEvaluationContext context = new MethodBasedEvaluationContext(targetObject, targetMethod, args, nameDiscoverer);
        // Put the method parameters in the SPEL context
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        returnparser.parseExpression(spel).getValue(context, String.class); }}Copy the code

Finally, you can test the code against the normal use of sections, which I won’t go into here.

conclusion

The clever use of aspects to decouple the business code from the framework code greatly improves the simplicity of the code. Not only that, we don’t have to write the same logic everywhere, which reduces the probability of errors and makes the code easier to maintain. Of course, there are many other things you can do with slice, such as caching, log monitoring, and so on. I believe that a lot of abstract code, you will be promoted as soon as possible.