The principle and method of distributed locking based on redis

  • 2020-10-31 22:03:01
  • OfStack

preface

With the continuous expansion of the system, distributed lock is the most basic guarantee. Unlike single-machine multithreading, distributed across multiple machines. A Shared variable for a thread cannot cross a machine.

java concurrency processing provides ReentrantLock or Synchronized for mutex control in order to ensure that one can only be operated by the same thread in a high-concurrency scenario. But this only works in stand-alone environments. We implement distributed locks in about three ways.

redis implements distributed locking The database implements distributed locking zk implements distributed locking

Today we introduce distributed locking through redis. In fact, these three types belong to the first category compared with java. Are external locks belonging to the program.

The principle of analyzing

Each of the three distributed locks above controls release or rejection by locking and unlocking each request on its own basis. The redis lock is based on the setnx command it provides. setnx if and only if key does not exist. If a given key already exists, setnx does nothing. setnx is an atomic operation. Compared to database distribution, because redis memory is light. So redis distributed locks perform better

implementation

The principle is simple. Combined with THE springboot project, we realized a set of cases to understand the interface inventory locking through annotation form

Write a note

We write annotations. Allow us to add annotations to the interface to provide interception information


/**
 * @author  Zhang xinghua 
 * @version V1.0
 * @Package com.ay.framework.order.redis.product
 * @date 2020 years 03 month 26 day , 0026 10:29
 * @Copyright © 2020  An Yuan Technology Co., LTD 
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface StockLock {

  /**
   * @author zxhtom
   * @Description  The lock key The prefix 
   * @Date 15:25 2020 years 03 month 25 day , 0025
   * @Param []
   * @return java.lang.String
   */
  String prefix() default "";
  /**
   * @author zxhtom
   * @Description key The delimiter 
   * @Date 15:27 2020 years 03 month 25 day , 0025
   * @Param []
   * @return java.lang.String
   */
  String delimiter() default ":";
}

/**
 * @author  Zhang xinghua 
 * @version V1.0
 * @Package com.ay.framework.order.redis.product
 * @date 2020 years 03 month 26 day , 0026 11:09
 * @Copyright © 2020  An Yuan Technology Co., LTD 
 */
@Target({ElementType.PARAMETER , ElementType.METHOD , ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface StockParam {
  /**
  * @author zxhtom
  * @Description  composition key
  * @Date 11:11 2020 years 03 month 26 day , 0026
  * @Param []
  * @return java.lang.String[]
  */
  String[] names() default {""};
}

Interceptor interception

The key to the implementation of redis distributed locks is the writing of interceptors. The above annotation is only an aid to implementing interception.


@Around("execution(public * *(..)) && @annotation(com.ay.framework.order.redis.product.StockLock)")

Interception of StockLock annotations via Around of springboot. By interception, we can get the method of interception, parameters, and the parameters of the required lock.

We get the name of the lock that we want called [a] and then we decrements that key through the atomic operation of redis.

In order to facilitate the inventory reduction we can carry out the inventory update operation. We need to resort to another lock before we can destock. This one is called a_key.

In other words, to access our interface, we must acquire [a] locks, and to obtain [a] locks we need to reduce inventory. The [a_key] lock needs to be acquired before inventory can be reduced.

Once we've got the lock and we've processed the logic we need to release the lock.


RedisAtomicLong entityIdCounter = new RedisAtomicLong(lockKey, redisTemplate.getConnectionFactory());
  if (redisTemplate.hasKey(CoreConstants.UPDATEPRODUCTREDISLOCKKEY + lockKey)) {
    // said lockKey There is a change in the inventory information. No transaction can be made at this point 
    throw new BusinessException(" Inventory changes. Cannot trade for the time being ");
  }
  Long increment = entityIdCounter.decrementAndGet();
  if (increment >= 0) {
    try {
      Object proceed = pjp.proceed();
    } catch (Throwable throwable) {
      // The occupied resources need to be released back into the resource pool 
      while (!redisLock.tryGetLock(CoreConstants.UPDATEPRODUCTREDISLOCKKEY + lockKey, "")) {

      }
      // said lockKey There is a change in the inventory information. No transaction can be made at this point 
      long l = entityIdCounter.incrementAndGet();
      if (l < 1) {
        redisTemplate.opsForValue().set(lockKey,1);
      }
      redisLock.unLock(CoreConstants.UPDATEPRODUCTREDISLOCKKEY + lockKey);
      throwable.printStackTrace();
    }
  } else {
    redisTemplate.opsForValue().set(lockKey,0);
    throw new BusinessException(" Out of stock! Unable to operate ");
  }

Because if we lock, we need to release the lock. However, an exception occurred when the program was processing business in the middle of the process, resulting in the failure to go to the lock release step. This causes our distributed lock 1 to be locked. Commonly known as "deadlock". To avoid this scenario. We often give an expiration date when we lock a lock. Auto release lock expired. This feature happens to be the same as redis's expiration policy.

Tools mentioned above

RedisLock


public Boolean tryGetLock(String key , String value) {
  return tryGetLock(key, value, -1, TimeUnit.DAYS);
}
public Boolean tryGetLock(String key , String value, Integer expire) {
  return tryGetLock(key, value, expire, TimeUnit.SECONDS);
}
public Boolean tryGetLock(String key , String value, Integer expire , TimeUnit timeUnit) {
  ValueOperations operations = redisTemplate.opsForValue();
  if (operations.setIfAbsent(key, value)) {
    // instructions  redis There is no the key ,  In other words   Locking success   Set an expiration time to prevent deadlocks 
    if (expire > 0) {
      redisTemplate.expire(key, expire, timeUnit);
    }
    return true;
  }
  return false;
}

public Boolean unLock(String key) {
  return redisTemplate.delete(key);
}

StockKeyGenerator


@Component()
@Primary
public class StockKeyGenerator implements CacheKeyGenerator {
  @Override
  public String getLockKey(ProceedingJoinPoint pjp) {
    // Get method signature 
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    Method method = signature.getMethod();
    // Access method cacheLock annotations 
    StockLock stockLock = method.getAnnotation(StockLock.class);
    // Get method parameters 
    Object[] args = pjp.getArgs();
    Parameter[] parameters = method.getParameters();
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < parameters.length; i++) {
      StockParam stockParam = parameters[i].getAnnotation(StockParam.class);
      Object arg = args[i];
      if (arg instanceof Map) {
        Map<String, Object> temArgMap = (Map<String, Object>) arg;
        String[] names = stockParam.names();
        for (String name : names) {
          if (builder.length() > 0) {
            builder.append(stockLock.delimiter());
          }
          builder.append(temArgMap.get(name));
        }
      }

    }
    return builder.toString();
  }
}

Problem analysis

Based on the above analysis of a deadlock scenario, redis distributed lock solves the distributed problem well. But there are still problems. The following is a list of the problems encountered in writing this site.

Business processing time > Lock expiration time

The a thread gets the lock, 8S is needed to start the business process,

In 8S, the lock is valid for 5S. After the lock expires, which is 6S, the b thread enters and starts to acquire the lock. At this time, b can acquire a new lock. That's when it's problematic.

Suppose that the b thread business processing only needs 3S, but because the a thread releases the lock, the b thread does not release the lock at 8S, and the lock of b does not expire, but there is no lock at this time. This causes the C thread to enter as well

conclusion


Related articles: