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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 多线程执行事务中再加锁导致的bug----------记一次线上问题优化

image.png

先贴上问题代码:

/**
 * 根据用户手机号进行注册操作
 */
// 启动@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:

  • 当用户执行注册操作,重复点击注册按钮时,假设线程A和B同时执行到 redisLock.lock()时,假设线程A获取到锁,线程B进入自旋等待,线程A执行mapper.findByMobile(body.getAccount(), body.getRegRes())操作,发现用户手机不存在数据库中,进行注册操作(添加用户信息入库等),执行完毕,释放锁。执行后续添加注册日志,上报到数据分析平台操作,注意此时事务还未提交。
  • 线程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;
}


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
安全 Java 编译器
线程安全问题和锁
本文详细介绍了线程的状态及其转换,包括新建、就绪、等待、超时等待、阻塞和终止状态,并通过示例说明了各状态的特点。接着,文章深入探讨了线程安全问题,分析了多线程环境下变量修改引发的数据异常,并通过使用 `synchronized` 关键字和 `volatile` 解决内存可见性问题。最后,文章讲解了锁的概念,包括同步代码块、同步方法以及 `Lock` 接口,并讨论了死锁现象及其产生的原因与解决方案。
81 10
线程安全问题和锁
|
2月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
44 2
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
46 4
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
1月前
|
运维 API 计算机视觉
深度解密协程锁、信号量以及线程锁的实现原理
深度解密协程锁、信号量以及线程锁的实现原理
33 1
|
1月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
32 0
|
1月前
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
26 0
|
1月前
|
Java API
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
31 0
|
1月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
43 0
|
1月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解