In the water touch fish, suddenly heard some users reflect increased a number of the same data, the user immediately quit, let us immediately repair, or we will complain.

I can’t touch the fish anymore. I have to go see what’s going on. According to the user, there was a dot card in the network at that time, so we submitted several times, and finally found more than ten pieces of the same data.

Can only say that nowadays people are too impatient, even these seconds of time can not wait, used to. In the mind to ridicule ridicule, this problem or to solve, or the boss can not be used to me.

If you think about it, the user clicks several times while the network is delayed, and the last few requests are sent to the server to access the relevant interface and finally perform the insert.

Now that we know the cause, how to solve it. My first thought was to use annotations + AOP. By defining relevant fields in custom annotations, such as expiration time, the time within which the same user cannot submit a request again. The annotations are then applied to the interface as needed, and the interceptor determines if the interface exists and intercepts it if it does.

Once this problem is solved, another problem needs to be solved, which is how to determine if the current user has accessed the current interface for a limited time. In fact, this is also simple, you can use Redis to do, username + interface + parameter as a unique key, and then set the expiration time of this key to the value of the expiration field in the annotation. Setting an expiration time allows the key to expire automatically, otherwise the interface will remain inaccessible if the thread suddenly stops working.

The other thing to watch out for is that if you go to Redis to get the key, and then determine that the key doesn’t exist, set the key; If yes, the access time is not reached, and a message is displayed. That’s a good idea, but if you split get and set into two operations, it’s not atomicity, and you can get it wrong in multiple threads. So you have to turn these two operations into one atomic operation.

Once you’ve analyzed it, start working.

1. Custom annotations

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** ** to prevent simultaneous submission of annotations */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatCommit {
    // The expiration time of the key is 3s
    int expire(a) default 3;
}
Copy the code

For simplicity, only one field is defined, with a default value of 3, that is, the same user is not allowed to access the same interface repeatedly for 3s. Custom values can also be passed in when used.

We just need to add the annotation to the corresponding interface

@NoRepeatCommitor@NoRepeatCommit(expire = 10)
Copy the code

2. Custom interceptors

With custom annotations, it’s time to write interceptors.

@Aspect
public class NoRepeatSubmitAspect {
    private static Logger _log = LoggerFactory.getLogger(NoRepeatSubmitAspect.class);
    RedisLock redisLock = new RedisLock();

    @Pointcut("@annotation(com.zheng.common.annotation.NoRepeatCommit)")
    public void point(a) {}

    @Around("point()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        / / get request
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = servletRequestAttributes.getRequest();
        HttpServletResponse responese = servletRequestAttributes.getResponse();
        Object result = null;

        String account = (String) request.getSession().getAttribute(UpmsConstant.ACCOUNT);
        User user = (User) request.getSession().getAttribute(UpmsConstant.USER);
        if (StringUtils.isEmpty(account)) {
            return pjp.proceed();
        }

        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        NoRepeatCommit form = method.getAnnotation(NoRepeatCommit.class);

        String sessionId = request.getSession().getId() + "|" + user.getUsername();
        String url = ObjectUtils.toString(request.getRequestURL());
        String pg = request.getMethod();
        String key = account + "_" + sessionId + "_" + url + "_" + pg;
        int expire = form.expire();
        if (expire < 0) {
            expire = 3;
        }

        / / acquiring a lock
        boolean isSuccess = redisLock.tryLock(key, key + sessionId, expire);
        // Succeeded
        if (isSuccess) {
            // Execute the request
            result = pjp.proceed();
            int status = responese.getStatus();
            _log.debug("status = {}" + status);
            // Release the lock automatically after 3s, or manually release it
            // redisLock.releaseLock(key, key + sessionId);
            return result;
        } else {
            // Failed as a duplicate request
            return newUpmsResult(UpmsResultConstant.REPEAT_COMMIT, ValidationError.create(UpmsResultConstant.REPEAT_COMMIT.message)); }}}Copy the code

The interceptor defines a pointcut for the NoRepeatCommit annotation, so the interface marked by the NoRepeatCommit annotation will enter the interceptor. Here I use account + “_” + sessionId + “_” + URL + “_” + pg as the unique key to indicate that a user accesses an interface.

The key line is Boolean isSuccess = redislock. tryLock(key, key + sessionId, expire); . Take a look at the RedisLock class.

3. Redis utility class

As discussed above, obtaining locks and setting locks need to be atomic, otherwise problems can occur in concurrent environments. Here you can use the SETNX command of Redis.

/** * Redis distributed locks implement Lua expressions to preserve atomicity of data */
public class RedisLock {

    /** ** Redis lock success constant */
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    private static final String LOCK_SUCCESS= "OK";
    /** * lock Lua expressions. * /
    private static final String RELEASE_TRY_LOCK_LUA =
            "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end";
    /** * Unlocks the Lua expression. */
    private static final String RELEASE_RELEASE_LOCK_LUA =
            "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /** * locking * supports repetition, thread safety * Since the thread holding the lock crashes, there is no deadlock, because the lock is automatically released when it expires *@paramLockKey lockKey *@paramUserId unique identifier of the lock client (use the userId, which needs to be converted to String) *@paramExpireTime Lock expiration time *@returnOK If key is set to */
    public boolean tryLock(String lockKey, String userId, long expireTime) {
        Jedis jedis = JedisUtils.getInstance().getJedis();
        try {
            jedis.select(JedisUtils.index);
            String result = jedis.set(lockKey, userId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (LOCK_SUCCESS.equals(result)) {
                return true; }}catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(jedis ! =null)
                jedis.close();
        }

        return false;
    }

    /** * unlock * corresponds to tryLock, used to release the lock * unlock must be the same person as the lock, others can not unlock the lock **@paramLockKey lockKey *@paramUserId unique identifier of the unlock client (the userId is used and needs to be converted to String) *@return* /
    public boolean releaseLock(String lockKey, String userId) {
        Jedis jedis = JedisUtils.getInstance().getJedis();
        try {
            jedis.select(JedisUtils.index);
            Object result = jedis.eval(RELEASE_RELEASE_LOCK_LUA, Collections.singletonList(lockKey), Collections.singletonList(userId));
            if (RELEASE_SUCCESS.equals(result)) {
                return true; }}catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(jedis ! =null)
                jedis.close();
        }

        return false; }}Copy the code

Set (lockKey, userId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); . The set method looks like this

/* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1 GB). Params: The key - value - NXXX - NX | XX, NX - Only set the key if it does not already exist. XX - Only set the key if it already exist. Expx - EX | PX. expire time units: EX = seconds; PX = milliseconds time -- expire time in the units of EXPx Returns: Status code reply */
public String set(final String key, final String value, final String nxxx, final String expx,
      final long time) {
    checkIsInMultiOrPipeline();
    client.set(key, value, nxxx, expx, time);
    return client.getStatusCodeReply();
  }
Copy the code

The key is set only when it does not exist. If the key is set successfully, OK is returned. This allows you to query and set atomicity.

Note that I’m running out of jedis, so I need to close it, or I’m going to run out of connections, and I’m not going to tell you that I crashed the server.

4. Anything else you want to say

In fact, these three steps are almost enough, almost enough. What if, for example, I haven’t finished executing the logic for the interface within the expire set time?

Well, instead of busting the wheel here ourselves, why don’t we just use the strong wheel? Such as Redisson, to implement distributed locks, then the above problem is not to worry about. A watchdog will do this for you, and when the key expires, if it is detected that the key is still held by the thread, it will reset the expiration time of the key.