The original article was first published in The book, this article will “Based on AOP and Redis implementation of a simple version of distributed lock” and “based on AOP and Redis implementation of a simple version of distributed lock (ii)” two articles are integrated, modified part of the content.

Concurrency problems are often encountered in projects. In theory, some methods should only be called once, but they are called repeatedly because of concurrency, resulting in system problems. In our case, this is most likely to happen with push messaging services. Whether SMS or APP push, or public account push, because of concurrent problems caused by repeated push, is certainly not allowed.

So I’m going to solve this problem with distributed locking.

Distributed locks generally solve the following two types of problems:

1, efficiency problems, such as repeated texting, repeated generation of the same order, etc.

2, the correctness of the problem, such as a request for a deduction at the same time do not allow other requests to deduct at the same time.

Of course, they write the simplified version because, this time, distributed locking is only intended to solve the problem of efficiency, not correctness.

The reason is based on Redis, because Redis as a basic Internet companies will have to cache database, simple and easy to use, the threshold of entry is low.

AOP based implementations, on the other hand, are less intrusive because they are annotated, with virtually no modifications to the native code, just annotations.

Here is a simple version of the distributed lock implementation:

First we need an annotation, the LockAnnotation for the method

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, Elementtype. TYPE}) public @interface LockAnnotation {/** ** the prefix of the locked key ** @return
     */
    String lockField() default ""; /** * The value of the locked key * @return
     */
    String lockKey() default ""; /** * automatic lock release time, unit: s ** @return*/ int lockTime() default 3; /** * The maximum waiting time to obtain the lock, in s. By default, no waiting is performed. 0 indicates a quick failurereturn
     */
    int waitTime() default 0;
}

Copy the code

LockAnnotation is applied to methods that require distributed locks to indicate that it is a method that requires distributed locks to introduce aspects. LockField is used for prefix recognition of the key of redis. LockKey is the key that is used to specify redis, implemented by SpEL syntax. LockTime is the default expiration time for locks added to prevent deadlocks, and waitTime is designed to allow for lock acquisition failures to wait rather than just fail quickly.

Finally, the implementation of the LockAspect of the section

@Component
@Aspect
public class LockAspect {

    private static Logger logger = LoggerFactory.getLogger(LockAspect.class);

    private static final String REDIS_SET_SUCCESS = "OK";

    @Resource
    private CacheUtils cacheUtils;

    @Around("@annotation(lockAnnotation)")
    public Object lockAround(ProceedingJoinPoint joinPoint, LockAnnotation lockAnnotation) throws Throwable {
        ExpressionParser parser = new SpelExpressionParser();
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        EvaluationContext context = new StandardEvaluationContext();

        Object[] args = joinPoint.getArgs();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        String[] params = discoverer.getParameterNames(signature.getMethod());
        for (int len = 0; len < params.length; len++) {
            context.setVariable(params[len], args[len]);
        }
        Expression expression = parser.parseExpression(lockAnnotation.lockKey());
        String lockKey = expression.getValue(context, String.class);
        int lockTime = lockAnnotation.lockTime() > 1 ? lockAnnotation.lockTime() : 1;
        int waitTime = lockAnnotation.waitTime() > 0 ? lockAnnotation.waitTime() : 0;
        int lockTime = lockAnnotation.lockTime();
        String randomValue = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();
        long endTime = System.currentTimeMillis() + waitTime * 1000;
        try {
            do {
                if (this.getLock(lockField, lockKey, randomValue, lockTime)) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Lock succeeded with method name {} and parameter {}", joinPoint.getSignature(),
                                String.join("-", Lists.newArrayList(args).stream().map(obj -> JSONObject.toJSONString(ObjectUtils.defaultIfNull(obj, "null")))
                                        .collect(Collectors.toList())));
                    }
                    Object returnObject = joinPoint.proceed(args);
                    return returnObject;
                }
                int sleepTime = Math.min(300, waitTime * 100);
                if (logger.isDebugEnabled()) {
                    logger.debug("Unable to acquire lock, wait {}ms, method name {}, parameter {}", sleepTime, joinPoint.getSignature(),
                            String.join("-", Lists.newArrayList(args).stream().map(obj -> JSONObject.toJSONString(ObjectUtils.defaultIfNull(obj, "null")))
                                    .collect(Collectors.toList())));
                }
                Thread.sleep(sleepTime);
            } while (System.currentTimeMillis() <= endTime);
            if (logger.isInfoEnabled()) {
                logger.info("Failed to acquire lock, abandoned wait, previously wait {}ms, method will not execute, method name {}, parameter {}", System.currentTimeMillis() - startTime, joinPoint.getSignature()
                        , String.join("-", Lists.newArrayList(args).stream().map(Object::toString)
                                .collect(Collectors.toList())));
            }
            returnnull; } finally { cacheUtils.delLock(lockField, lockKey, randomValue); }}}Copy the code

The setLock are implemented by Redis set of instructions, including [NX | XX] choose NX, and then set the expiration time. DelLock is implemented through the Eval instruction of Redis to execute the Lua script, where the script code is as follows:

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Copy the code

The above lock acquisition and lock release process has been modified, the original code is not perfect as follows:

try {
	if (cacheUtils.incr(lockField, lockKey) > 1) {
		if (logger.isDebugEnabled()) {
			logger.debug("Unable to obtain lock, method name {}, argument {}", joinPoint.getSignature(), String.join("-", Lists.newArrayList(args).stream().map(Object::toString).collect(Collectors.toList())));
        }
		return null;
    }
	cacheUtils.expireKey(lockField, lockKey, lockTime);
	if (logger.isDebugEnabled()) {
		logger.debug("Lock can be obtained with method name {} and argument {}", joinPoint.getSignature(), String.join("-", Lists.newArrayList(args).stream().map(Object::toString).collect(Collectors.toList())));
	}
	returnObject = joinPoint.proceed(args);
} finally {
	if(cacheUtils.ttl(lockField, lockKey) > 0) { cacheUtils.del(lockField, lockKey); }}Copy the code

There are two main differences:

1. The previous process of creating a lock and setting the lock validity period was carried out in two steps, which caused risks. If the machine goes down after the lock is created, it will be completely deadlocked. This problem is avoided by merging lock creation and lock validity into one step, SetNX.

2. In the previous lock release process, only TTL was judged, but there was no guarantee that I would still be the lock holder when the lock was released. If you release the lock without judgment, you will accidentally delete locks created on other requests. After improvement, by generating a random number, and then release the lock through lua script to execute, first get the value of the lock, and then determine whether the lock released is the original request to create the lock, if so, then release the lock.

if (randomValue.equals(cacheUtils.get(lockField, lockKey))) {
      cacheUtils.del(lockField, lockKey);
}
Copy the code

The reason for using lua scripts instead of the aforementioned get, then del operations is to preserve the atomicity of the operations. Assuming the above approach, get and DEL are executed step by step. If user A is required to perform the DEL operation before, in case the del operation is not performed in time due to other reasons, the lock expiration will be automatically released. Then, request B finds that the lock can be created, and the lock will be created. Request A suddenly recovers to release the lock, but the holder of the lock is Request B. Request A mistakenly deletes the lock held by request B. This problem is complicated because Redis does not have get and del as one operation. The only way to solve this problem is to combine the two operations with lua scripts.

The principle is introduced, then introduce how to use.

LockKey is named as ‘x=’+#x as possible. If the variable is an object and you need to get the value of the object, for example, orderSn in order, you can obtain the value in order.orderSn.

@LockAnnotation(lockField = "lock", lockTime = 10, lockKey = "'orderSn='+#orderSn")
public Integer lock(String action, String orderSn) {
    System.out.println(action);
    return 1;
}
Copy the code

Note that the constant ‘orderSn=’ needs to be in single quotation marks, otherwise it will be treated as an assignment syntax, whereas the value of orderSn after # is as long as it is consistent with the variable name orderSn in the method

If you have better suggestions, welcome to communicate with us.