Spring事务同步synchronized一起使用引发的问题

​ 今天在开发一个功能时可能存在并发问题,就是可能同时多个人访问一个方法,但是方法执行是有先后顺序的,同时该方法加入了事务。于是我就在该方法上加上了同步synchronized。为了测试就自己建了结构线程同时访问该方法,发现第一个线程访问完后,要该一个数据的状态,如果下一个线程进入的时候先判断状态,如果状态变更了就不再进行执行了,但是第二线程执行的时候发现数据的状态是第一个线程更改之前的状态。具体的测试代码大体如下:

@Transactional(rollbackFor = RuntimeException.class)
public synchronized boolean finishTransfer(TransferFinishPo transferFinishPo, Supplier<SysUser> apply) {SysUser user = apply.get();final Transfer transfer = this.getTransferById(transferFinishPo.getId());if (transfer.getStatus().equals(TransferStatusEnum.DONE.getCode())) {throw new ServiceException("已经完成的调拨,不需要再进行操作!");}//中间还有很多执行语句省略。。。transfer.setStatus(TransferStatusEnum.DONE.getCode());this.updateById(transfer);return true;}
//测试@Testvoid finishTransfer() {TransferFinishPo transferFinishPo = new TransferFinishPo();transferFinishPo.setId(36L);transferFinishPo.setTransportNo("1231231");List<AttachVo> attachVos = new ArrayList<>();AttachVo attachVo = new AttachVo();transferFinishPo.setAttaches(attachVos);new Thread(()->{System.out.println(" 启动线程1" );transferService.finishTransfer(transferFinishPo, () -> USER);}).start();new Thread(()->{System.out.println(" 启动线程2" );transferService.finishTransfer(transferFinishPo, () -> USER);}).start();new Thread(()->{System.out.println(" 启动线程3" );transferService.finishTransfer(transferFinishPo, () -> USER);}).start();new Thread(()->{System.out.println(" 启动线程4" );transferService.finishTransfer(transferFinishPo, () -> USER);}).start();try {System.in.read();} catch (IOException e) {e.printStackTrace();}}

解决步骤:

  1. 开始怀疑是mybatis,mybaits_plus,mysql存在缓存的问题。

    因为我觉得是存在缓存,在第二个执行查询的时候是不是没有直接去数据库查询就从缓存中获取的,一级缓存,二级缓存。但是我当时也有 个疑问就是,数据缓存不是在更新update 的时候会发相关的查询就给作废了吗?

    没有其他的办法就先把缓存设置去掉

    • 先去掉一级缓存就是在mapper 文件中的 ,很遗憾没有起作用

    • 接着就把二级缓存关闭,很遗憾还是没有起作用

      mybatis-plus:mapper-locations: classpath*:mapper/**/*.xmltypeAliasesPackage: com.ruoyi.**.domainconfiguration:cache-enabled: false
      
    • 那是不是跟mysql有关系呢?我用的是mysql8,我记得mysql8是默认将缓存关闭的,实时也是这样的,但是当时就怀疑到这个方向了,于是各种设置mysql的缓存,期间还因为修改配置导致mysql启动不起来,当时环境是用的docker容器,导致容器起不来,也没法进入修改配置,只能重新起一个容器,幸亏docker volumes 的数据持久化,只需要重启一个容器就可以,吓出一阵冷汗。

  2. 那就是方向有问题,最根本的原因就是第一个线程事务执行,其他的线程也执行了,那跟事务有关,那就看数据库是怎么执行的。看mysql 的执行情况,那是根本。

    于是想验证一下这个想法,就看mysql的执行日志

    • 文件记录数据库操作日志
        show variables like 'gen%';
      

        ​          set global general_log=ON;​            进入容器查看日志文件​          tail -f 500 /var/lib/mysql/ecc9f7172a61.log
    
    • 查看到一个记录

      2022-07-23T06:36:33.638423Z       809 Query     SET autocommit=0
      2022-07-23T06:36:33.638569Z       812 Query     SET autocommit=0
      2022-07-23T06:36:33.638775Z       810 Query     SET autocommit=0
      2022-07-23T06:36:33.639129Z       811 Query     SET autocommit=0
      2022-07-23T06:36:33.810973Z       812 Query     SELECT id,code,order_id,purchase_bom_id,purchase_detail_id,material_id,stock_id,from_store_id,from_store_name,to_store_id,to_store_name,stock,pending_stock,customer_stock,customer_pending_stock,status,del_flag,version,finish_time,material_name,material_code,supplier,supplier_id,supplier_pn,transport_no,create_by,create_time,update_by,update_time,create_user_name,update_user_name FROM dhos_transfer WHERE id=36
      2022-07-23T06:36:33.941926Z       812 Query     SELECT @@session.transaction_read_only
      2022-07-23T06:36:33.945942Z       812 Query     UPDATE dhos_transfer  SET code='TO1656319433900',=
      ==========================================中间省略了很多查询更新语句======================================================
      2022-07-23T06:36:34.472950Z       810 Query     SELECT id,code,order_id,purchase_bom_id,purchase_detail_id,material_id,stock_id,from_store_id,from_store_name,to_store_id,to_store_name,stock,pending_stock,customer_stock,customer_pending_stock,status,del_flag,version,finish_time,material_name,material_code,supplier,supplier_id,supplier_pn,transport_no,create_by,create_time,update_by,update_time,create_user_name,update_user_name FROM dhos_transfer WHERE id=36
      2022-07-23T06:36:34.473252Z       812 Query     commit
      2022-07-23T06:36:34.490570Z       810 Query     SELECT @@session.transaction_read_only
      2022-07-23T06:36:34.490626Z       812 Query     commit
      2022-07-23T06:36:34.492968Z       812 Query     SET autocommit=1
      2022-07-23T06:36:34.493466Z       810 Query     UPDATE dhos_transfer  SET code='TO1656319433900',
      

      从上面看前面四个是开启了四个事务,这个跟我测试的四个线程相对应

      最下面的是重点,812这个线程先执行的,但是他没有提交事务,810线程就开始执行了?什么鬼?事务不是具有原子性吗?难道是事务失效了?那也不对,事务开启提交了,说明事务没有问题,只能说四个线程并发出了问题。

    • 是不是事务太长了,执行出现了问题,于是我就将更新的语句放在最上面,先更新状态,但是还是没有解决。

  3. 看来数据库事务没有问题,那是不是我测试语句写的有问题?先让第一个之后,线程睡一会再执行也不行。

  4. 是不是用的springboot 的测试框架有问题?不支持事务,那不可能啊,事务都开启了,自己否定自己。

  5. 那还有啥问题,只能看事务, 这个时候我想到了spring 的事务机制,是不是这个的问题?确实是。

    • spring事务是利用了spring 的AOP原理实现的,简单点就是利用注解@Transactional在执行方法的前后加入事务开启和提交回滚的语句,这样就减少了我们开发程序的时候写一些重复的语句,就是动态代理吧,把你的方法包裹在里面,你认为开始执行你的方法了实际上先执行的是spring在你的语句前包裹上的语句
    • 开始包裹的就是开启事务,这个正好和mysql的日志对应了,开启了四个线程,同时开启事务
    • 然后执行你的同步synchronized 方法,这几个时候并发就会竞争锁,一个线程进入了临界区,其他线程等待,开始执行你的更新查询语句。
    • 你的语句执行完了,那就是要退出临界区了,也就释放了锁
    • 这个时候其他的线程开始竞争锁,有一个进入临界区,开始查询,于此同时第一个线程实际上还没有执行完成,因为spring还在后面包裹着一段语句,这个执行完才能结束,后面的是关键呢,就是提交事务,这就看到了mysql里面的日志执行情况了,810执行查询了,812还没有提交。导致810查询的是812未提交的数据。导致数据不一致了。及时后面提交了,也对810不起作用了。
    • 综上可以看出是spring事务机制和synchronized 同步出现了矛盾,这是问题的根本原因。

解决步骤:

  1. 明白了原因解决方案就好解决了,就是将注解事务@Transactional和synchronized 语句分开不在同一个方法中同时将事务放在同步里面,不能将同步放在事务里面

  2. 我网上找了一下看到两个方案:

    1. 第一个不 要将@Transactional和synchronized用在一个方法上,而是新建一个被synchronized修饰的方法调用被@Transactional调用的方法。这个方案有个缺陷就是不能将这两个方法放在一个service中,这样会导致你的事务不起作用

  1. 第二方式是将同步语句放在controller中,调用service方法,当然我觉得这和第一个方案基本一样

  2. 我采用的是第二中方式

        @PostMapping(value = "/finishTransfer/{id}")public synchronized R<Boolean> finishTransfer(@Validated @RequestBody TransferFinishPo transferFinishPo, @PathVariable("id") Long id, HttpServletRequest request) {if (transferFinishPo.getId() == null) {transferFinishPo.setId(id);}return R.ok(iTransferService.finishTransfer(transferFinishPo, supplierFunction.apply(request)));}
    
  3. 我的测试方法也进行了改进

     @Testvoid finishTransfer() {TransferFinishPo transferFinishPo = new TransferFinishPo();transferFinishPo.setId(36L);transferFinishPo.setTransportNo("1231231");List<AttachVo> attachVos = new ArrayList<>();AttachVo attachVo = new AttachVo();attachVos.add(attachVo);transferFinishPo.setAttaches(attachVos);new Thread(()->{System.out.println(" 启动线程1" );this.synchFinishTransfer(transferFinishPo, () -> USER);}).start();new Thread(()->{System.out.println(" 启动线程2" );this.synchFinishTransfer(transferFinishPo, () -> USER);}).start();new Thread(()->{System.out.println(" 启动线程3" );this.synchFinishTransfer(transferFinishPo, () -> USER);}).start();new Thread(()->{System.out.println(" 启动线程4" );this.synchFinishTransfer(transferFinishPo, () -> USER);}).start();try {System.in.read();} catch (IOException e) {e.printStackTrace();}}public synchronized boolean synchFinishTransfer(TransferFinishPo transferFinishPo,   Supplier<SysUser> apply) {transferService.finishTransfer(transferFinishPo, apply);return true;}
    
  4. 再来看下mysql日志,可以看到第一次只开启了一个线程,执行完成后提交完成事务后,在同一个线程里面继续执行

    2022-07-23T07:36:13.313504Z       881 Query     SET autocommit=0
    2022-07-23T07:36:13.704529Z       881 Query     SELECT id,parent_id,bom_id,purchase_bom_id,vpn_id,material_id,find_num,group_pn,hh_pn,description,mfg_name,mfg_id,mfg_pn,qty,location,is_primary,src_mode,status,del_flag,version,operate,remark,purchase_order_id,purchase_sum,price_sum,store_id,store_name,schedule_by,schedule_time,schedule_user_name,schedule_status,freeze_stock,freeze_customer_stock,freeze_pending_stock,freeze_customer_pending_stock,remain_sum,remain_by,remain_time,remain_user_name,used_sum,create_by,create_time,update_by,update_time,create_user_name,update_user_name FROM dhos_purchase_bom_detail WHERE id=2124
    2022-07-23T07:36:13.886664Z       881 Query     SELECT  id,project_id,project_name,pca_description,description,mode,pca_pn,pca_revision,customer_name,customer_id,status,del_flag,version,main_ver,second_ver,change_status,publish_ver,publish_time,bom_id,order_id,order_no,total_sum,pcba_counts,customer_counts,total_price_sum,min_total_price_sum,purchase_price_sum,store_id,store_name,finish_counts,schedule_time,schedule_status,remain_time,remain_status,schedule_user_id,create_by,create_time,update_by,update_time,create_user_name,update_user_name  FROM dhos_purchase_bom
    ==========================================中间省略了很多查询更新语句======================================================
    2022-07-23T07:36:14.624281Z       881 Query     commit
    2022-07-23T07:36:14.644727Z       881 Query     commit
    2022-07-23T07:36:14.647279Z       881 Query     SET autocommit=1
    2022-07-23T07:36:14.652270Z       881 Query     SET autocommit=0
    2022-07-23T07:36:14.660313Z       881 Query     SELECT id,code,order_id,purchase_bom_id,purchase_detail_id,material_id,stock_id,from_store_id,from_store_name,to_store_id,to_store_name,stock,pending_stock,customer_stock,customer_pending_stock,status,del_flag,version,finish_time,material_name,material_code,supplier,supplier_id,supplier_pn,transport_no,create_by,create_time,update_by,update_time,create_user_name,update_user_name FROM dhos_transfer WHERE id=36
    2022-07-23T07:36:14.678394Z       881 Query     rollback
    2022-07-23T07:36:14.683117Z       881 Query     commit
    2022-07-23T07:36:14.686371Z       881 Query     SET autocommit=1
    2022-07-23T07:36:14.690063Z       881 Query     SET autocommit=0
    2022-07-23T07:36:14.700072Z       881 Query     SELECT id,code,order_id,purchase_bom_id,purchase_detail_id,material_id,stock_id,from_store_id,from_store_name,to_store_id,to_store_name,stock,pending_stock,customer_stock,customer_pending_stock,status,del_flag,version,finish_time,material_name,material_code,supplier,supplier_id,supplier_pn,transport_no,create_by,create_time,update_by,update_time,create_user_name,update_user_name FROM dhos_transfer WHERE id=36
    2022-07-23T07:36:14.708335Z       881 Query     rollback
    2022-07-23T07:36:14.712286Z       881 Query     commit
    2022-07-23T07:36:14.716489Z       881 Query     SET autocommit=1
    2022-07-23T07:36:14.720641Z       881 Query     SET autocommit=0
    2022-07-23T07:36:14.730510Z       881 Query     SELECT id,code,order_id,purchase_bom_id,purchase_detail_id,material_id,stock_id,from_store_id,from_store_name,to_store_id,to_store_name,stock,pending_stock,customer_stock,customer_pending_stock,status,del_flag,version,finish_time,material_name,material_code,supplier,supplier_id,supplier_pn,transport_no,create_by,create_time,update_by,update_time,create_user_name,update_user_name FROM dhos_transfer WHERE id=36
    2022-07-23T07:36:14.746516Z       881 Query     rollback
    2022-07-23T07:36:14.752392Z       881 Query     commit
    2022-07-23T07:36:14.760410Z       881 Query     SET autocommit=1
    

    思维发散

    通过这个事件我还想到了,在同一个service里面一个有事务注解的方法去调一个也有事务注解的方法,第二事务注解会不起作用,这个鬼也是aop导致的,第二方法在第一个方法调用时,只是认为一个方法,他不会在第二个方法的前后加上事务,所以建议不要将两个方法放在一个service中,或者网上有说自己注入到自己 里面可以。

    终于解决了,困扰了我两天了,一度想换个方式执行,但是心里总是很纠结,由此看来还是自己对spring框架不是很熟悉导致的。

    解决的方向很重要,我开始是方向错了,走了好多弯路。

Spring事务同步synchronized一起使用引发的问题相关推荐

  1. Spring事务原理一探

    概括来讲,事务是一个由有限操作集合组成的逻辑单元.事务操作包含两个目的,数据一致以及操作隔离.数据一致是指事务提交时保证事务内的所有操作都成功完成,并且更改永久生效:事务回滚时,保证能够恢复到事务执行 ...

  2. 【技术干货】Spring事务原理一探

    本篇文章是网易云信研发工程师对Spring事务实现原理及实现的研究和总结,分享给大家,希望和大家共同探讨. 事务是一个由有限操作集合组成的逻辑单元.事务操作包含两个目的,数据一致以及操作隔离.数据一致 ...

  3. Spring事务机制

    目录 事务 数据库事务 `A C I D` 属性 数据库事务隔离级别 `Spring` 中事务传播特性 `Spring` 中事务的隔离级别 `spring aop` 原理 `spring aop` 方 ...

  4. jdbctemplate 开启事务_浅入浅出 Spring 事务传播实现原理

    本文和大家一起刨析 Spring 事务的相关源码,篇幅较长,代码片段较多,建议使用电脑阅读 本文目标 理解Spring事务管理核心接口 理解Spring事务管理的核心逻辑 理解事务的传播类型及其实现原 ...

  5. Spring→事务、隔离级别、事务传播行为、编程式事务控制、XML配置声明式事务(原始方式)、XML配置声明式事务(基于tx/aop)、@注解配置声明式事务、优势总结

    事务 Spring事务管理 不考虑隔离引发问题 隔离级别 事务传播行为 演示环境搭建 编程式事务控制 XML配置声明式事务(原始方式) XML配置声明式事务(基于tx/aop) @注解配置声明式事务 ...

  6. java 锁表后事务提交_关于synchronized锁在Spring事务中进行数据更新同步,仍出现线程安全问题...

    #1 问题描述# 最近有小伙伴在做商品抽奖活动时,在对奖品库存进行扣减,有线程安全的问题,遂加锁synchronized进行同步,但发现加锁后并没有控制住库存线程安全的问题,导致库存仍被超发. 先简单 ...

  7. 关于synchronized锁在Spring事务中进行数据更新同步,仍出现线程安全问题

    为什么80%的码农都做不了架构师?>>>    #1 问题描述# 最近有小伙伴在做商品抽奖活动时,在对奖品库存进行扣减,有线程安全的问题,遂加锁synchronized进行同步,但发 ...

  8. 在Spring事务管理下,Synchronized为啥还线程不安全?

    在synchronized 锁住方法的情况下,竟然出现了脏写 Tips 昨天本来打算是准备着一支烟 一杯咖啡 一个bug写一天的,突然我们组长跟我们说线上环境报错了, 还出现了"服务器异常, ...

  9. 说说 Spring 的事务同步管理器

    Spring 将 JDBC 的 Connection.Hibernate 的 Session 等访问数据库的连接或者会话对象统称为资源,这些资源在同一时刻是不能多线程共享的 . 为了让 DAO 或 S ...

  10. 【Spring 】Synchronized锁在Spring事务管理下,为啥还线程不安全?

    1.概述 转载:Synchronized锁在Spring事务管理下,为啥还线程不安全? 知乎问题:知乎

最新文章

  1. 28. extjs中Ext.BLANK_IMAGE_URL的作用
  2. 1vmware中的centos7配置静态变量
  3. 参加51CTO培训,华为HCNP认证考试通过啦
  4. Makefile 规则
  5. Spring MVC 生成文件类型响应
  6. bzoj千题计划181:bzoj1878: [SDOI2009]HH的项链
  7. Apache软件基金会Member陈亮:一名开源拓荒者的 Apache之旅
  8. java 获取手机系统_Android系统信息获取
  9. .net操作读取word中的图像并保存
  10. 实现APP-V服务全程跟踪(二)
  11. centos7安装rabbitmq 总结
  12. word2vec数学原理详解
  13. Azkaban安装并设置定时任务Schedule以及邮件发送接收
  14. 王爽老师汇编第三版课程设计 一
  15. 从潞晨到世界名校,实习生火热招聘中
  16. 切换盘符:使用cmd命令行 cd e: 无法切换到E盘
  17. 物料主数据 分类视图导入 BAPI_OBJCL_CREATE
  18. html6+树状下拉列表,layui+ztree 树状下拉框
  19. 神经网络 mse一直不变_卷积神经网络中十大拍案叫绝的操作
  20. 基于卷积神经网络的近红外夜间道路行人识别

热门文章

  1. 使用Map集合来做一个不同姓氏人数的统计 有一个String数组保存着10个人的姓名{“张三“,“李四“,“王二“...} 通过程序设计,把不同姓氏的姓氏和人数保存到Map集合中
  2. NH2-UiO-66,CAS:1260119-00-3
  3. vtkPolyData获取bounds点坐标
  4. 川的第一份博客——内容定义
  5. python删除第一行_python学习之删除DataFrame某一行/列内容
  6. CTU Open Contest 2019 G. Beer Mugs 异或维护奇偶性
  7. 用rand(7)构造rand(10)
  8. 在Windows Server 2019上部署Deskpool桌面云系统
  9. 灵敏度分享码显示服务器不可用,和平精英灵敏度分享码怎么使用 复制高玩主播灵敏度方法...
  10. c语言中getc函数,C语言中getc怎么用?