• notice
  • Congratulations on the launch of the Sought Tech site

User duplicate registration analysis - bugs caused by locking in multi-threaded transactions

a recurring process

Online client users need to bind a mobile phone number when logging in by scanning QR codes on WeChat. After binding the mobile phone, the user purchases products on the client terminal and logs in again, and finds that the user account ID has been changed. It is no longer the mobile phone number that the user just bound. The ID of the user account that is automatically logged in at the time, query the online database, and find that the same mobile phone has generated multiple account IDs, so the problem has reappeared

Two analysis process

It is found that one mobile phone number in the database has generated multiple user accounts. The first reaction is that the user clicks the binding button multiple times during the process of binding the mobile phone number, causing the binding interface to be called multiple times, causing multiple threads to call the user registration interface concurrently. , and then generate multiple accounts. In order to verify our conjecture, directly check the user registration method after binding the mobile phone

/**
 * Register according to the user's mobile phone number
 */
// Start @Transactional transaction annotation
@Transactional(rollbackFor = Exception. class)
public boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp<BaseRespHeader, LoginRespBody> resp) {
    RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10);
    boolean lock;
    try {
        lock = redisLock. lock();
        // Use redis distributed lock
        if (lock) {
            // Query the database to see if the user's phone number has been successfully inserted, if it exists, exit the operation
            MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());
            if (Objects. nonNull(member)) {
                resp.setResultFail(ReturnCodeEnum.USER_EXIST);
                return false;
            }
            // Execute the user registration operation, including inserting the user table, order table, and whether to be invited
            ...
        }
    } catch (Exception e) {
        log.error("User registration failed: ", e);
        throw new Exception("User registration failed");
    } finally {
        redisLock.unLock();
    }
    // Add registration logs and report to the data analysis platform...
    return true;
}

At first glance at the code, in a distributed environment, first add a distributed lock to ensure that it can only be executed by one thread at the same time, and then judge whether there is user mobile phone information in the database, exit if it exists, and perform user registration if it does not exist, what is logic? There is no problem, but the online environment does have the problem of repeated registration of the same mobile phone number. First, the code is included in the @Transactionalannotation , which is to execute the registration logic in the automatic transaction

Now the blogger will take everyone to recall that there are 4 isolation levelsMySQL for transactions

  • Read uncommitted: read uncommitted, as long as other transactions modify the data, even if it is not committed, this transaction can still see the modified data value.

  • Read committed: The read has been committed. After other transactions have submitted the modification to the data, this transaction can read the modified data value.

  • Repeatable read: Repeatable read, no matter whether other transactions modify and commit data, the data value seen in this transaction is always not affected by other transactions.

  • Serializable: Serialization, the execution of a transaction one by one.

  • MySQL database uses repeatable read (Repeatable read) by default.

The higher the isolation level, the more data integrity and consistency can be guaranteed, but the impact on concurrency performance is also greater. The default isolation level of MySQL is read repeatable read. In the above scenario, that is to say, no matter whether other thread transactions have submitted data or not, the data value seen in the transaction where the current thread is located is always unaffected by other transactions

Speaking human words (emphasis): MySQLin the transaction where a thread is located, the uncommitted data of another thread transaction cannot be read

The analysis process is given below in conjunction with the above code: the above registration logic is included in the Springprovided automatic transaction, and the entire method is in the transaction. Locking is also performed within a transaction. In the end, our 线程Bregistration cannot query the unsubmitted 线程Adata of another registration in the current transaction, for example

eg:

  1. When the user performs the registration operation and clicks the registration button repeatedly, assuming that threads A and B are executed at the same redisLock.lock()time , assuming that thread A acquires the lock, thread B enters the spin wait, thread A executes the mapper.findByMobile(body.getAccount(), body.getRegRes())operation and finds that the user's mobile phone does not exist in the database, and proceeds After the registration operation (adding user information into the database, etc.), the lock is released after the execution is completed. Perform subsequent operations of adding registration logs and reporting to the data analysis platform. Note that the transaction has not been submitted yet.

  2. Thread B finally acquires the lock and performs the mapper.findByMobile(body.getAccount(), body.getRegRes())operation. In our initial assumption, we thought it would return that the user already exists, but the actual execution result is not like this. The reason is that the transaction of thread A has not been submitted yet, and thread B cannot read the data of the uncommitted transaction of thread A, which means that the user's registered information cannot be found. So far, we know the reason for the user's repeated registration.

Three solutions:

give three solutions

3.1 Modify the scope of the transaction, minimize the operation code of the transaction, and ensure that the transaction submission is completed before the end of the lock. The code is as follows to start the manual transaction, so that other threads can see the latest data in the locked code block

@Autowired
private PlatformTransactionManager platformTransactionManager;

@Autowired
private TransactionDefinition transactionDefinition;

private boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp<BaseRespHeader, LoginRespBody> resp) {
    RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10);
    boolean lock;
    TransactionStatus transaction = null;
    try {
        lock = redisLock. lock();
        // Use redis distributed lock
        if (lock) {
            // Query the database to see if the user's phone number has been successfully inserted, if it exists, exit the operation
            MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());
            if (Objects. nonNull(member)) {
                resp.setResultFail(ReturnCodeEnum.USER_EXIST);
                return false;
            }
            // start the transaction manually
            transaction = platformTransactionManager.getTransaction(transactionDefinition);
            // Execute the user registration operation, including inserting the user table, order table, and whether to be invited
            ...
            // Manually commit the transaction
            platformTransactionManager.commit(transaction);
            ...
        }
    } catch (Exception e) {
        log.error("User registration failed: ", e);
        if (transaction != null) {
            platformTransactionManager.rollback(transaction);
        }
        return false;
    } finally {
        redisLock.unLock();
    }
    // Add registration logs and report to the data analysis platform...
    return true;
}

3.2 Add anti-duplication submission processing for the registration interface when the user registers

The following is a current limiting logic based on AOPaspect + annotation

/**
 * Current limit enumeration
 */
public enum LimitType {
    // default
    CUSTOMER,
    // by ip addr
    IP
}

/**
 * Custom interface current limit
 *
 * @author jacky
 */
@Target(ElementType. METHOD)
@Retention(RetentionPolicy. RUNTIME)
public @interface Limit {
    boolean useAccount() default true;
    String name() default "";
    String key() default "";
    String prefix() default "";
    int period();
    int count();
    LimitType limitType() default LimitType. CUSTOMER;
}

/**
 * Limiter slice
 */
@Slf4j
@Aspect
@Component
public class LimitAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Pointcut("@annotation(com.dogame.dragon.sparrow.framework.common.annotation.Limit)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder. getRequestAttributes();
        HttpServletRequest request = attrs. getRequest();
        MethodSignature signature = (MethodSignature) joinPoint. getSignature();
        Method signatureMethod = signature. getMethod();
        Limit limit = signatureMethod. getAnnotation(Limit. class);
        boolean useAccount = limit. useAccount();
        LimitType limitType = limit. limitType();
        String key = limit. key();
        if (StringUtils. isEmpty(key)) {
            if (limitType == LimitType.IP) {
                key = IpUtils. getIpAddress(request);
            } else {
                key = signatureMethod. getName();
            }
        }
        if (useAccount) {
            LoginMember loginMember = LocalContext. getLoginMember();
            if (loginMember != null) {
                key = key + "_" + loginMember. getAccount();
            }
        }
        String join = StringUtils.join(limit.prefix(), key, "_", request.getRequestURI().replaceAll("/", "_"));
        List<String> strings = Collections. singletonList(join);

        String luaScript = buildLuaScript();
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        Long count = stringRedisTemplate.execute(redisScript, strings, limit.count() + "", limit.period() + "");
        if (null != count && count.intValue() <= limit.count()) {
            log.info("The {}th access key is {}, the interface described as [{}]", count, strings, limit.name());
            return joinPoint. proceed();
        } else {
            throw new DragonSparrowException("The number of visits in a short period of time is limited");
        }
    }

    /**
     * Limit script
     */
    private String buildLuaScript() {
        return "local c" +
                "\nc = redis. call('get',KEYS[1])" +
                "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
                "\nreturn c;" +
                "\nend" +
                "\nc = redis. call('incr',KEYS[1])" +
                "\nif tonumber(c) == 1 then" +
                "\nredis. call('expire',KEYS[1],ARGV[2])" +
                "\nend" +
                "\nreturn c;";
    }
}

3.3 The front end adds anti-connection processing for the bound mobile phone button

Four summary

Online projects need to think more about the use of automatic transaction annotations Springprovided by , minimize the scope of transaction impact, and add anti-repeat click processing on the front and back ends for buttons such as registration




Tags

Technical otaku

Sought technology together

Related Topic

0 Comments

Leave a Reply

+