Implement distributed locks based on Spring aop and redisson (set lockName flexibly)

1. What you should already know when reading this article

  1. Spring boot framework is basically used (I use spring cloud distributed framework here)
  2. Basic principles of aop
  3. Understand redisson distributed locking mechanism
  4. Adequate knowledge of reflection and annotation usage

If you don't know enough about the above content, it will be difficult to read this article. (for the first time, you are welcome to correct any inappropriate or better solution.).

2. Implementation effect (simplest function)

    @DistributedLock("testLock")
    public R testWrite(@DistributedLockParam String param) {
        System.out.println(new Date());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getId());
        System.out.println(new Date());
        return R.ok();
    }

         The @ DistributedLock method holds a reisson lock during execution. When I use JMeter (pressure measurement tool, which can be simply understood as sending n requests almost at the same time) to make three requests, the console input contents are as follows

        It can be seen that the three requests are executed orderly and the distributed lock takes effect. I will explain the specific implementation ideas and codes below.

3. Implementation ideas

        The redisson framework's distributed lock is implemented based on redis. By accessing the key value with the same name, you can judge whether other threads are executing the current task. In java code, you can lock before the logical code and unlock after the logical code.

        Spring's aop will automatically trigger the execution of aop when calling the methods in the objects managed by the Bean container. Therefore, we can design an aop to help us complete the above reisson function, so we don't need to manually write the locking and unlocking process.

        The following is a simple redissonDemo. Of course, it lacks many parameters that need to be used in real use.

    @Autowired
    private RedissonClient redissonClient;
    public void lockDemo() {
        String name = "lockName";
        RLock lock = redissonClient.getLock(name);
        try {
            lock.lock();
            //do someThing
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

          We can see that our real business code is contained in a try. There is a section of lock logic before and after the business. Therefore, aop tells us to choose surround notification. At the same time, when creating a lock, we need some parameters. When writing this aop, we need to consider how to pass the parameters to it.

4. aop prototype

        As mentioned above, we need to provide parameters for aop, and then aop will lock and unlock us through these parameters. Here, we choose to set the annotation as the tangent point, because the annotation can meet the requirement of providing parameters. In this way, the prototype of aop is ready to come out.

    @Autowired
    private RedissonClient redissonClient;

    @Pointcut("@annotation(com.xxx.common.aop.distributed.DistributedLock)")
    public void distributedLockAspect() {}

    @Around(value = "distributedLockAspect()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        return doLock(pjp);
    }

5. @ distributedlock (tangent point)

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
    /**
     * The name of the lock.
     * If lockName can be determined, set this property directly.
     */
    String value();

    /**
     * Whether to use an attempt lock.
     */
    boolean tryLock() default false;
    /**
     * Maximum waiting time.
     * This field is valid only when tryLock() returns true.
     */
    long waitTime() default 30L;
    /**
     * Lock timeout.
     * If tryLock is false and leaseTime is set to 0 or less, it will become lock()
     */
    long leaseTime() default 5L;
    /**
     * Time unit. The default is seconds.
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

        @ Target ({ElementType. Method}) means that the current annotation is used on the method  

        @ Retention(RetentionPolicy.RUNTIME) means that the current annotation is valid at runtime

        The annotation contains some attributes commonly used in locking. If you read these attributes from aop, you can also perform the locking and unlocking process. However, in fact, we often need to set the lock granularity when setting the lock   of

        Explain the problem of lock granularity. We know that locks are used to synchronize some functions of asynchronous processing to prevent thread safety problems. However, when this annotation is implemented, there is only one value. This value is fixed for a method, but there will be problems. For example, you have to check out (method) after shopping in the supermarket, As a result, although there are many checkout counters in the supermarket, only one person is allowed to check out, and others are stopped outside, because the first person directly brings (locks) the door when he goes in to check out, and he locks the whole method, resulting in other people's inability to complete the check-out. At this time, our correct solution is to let all checkout counters receive a customer, That is, our checkout method needs to be able to build independent locks for each checkout table (in fact, it is the difference between row level locks and table level locks).

        Our method is the same, but we need to make it generate different locks in different situations. At this time, the first thing we think of should be the method parameters. We can consider using different method parameters to determine the lock form. Thus, a second annotation appears.

6. @DistributedLockParam

        

@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLockParam {
    /**
     * If it is an object, fill in which attribute to use. If it is a basic data type or String, press the default
     * @return
     */
    String value() default "";

    /**
     * The current attribute is in the order of the lock directory. The smaller the value, the higher the value
     * **Not heavy
     * @return
     */
    int sort() default 0;
}

        The function of this annotation is to write on the method parameters. When we need to set the lock granularity, fill in this annotation on the corresponding parameters, and then read the value of the parameter containing this annotation in aop and spell it in the real lock name.

        Since granularity may consist of more than one element, the sort attribute is added to the annotation to sort the granularity parameters.

        Since the parameters of a method may be either a basic type or an object, in addition to marking the basic type, our granularity marking may also be used to mark an attribute in the object. Even if the attribute may also be an object, we need the attribute of the object attribute

        Well, keep this in mind until we get to aop logic.

         As for the value in value, in the setting, the toString of the target is directly used by default. If you want to use a property of the object, modify the value to the corresponding property name. If it is a property of the property, it is represented by ".".

         Of course, since we have supported multiple granularity tags, we should allow you to select multiple attributes of an object as granularity. There are two ways to implement this function. The first is to segment the value value. For example, "a;b" means to use attribute a as the first tag and attribute b as the second tag.

        But what if I want to splice a property of another object between these two? (no, just in case! guna!), in addition, our value has allowed to use "." as a deep query mark. Is it too messy to add a semicolon.

        So we abandoned this implementation and replaced it with another, reuse annotations.

        I won't say how to reuse annotations. This is a fixed process, and only the code is posted here.

        You need to add a new annotation @ Repeatable in @ DistributedLockParam.

@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(DistributedLockParams.class)
public @interface DistributedLockParam {
    /**
     * If it is an object, fill in which attribute to use. If it is a basic data type or String, press the default
     * @return
     */
    String value() default "";

    /**
     * The current attribute is in the order of the lock directory. The smaller the value, the higher the value
     * **Not heavy
     * @return
     */
    int sort() default 0;
}

        Then you need an additional annotation as its container.

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ ElementType.PARAMETER })
public @interface DistributedLockParams {
    DistributedLockParam[] value();
}

        So far, all the three annotations we used have been completed.

        Then, sit still and we're going to speed up.

7. aop logic

        In the previous aop prototype, our surround notification called the doLock method.

private Object doLock(ProceedingJoinPoint pjp) throws Throwable {
        //The class where the tangent point is located
        Class targetClass = pjp.getTarget().getClass();
        //Annotation method is used
        String methodName = pjp.getSignature().getName();
        Class[] parameterTypes = ((MethodSignature)pjp.getSignature()).getMethod().getParameterTypes();
        Method method = targetClass.getMethod(methodName, parameterTypes);
        Object[] arguments = pjp.getArgs();
        // Get the desired lock name according to the reflection method
        String lockName = getLockName(method, arguments);
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        // Generate lock
        RLock lock = lock(lockName, distributedLock);
        try {
            return pjp.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

        Among them, the first step we need to do is to obtain the methods and parameters to the connection point, which is the storage location of the parameters of our reisson lock. This part of the code is relatively fixed.

        After obtaining these two elements, we can splice the name of the lock with the getLockName method.

        

private String getLockName(Method method, Object[] arguments) {
        // Get annotation
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        // Get prefix
        StringBuilder lockName = new StringBuilder(distributedLock.value());
        // Used to store lock granularity tags
        TreeMap<Integer, String> treeMap = new TreeMap<>();
        // Traverse parameters to find granularity markers
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            // parameterAnnotations[i]: annotation array of the ith parameter
            for (Annotation annotation : parameterAnnotations[i]) {
                // Traverse the required annotations (if only one is DistributedLockParam, if there are multiple on a parameter, it will be automatically combined into DistributedLockParams)
                if (annotation instanceof DistributedLockParam) {
                    // Get annotation
                    DistributedLockParam distributedLockParam = (DistributedLockParam) annotation;
                    // Put attributes in treemap
                    fillTreeMap(distributedLockParam, arguments[i], treeMap);
                }else if (annotation instanceof DistributedLockParams) {
                    // Get annotation
                    DistributedLockParams distributedLockParams = (DistributedLockParams) annotation;
                    // Put attributes in treemap
                    for (DistributedLockParam distributedLockParam : distributedLockParams.value()) {
                        fillTreeMap(distributedLockParam, arguments[i], treeMap);
                    }
                    break;
                }
            }
        }
        // After collection, splice lockName
        separate(lockName, treeMap);
        return lockName.toString();
    }

    private void fillTreeMap(DistributedLockParam distributedLockParam, Object argument, TreeMap<Integer, String> treeMap) {
        // Get property name
        String field = distributedLockParam.value();
        if (field.equals("")) {
            // Basic attributes are used directly
            field = argument.toString();
        } else {
            // Object reflection data
            try {
                String[] values = field.split("\\.");
                for (int i = 0; i < values.length; i++) {
                    Field declaredField = argument.getClass().getDeclaredField(values[i]);
                    declaredField.setAccessible(true);
                    if (i == values.length - 1) {
                        // The last one is a real object from which the attributes are extracted
                        field = declaredField.get(argument).toString();
                        // If you don't jump out here, an error will be reported in the execution of the next sentence
                        break;
                    }
                    // Switch to subordinate objects
                    argument = declaredField.get(argument);
                }

            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new RRException("Error in distributed lock parameters");
            }
        }
        // After confirmation, put it into treeMap and sort it automatically
        treeMap.put(distributedLockParam.sort(), field);
    }

    private void separate(StringBuilder lockName, TreeMap<Integer, String> treeMap) {
        treeMap.values().forEach(s -> lockName.append(":").append(s));
    }

         First, get the @ DistributedLock annotation from the method and get the value in it, which is the prefix of our lock name. If there is no subsequent splicing operation, it is our lock name.

        The second step is to get all the granularity tags. Because of the sorting function, we introduce treeMap to sort the tags we find.

        

Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            // parameterAnnotations[i]: annotation array of the ith parameter
            for (Annotation annotation : parameterAnnotations[i]) {
                // Traverse the required annotations (if only one is DistributedLockParam, if there are multiple on a parameter, it will be automatically combined into DistributedLockParams)
                if (annotation instanceof DistributedLockParam) {
                    // Get annotation
                    DistributedLockParam distributedLockParam = (DistributedLockParam) annotation;
                    // Put attributes in treemap
                    fillTreeMap(distributedLockParam, arguments[i], treeMap);
                }else if (annotation instanceof DistributedLockParams) {
                    // Get annotation
                    DistributedLockParams distributedLockParams = (DistributedLockParams) annotation;
                    // Put attributes in treemap
                    for (DistributedLockParam distributedLockParam : distributedLockParams.value()) {
                        fillTreeMap(distributedLockParam, arguments[i], treeMap);
                    }
                    break;
                }
            }
        }

        This code is the whole process of getting tags. First, we will get the annotation array of the parameter list, which is a two-dimensional array. The two indexes represent the index of the parameter and the index of the annotation respectively.

        We need to traverse each annotation in this array to determine whether it is the @ DistributedLockParam and @ DistributedLockParams we need (when our annotation is not reused, the annotation is @ DistributedLockParam, but when we reuse this annotation, we get @ DistributedLockParams, which contains all @ DistributedLockParams).

        After obtaining the annotation, extract the data in it through the fillTreeMap() method. For value, the toString of the target will be directly extracted by default. If we handwrite value

            // Object reflection data
            try {
                String[] values = field.split("\\.");
                for (int i = 0; i < values.length; i++) {
                    Field declaredField = argument.getClass().getDeclaredField(values[i]);
                    declaredField.setAccessible(true);
                    if (i == values.length - 1) {
                        // The last one is a real object from which the attributes are extracted
                        field = declaredField.get(argument).toString();
                        // If you don't jump out here, an error will be reported in the execution of the next sentence
                        break;
                    }
                    // Switch to subordinate objects
                    argument = declaredField.get(argument);
                }

        for recursion's Good Friday is coming.

        First, we segment value according to the agreed rules to get the name of each level of attribute, and then we need to find this attribute at the reflection level.

        At the beginning, argument is our parameter itself. We can obtain the current attribute we need through argument.getClass().getDeclaredField(values[i]). If we have explored the last level at this time, we will directly store this attribute in the treeMap and jump out (recursive header). On the contrary, we need to explore again (recursive body) based on this attribute , during recursion, our argument actually refers to the currently traversed object, so before entering the next recursion, we first need to point the argument to our subordinate attribute object.

        After the above logic is executed, we get a treeMap, which contains all our tags and has been sorted according to sort.

        Then do a simple splicing.

private void separate(StringBuilder lockName, TreeMap<Integer, String> treeMap) {
        treeMap.values().forEach(s -> lockName.append(":").append(s));
    }

        ":" is used for splicing because the colon is displayed in the directory in redis, which is similar to our "/".

        The later operation is very simple.

RLock lock = lock(lockName, distributedLock);
        try {
            return pjp.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
private RLock lock(String lockName, DistributedLock distributedLock) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockName);
        // Lock
        if (distributedLock.tryLock()) {
                lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());
        } else {
            long leaseTime = distributedLock.leaseTime();
            if (leaseTime > 0) {
                lock.lock(distributedLock.leaseTime(), distributedLock.timeUnit());
            } else {
                lock.lock();
            }
        }
        return lock;
    }

        Extract other lock parameters in @ DistributedLock, then build our lock and perform locking, try to return the result, and finally unlock at one go.

8. Some lock failures

        In this lock, lock failure means aop failure, so the problem can become under what circumstances aop will fail.

        This involves the principle of aop. I only briefly describe it. If you are interested, you can search it yourself.

        aop is based on proxy mode. Spring's aop will be called when we   The method in the object managed by the Bean container is triggered automatically, so it cannot take effect in two cases.

  1. The proxy object cannot access the method: when our method is modified by final or private, the proxy cannot be implemented.
  2. When we do not call through the Bean container (not obtained from the context and not injected): the most common case is that non annotated methods directly call the annotation method of this class. At this time, the annotation of the annotation method will not take effect.

9. Complete code

// spring related dependencies will not be posted
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>
import com.xxx.common.aop.distributed.DistributedLock;
import com.xxx.common.aop.distributed.DistributedLockParam;
import com.xxx.common.aop.distributed.DistributedLockParams;
import com.xxx.common.exception.RRException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.TreeMap;

@Aspect
@Component
public class DistributedLockAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Pointcut("@annotation(com.xxx.common.aop.distributed.DistributedLock)")
    public void distributedLockAspect() {}

    @Around(value = "distributedLockAspect()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        return doLock(pjp);
    }

    private Object doLock(ProceedingJoinPoint pjp) throws Throwable {
        //The class where the tangent point is located
        Class targetClass = pjp.getTarget().getClass();
        //Annotation method is used
        String methodName = pjp.getSignature().getName();
        Class[] parameterTypes = ((MethodSignature)pjp.getSignature()).getMethod().getParameterTypes();
        Method method = targetClass.getMethod(methodName, parameterTypes);
        Object[] arguments = pjp.getArgs();
        // Get the desired lock name according to the reflection method
        String lockName = getLockName(method, arguments);
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        // Generate lock
        RLock lock = lock(lockName, distributedLock);
        try {
            return pjp.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private RLock lock(String lockName, DistributedLock distributedLock) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockName);
        // Lock
        if (distributedLock.tryLock()) {
                lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());
        } else {
            long leaseTime = distributedLock.leaseTime();
            if (leaseTime > 0) {
                lock.lock(distributedLock.leaseTime(), distributedLock.timeUnit());
            } else {
                lock.lock();
            }
        }
        return lock;
    }

    private String getLockName(Method method, Object[] arguments) {
        // Get annotation
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        // Get prefix
        StringBuilder lockName = new StringBuilder(distributedLock.value());
        // Used to store lock granularity tags
        TreeMap<Integer, String> treeMap = new TreeMap<>();
        // Traverse parameters to find granularity markers
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            // parameterAnnotations[i]: annotation array of the ith parameter
            for (Annotation annotation : parameterAnnotations[i]) {
                // Traverse the required annotations (if only one is DistributedLockParam, if there are multiple on a parameter, it will be automatically combined into DistributedLockParams)
                if (annotation instanceof DistributedLockParam) {
                    // Get annotation
                    DistributedLockParam distributedLockParam = (DistributedLockParam) annotation;
                    // Put attributes in treemap
                    fillTreeMap(distributedLockParam, arguments[i], treeMap);
                }else if (annotation instanceof DistributedLockParams) {
                    // Get annotation
                    DistributedLockParams distributedLockParams = (DistributedLockParams) annotation;
                    // Put attributes in treemap
                    for (DistributedLockParam distributedLockParam : distributedLockParams.value()) {
                        fillTreeMap(distributedLockParam, arguments[i], treeMap);
                    }
                    break;
                }
            }
        }
        // After collection, splice lockName
        separate(lockName, treeMap);
        return lockName.toString();
    }

    private void fillTreeMap(DistributedLockParam distributedLockParam, Object argument, TreeMap<Integer, String> treeMap) {
        // Get property name
        String field = distributedLockParam.value();
        if (field.equals("")) {
            // Basic attributes are used directly
            field = argument.toString();
        } else {
            // Object reflection data
            try {
                String[] values = field.split("\\.");
                for (int i = 0; i < values.length; i++) {
                    Field declaredField = argument.getClass().getDeclaredField(values[i]);
                    declaredField.setAccessible(true);
                    if (i == values.length - 1) {
                        // The last one is a real object from which the attributes are extracted
                        field = declaredField.get(argument).toString();
                        // If you don't jump out here, an error will be reported in the execution of the next sentence
                        break;
                    }
                    // Switch to subordinate objects
                    argument = declaredField.get(argument);
                }

            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new RRException("Error in distributed lock parameters");
            }
        }
        // After confirmation, put it into treeMap and sort it automatically
        treeMap.put(distributedLockParam.sort(), field);
    }

    private void separate(StringBuilder lockName, TreeMap<Integer, String> treeMap) {
        treeMap.values().forEach(s -> lockName.append(":").append(s));
    }

    @AfterThrowing(value = "distributedLockAspect()", throwing="ex")
    public void afterThrowing(Throwable ex) {
        throw new RuntimeException(ex);
    }

}
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
    /**
     * The name of the lock.
     * If lockName can be determined, set this property directly.
     */
    String value();

    /**
     * Whether to use an attempt lock.
     */
    boolean tryLock() default false;
    /**
     * Maximum waiting time.
     * This field is valid only when tryLock() returns true.
     */
    long waitTime() default 30L;
    /**
     * Lock timeout.
     * If tryLock is false and leaseTime is set to 0 or less, it will become lock()
     */
    long leaseTime() default 5L;
    /**
     * Time unit. The default is seconds.
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}
import java.lang.annotation.*;

@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(DistributedLockParams.class)
public @interface DistributedLockParam {
    /**
     * If it is an object, fill in which attribute to use. If it is a basic data type or String, press the default
     * @return
     */
    String value() default "";

    /**
     * The current attribute is in the order of the lock directory. The smaller the value, the higher the value
     * **Not heavy
     * @return
     */
    int sort() default 0;
}
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ ElementType.PARAMETER })
public @interface DistributedLockParams {
    DistributedLockParam[] value();
}

Tags: Java Spring

Posted on Mon, 22 Nov 2021 02:27:29 -0500 by TMX