多线程执行事务中再加锁导致的bug----------记一次线上问题优化

168 阅读3分钟

先贴上问题代码:

/**
 * 根据用户手机号进行注册操作
 */
// 启动@Transactional事务注解
@Transactional(rollbackFor = Exception.class)
private 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();
        // 使用redis分布式锁
        if (lock) {
            // 查询数据库该用户手机号是否插入成功,已存在则退出操作
            MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());
            if (Objects.nonNull(member)) {
                resp.setResultFail(ReturnCodeEnum.USER_EXIST);
                return false;
            }
            // 执行用户注册操作,包含插入用户表、订单表、是否被邀请
            ...
        }
    } catch (Exception e) {
        log.error("用户注册失败:", e);
        throw new Exception("用户注册失败");
    } finally {
        redisLock.unLock();
    }
    // 添加注册日志,上报到数据分析平台...
    return true;
}

初看代码,以为逻辑上没有问题,在分布式环境中,先加分布式锁判断是否存在用户手机信息,已存在则退出,不存在则执行用户注册操作,但是在实际执行过程中,当多线程同时执行会发现在极端情况下会有相同手机号重复注册的情况

由于上诉注册逻辑包含在spring提供的自动事务中,整个方法都在事务中。而加锁也在事务中执行。举个例子

eg:

  1. 当用户执行注册操作,重复点击注册按钮时,假设线程A和B同时执行到 redisLock.lock()时,假设线程A获取到锁,线程B进入自旋等待,线程A执行mapper.findByMobile(body.getAccount(), body.getRegRes())操作,发现用户手机不存在数据库中,进行注册操作(添加用户信息入库等),执行完毕,释放锁。执行后续添加注册日志,上报到数据分析平台操作,注意此时事务还未提交。
  2. 线程B终于获取到锁,执行mapper.findByMobile(body.getAccount(), body.getRegRes())操作,在我们一开始的假相中,以为这里会返回用户已存在,但是实际执行结果并不是这样的。原因就是线程A的事务还未提交,线程B读不到线程A未提交事务的数据也就是说查不到用户已注册信息,至此,我们知道了用户重复注册的原因。

回顾一下,MySQL事务的隔离级别有4个,读未提交、读已提交、可重复读、序列化,隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,MySQL的默认隔离级别是读可重复读。在上述场景里,也就是说一个线程是读不到另一个线程未提交的数据的。

解决办法有多种,比如修改上述注册代码,将事务的操作代码最小化保证在加锁结束前完成事务提交,代码如下,这样其他线程就能看到最新数据;或者是在用户注册时添加防重复提交处理。

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();
        // 使用redis分布式锁
        if (lock) {
            // 查询数据库该用户手机号是否插入成功,已存在则退出操作
            MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes());
            if (Objects.nonNull(member)) {
                resp.setResultFail(ReturnCodeEnum.USER_EXIST);
                return false;
            }
            // 手动开启事务
            transaction = platformTransactionManager.getTransaction(transactionDefinition);
            // 执行用户注册操作,包含插入用户表、订单表、是否被邀请
            ...
            // 手动提交事务
            platformTransactionManager.commit(transaction);
            ...
        }
    } catch (Exception e) {
        log.error("用户注册失败:", e);
        if (transaction != null) {
            platformTransactionManager.rollback(transaction);
        }
        return false;
    } finally {
        redisLock.unLock();
    }
    // 添加注册日志,上报到数据分析平台...
    return true;
}