describe

  • Dynamic configuration based on SpringEL expressions
  • Based on the slice, cut seamlessly
  • Supports behavior when a lock fails to be acquired, throw an exception or continue to wait, two lock modes, a wait retry, a direct exit

Source address: github.com/shawntime/s…

Method of use

@RedisLockable(key = {"#in.activityId", "#in.userMobile"}, expiration = 120, isWaiting = true, retryCount = 2)
@Override
public PlaceOrderOut placeOrder(OrderIn in) {
    // ------
}
Copy the code

Code implementation

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

    String prefix(a) default "";

    String[] key() default "";

    long expiration(a) default 60;

    boolean isWaiting(a) default false; // The default is no wait

    int retryCount(a) default- 1; // The number of lock wait retries, -1 is not unlimited

    int retryWaitingTime(a) default 10; // Lock wait retry interval, default 10 milliseconds
}
Copy the code
@Aspect
@Component
public class RedisLockInterceptor {

    private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

    private static final ExpressionParser PARSER = new SpelExpressionParser();

    @Pointcut("@annotation(com.shawntime.common.lock.RedisLockable)")
    public void pointcut(a) {}@Around("pointcut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {

        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method targetMethod = AopUtils.getMostSpecificMethod(methodSignature.getMethod(), point.getTarget().getClass());
        String targetName = point.getTarget().getClass().getName();
        String methodName = point.getSignature().getName();
        Object[] arguments = point.getArgs();

        RedisLockable redisLock = targetMethod.getAnnotation(RedisLockable.class);
        long expire = redisLock.expiration();
        String redisKey = getLockKey(redisLock, targetMethod, targetName, methodName, arguments);
        String uuid;
        if (redisLock.isWaiting()) {
            uuid = waitingLock(redisKey, expire, redisLock.retryCount(), redisLock.retryWaitingTime());
        } else {
            uuid = noWaitingLock(redisKey, expire);
        }
        if (StringUtils.isNotEmpty(uuid)) {
            try {
                return point.proceed();
            } finally{ RedisLockUtil.unLock(redisKey, uuid); }}else {
            throw newRedisLockException(redisKey); }}private String getLockKey(RedisLockable redisLock, Method targetMethod, String targetName, String methodName, Object[] arguments) {
        String[] keys = redisLock.key();
        String prefix = redisLock.prefix();
        StringBuilder sb = new StringBuilder("lock.");
        if (StringUtils.isEmpty(prefix)) {
            sb.append(targetName).append(".").append(methodName);
        } else {
            sb.append(prefix);
        }
        if(keys ! =null) {
            String keyStr = Joiner.on("+ '+").skipNulls().join(keys);
            EvaluationContext context = new StandardEvaluationContext(targetMethod);
            String[] parameterNames = DISCOVERER.getParameterNames(targetMethod);
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], arguments[i]);
            }
            Object key = PARSER.parseExpression(keyStr).getValue(context);
            sb.append("#").append(key);
        }
        return sb.toString();
    }

    private String noWaitingLock(String key, long expire) {
        return RedisLockUtil.lock(key, expire);
    }

    private String waitingLock(String key, long expire, int retryCount, int retryWaitingTime)
            throws InterruptedException {
        int count = 0;
        while (retryCount == -1 || count <= retryCount) {
            String uuid = noWaitingLock(key, expire);
            if(! StringUtils.isEmpty(uuid)) {return uuid;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(retryWaitingTime);
            } catch (InterruptedException e) {
                throw e;
            }
            count++;
        }
        return null; }}Copy the code
/** * Distributed lock tool class */
public final class RedisLockUtil {

    private static final int DEFAULT_EXPIRE = 60;

    private static final String SCRIPT =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
            + "then\n"
            + " return redis.call(\"del\",KEYS[1])\n"
            + "else\n"
            + " return 0\n"
            + "end";

    private RedisLockUtil(a) {
        super(a); }/ * * * *@paramKey Key * of the lock@returnIf value is null, the lock fails. If value is not NULL, the lock succeeds */
    public static String lock(String key) {
        return lock(key, DEFAULT_EXPIRE);
    }

    public static boolean lock(String key, String value) {
        return lock(key, value, DEFAULT_EXPIRE);
    }

    public static String lock(String key, long expire) {
        String value = UUID.randomUUID().toString();
        boolean nx = SpringRedisUtils.setNX(key, value, expire);
        return nx ? value : null;
    }

    public static boolean lock(String key, String value, long expire) {
        return SpringRedisUtils.setNX(key, value, expire);
    }

    public static void unLock(String key, String value) { SpringRedisUtils.lua(SCRIPT, Collections.singletonList(key), Collections.singletonList(value)); }}Copy the code

Three implementations of redis distributed locking

First: use the setnx(), get(), and getSet () methods

> SETNX command (SET if Not eXists) \ Syntax: SETNX key value\ Function: atomic operation, if and only if the key does Not exist, SET the key value to value, and return 1; If the given key already exists, SETNX does nothing and returns 0. \ GETSET command \ Syntax: GETSET key value\ Function: set the value of a given key to value and return the old value of the key, an error if the key exists but is not a string, and nil if the key does not exist. GET command syntax: GET key Function: return the string value associated with the key, or nil if the key does not exist. \ DEL command \ Syntax: DEL key [key...] \ function: delete a given key or keys. Non-existent keys are ignored.Copy the code
  • Setnx (lockkey, current time + expiration timeout), if 1 is returned, the lock is obtained successfully; If 0 is returned and no lock was acquired, go to 2.
  • Get (lockkey) Obtains the value oldExpireTime and compares the value with the current system time. If the value is smaller than the current system time, the lock is considered to have timed out and can be reobtained by other requests, and then goes to 3.
  • NewExpireTime = current time + expiration timeout, and getSet (LockKey, newExpireTime) returns currentExpireTime.
  • Check whether currentExpireTime is the same as oldExpireTime. If currentExpireTime is the same as oldExpireTime, it indicates that getSet is successfully set and the lock is obtained. If not, it means that the lock has been acquired by another request, and the current request can directly return failure or continue to retry.
  • After obtaining the lock, the current thread can start its own business processing. When the processing is complete, the thread compares its processing time with the timeout period set for the lock. If the timeout period is less than the timeout period set for the lock, the thread directly executes delete to release the lock. If the value is greater than the timeout period set for the lock, the lock does not need to be processed.
/** * wait for lock ** @param key redis key * @param expire time, in seconds * @return true: lock successfully, false, */ Private Boolean waitingLock(String key, long expire, int retryCount) {int count = 0; while (retryCount == -1 || count <= retryCount) { if (noWaitingLock(key, expire)) { return true; } try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } count++; } return false; } /** * lock ** @param key redis key * @param expire time, in seconds * @return true: lock successfully, false, */ Private Boolean noWaitingLock(String key, long expire) { long value = System.currentTimeMillis() + expire * 1000; long status = redisClient.setnx(key, value); if (status == 1) { return true; } long oldExpireTime = Long.parseLong(redisClient.get(key, "0", false)); if (oldExpireTime < System.currentTimeMillis()) { long newExpireTime = System.currentTimeMillis() + expire * 1000; String currentExpireTimeStr = redisClient.getSet(key, String.valueOf(newExpireTime)); if (StringUtils.isEmpty(currentExpireTimeStr)) { return true; } long currentExpireTime = Long.parseLong(currentExpireTimeStr); if (currentExpireTime == oldExpireTime) { return true; } } return false; } private void unLock(String key, long startTime, long expire) { long parseTime = System.currentTimeMillis() - startTime; if (parseTime <= expire * 1000) { redisClient.del(key); }}Copy the code

Through the SET key value [EX seconds] [PX milliseconds] [NX | XX]

EX second: Sets the expiration time of the key to second seconds. SET key value EX second The effect is the same as SETEX key second value. PX millisecond: set expiration time to millisecond milliseconds. SET key value PX millisecond has the same effect as PSETEX key millisecond value. NX: Set the key only when it does not exist. SET key value NX has the same effect as SETNX key value. XX: Set the key only when it already exists.

private boolean noWaitingLock2(String key, String uuid, long expire) { String value = redisClient.setnx(key, uuid, expire); return value ! = null; } private boolean waitingLock2(String key, String uuid, long expire, int retryCount) { int count = 0; while (retryCount == -1 || count <= retryCount) { if (noWaitingLock2(key, uuid, expire)) { return true; } try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } count++; } return false; }Copy the code

It is not feasible to delete locks directly using del, because it may cause the deletion of other locks by mistake.

For example, I applied the lock for 10 seconds, but the processing time was longer than 10 seconds. At 10 seconds, the lock automatically expired and was taken by someone else, who locked it again. At this point, I call Redis::del to delete the lock created by others. Using the Lua script, do get first, then del

private static final String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; private void unLock2(String key, String uuid) { Object result = redisClient.lua(script, Collections.singletonList(key), Collections.singletonList(uuid)); System.out.println(result); } public Object lua(final String script, List<String> keys, List<String> args) { Jedis jedis = null; try { jedis = pool.getResource(); return jedis.eval(script, keys, args); } catch (Exception ex) { LOGGER.error(ex); return 0; } finally { returnResource(jedis); }}Copy the code

Redissons implements distributed locks

RLock rLock = redisson.getLock(lockKey); long expired = lock.expire(); boolean isLock = rLock.tryLock(expired, TimeUnit.SECONDS); If (isLock) {try {// process} finally {rlock.unlock (); } } rLock.tryLock(3, expired, TimeUnit.SECONDS);Copy the code