• 项目实战:一个由多线程引起的线程安全问题(附:解决方案)


    项目场景:

    • 上游接口批量推送订单信息,订单外面还有运单信息(一个运单包含多个订单),订单和运单都允许有条件的修改。当接口收到推送过来的数据时,要先去查询这个订单对应的运单是否已经存在,不存在则直接把运单信息插入到数据库表中,存在要做判断运单的状态是否允许更新,不允许直接抛异常。
    "submitList": [
        {
          "deliveryCode": "YD12345678"
          "xdockOriginOrderDTOS": [
            {
              "deliveryCode": "YD12345678",
              "orderCode": "DD12345677"
            }
          ]
        },
        {
          "deliveryCode": "YD12345678"
          "xdockOriginOrderDTOS": [
            {
              "deliveryCode": "YD12345678",
              "orderCode": "DD12345688"
            }
          ]
        }
      ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    问题描述

    • 当上游一次推送属于一个运单下的两个订单过来的时候,如果没有在数据库表的运单号字段添加唯一索引,会出现两条运单号一样的数据。如果运单号字段添加了唯一索引,则会抛 ["\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '462479945432663' for key 'goods_xdock_package.goods_xdock_package_delivery_code_uindex'"] 这个异常。

    • 出现线程安全问题的批量新增运单的代码:

    // submitList 结构见上面,只展示和问题相关的字段
    public Boolean batchAddOriginOrder(List<XdockPackageDTO> submitList) {
    	for (XdockPackageDTO submitDTO : submitList) {
    	   try {
    	       this.addPackage(submitDTO);
    	   } catch (Exception e) {
    	       log.error(e.getMessage());
    	       e.printStackTrace();
    	   }
    	}	
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    public Boolean addPackage(XdockPackageDTO xdockPackageDTO) {
    	if (Strings.isNotBlank(xdockPackageDTO.getDeliveryCode())) {
            XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
            if (Objects.nonNull(one)) {
                Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
            } else {
    			XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
    	        xdockPackageService.save(xdockPackage);
    		}
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    原因分析:

    • 上面addPackage()方法主要包含3个步骤:

      • a. 查询数据库表中是否存在当前传入的运单号
      • b. 判断查询结果是否为空
      • c. 不为空就判断运单状态,为空就新增运单信息
        在这里插入图片描述
    • 循环遍历列表 for (XdockPackageDTO submitDTO : submitList) ,创建两个线程(线程1线程2)分别执行两条订单信息的创建。

    • 线程1先去执行步骤a(a. 查询数据库表中是否存在当前传入的运单号),然后执行步骤b(b. 判断查询结果是否为空)。由于CPU的切换关系,此时CPU的执行权被切换到了线程2。线程1将步骤b的判断结果等信息保存到线程1的工作内存中,就处于就绪状态,线程2处于运行状态。

    • 线程2也需要执行步骤a,由于线程1没有对运单号做新增操作。因此此时线程2执行步骤b查询到的结果也为空。

    • 此时CPU的执行权切换到了线程1上。线程1将工作内存中的之前存储的查询结果等数据恢复,执行步骤c(c. 为空就新增运单信息),插入运单号。线程1执行完毕,线程1销毁,

    • CPU执行线程2,线程2将工作内存中的之前存储的查询结果等数据恢复,执行步骤c(c. 为空就新增运单信息),插入运单号。如果没有在数据库表的运单号字段添加唯一索引,会出现两条运单号一样的数据。如果运单号字段添加了唯一索引,则会抛 ["\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '462479945432663' for key 'goods_xdock_package.goods_xdock_package_delivery_code_uindex'"] 这个异常。


    解决方案:

    Lock 锁

    private final ReentrantLock lock = new ReentrantLock();
    public Boolean addPackage(XdockPackageDTO xdockPackageDTO) {
    	  if (Strings.isNotBlank(xdockPackageDTO.getDeliveryCode())) {
    	      lock.lock();
    	      try {
    	          XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
    	          if (Objects.nonNull(one)) {
    	              Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
    	          } else {
    				  XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
    			      xdockPackageService.save(xdockPackage);
    			}
    	      }catch (Exception e) {
    	          e.printStackTrace();
    	      }finally {
    	          lock.unlock();
    	      }
    	  }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    synchronized锁

    synchronized (this) {
       XdockPackage one = xdockPackageService.getOne(new LambdaQueryWrapper<XdockPackage>().eq(XdockPackage::getDeliveryCode, xdockPackageDTO.getDeliveryCode()));
        if (Objects.nonNull(one)) {
            Assert.isTrue(PackageStatusEnum.CREATED.getCode().equals(one.getStatus()), "运单已签收,不允许修改");
        } else {
            XdockPackage xdockPackage = XdockConvert.INSTANCE.xdockPackage2DTO(xdockPackageDTO);
            xdockPackageService.save(xdockPackage);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    synchronized 与 Lock 的区别

    synchronized 和 Lock 的用法区别

    • synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
    • lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效,且在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。

    synchronized 和 Lock 性能区别

    • synchronized 是托管给 JVM 执行的,而 Lock 是 Java 写的控制锁的代码。在 JDK 1.5 中,synchronize 是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。但是到了 JDK 1.6,synchronize 进行很多优化,比如锁升级无锁 → 偏向锁(JDK15已废弃) → 轻量级锁 → 重量级锁)等等。所以在 JDK 1.6 以上 synchronize 的性能并不比 Lock 差。

    synchronized 和 lock 机制区别

    • synchronized 原始采用的是 CPU 悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
    • Lock 用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。

    你知道的越多,你不知道的越多。

  • 相关阅读:
    (c语言)五子棋<可修改棋数>
    基于Netty的高性能RPC框架(分布式缓存、雪花算法、幂等性)
    【无标题】c第三弹已发射
    Java 多线程之 synchronized (互拆锁/排他锁/非观锁)
    故障电弧探测器的必要性及组网方案 安科瑞 时丽花
    信号和槽自动链接20221021
    深度学习-nlp系列(2)文本分类(Bert)pytorch
    idea 启动项目报错 Command line is too long
    再见 Typescript,你好 Javascript 原生打字 ✨
    C++ 混合运算的类型转换
  • 原文地址:https://blog.csdn.net/qq_40722827/article/details/126067908