百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

多线程下 @Async 注解导致 Spring 中的事务失效,该怎么办?

mhr18 2024-10-08 04:37 38 浏览 0 评论

本文主要解决:Spring @Async 注解多线程导致Spring的注解@Transactional失效问题!

问题:多线程为什么会导致事务注解@Transactional失效

实现AOP的方法有动态代理、编译期,类加载期织入等等,Spring实现AOP的方法则就是利用了动态代理机制,正因如此,才会导致某些情况下@Async@Transactional不生效。

spring多线程的使用:

@Async注解使用如下

@EnableAsync //添加此注解开启异步调用(可用在配置类上,也可在启动类上标注)
public class ProviderApplication{
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}

线程池配置采用自定义线程池配置类即可例如:

@Configuration
@EnableAsync
public class TaskPoolConfig {
    @Autowired
    private ThreadPoolProperties threadPoolProperties;
    
    public final static String TASK_EXECUTOR="taskExecutor";

    @Bean(TASK_EXECUTOR)
    public Executor taskExecutor(){
        //使用VisiableThreadPoolTaskExecutor 监控线程池清空
        ThreadPoolTaskExecutor taskExecutor=new VisiableThreadPoolTaskExecutor();
        //配置核心线程数
        taskExecutor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
        //配置最大线程数
        taskExecutor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
        //配置队列大小
        taskExecutor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
        //配置线程池中的线程的名称前缀
        taskExecutor.setThreadNamePrefix(threadPoolProperties.getThreadNamePrefix());
        //配置非核心线程超时时间
        taskExecutor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 执行初始化
        taskExecutor.initialize();
        return taskExecutor;
    }
}

yml配置如下:

executor:
  corePoolSize: 20
  maxPoolSize: 100
  queueCapacity: 20
  keepAliveSeconds: 60
  threadNamePrefix: XCExecutor-

配置类中采用的 ThreadPoolProperties 类,是读取spring的yml配置文件获取:

@ConfigurationProperties(value = "executor")
@Component
public class ThreadPoolProperties {

    /**
     * 核心线程数量
     */
    private Integer corePoolSize;
    /**
     * 当核心线程都在跑任务,还有多余的任务会存到此处
     */
    private Integer maxPoolSize;
    /**
     * 如果queueCapacity存满了,还有任务就会启动更多的线程,直到线程数达到maxPoolSize。如果还有任务,则根据拒绝策略进行处理
     */
    private Integer queueCapacity;
    /**
     * 非核心线程的超时时长,超长后会被回收
     */
    private Integer keepAliveSeconds;
    /**
     * 线程名称前缀
     */
    private String threadNamePrefix;

    public Integer getCorePoolSize() {
        return corePoolSize;
    }

    public void setCorePoolSize(Integer corePoolSize) {
        this.corePoolSize = corePoolSize;
    }

    public Integer getMaxPoolSize() {
        return maxPoolSize;
    }

    public void setMaxPoolSize(Integer maxPoolSize) {
        this.maxPoolSize = maxPoolSize;
    }

    public Integer getQueueCapacity() {
        return queueCapacity;
    }

    public void setQueueCapacity(Integer queueCapacity) {
        this.queueCapacity = queueCapacity;
    }

    public Integer getKeepAliveSeconds() {
        return keepAliveSeconds;
    }

    public void setKeepAliveSeconds(Integer keepAliveSeconds) {
        this.keepAliveSeconds = keepAliveSeconds;
    }

    public String getThreadNamePrefix() {
        return threadNamePrefix;
    }

    public void setThreadNamePrefix(String threadNamePrefix) {
        this.threadNamePrefix = threadNamePrefix;
    }
}

配置类中的VisiableThreadPoolTaskExecutor类是负责打印线程的线程池运行状况打印,代码如下:

public class VisiableThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    private final static Logger logger= LoggerFactory.getLogger(VisiableThreadPoolTaskExecutor.class);

    private void showThreadPoolInfo(String prefix){
        ThreadPoolExecutor executor=getThreadPoolExecutor();

        if(null==executor){
            return;
        }
        // @TODO taskCount 任务总数 completedTaskCount 已完成数 activeCount 活跃线程数 queueSize 队列大小
        logger.info("{}, {},taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]",
                this.getThreadNamePrefix(),
                prefix,
                executor.getTaskCount(),
                executor.getCompletedTaskCount(),
                executor.getActiveCount(),
                executor.getQueue().size());

    }

    @Override
    public void execute(Runnable task) {
        showThreadPoolInfo("1. do execute");
        super.execute(task);
    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        showThreadPoolInfo("2. do execute");
        super.execute(task, startTimeout);
    }

    @Override
    public Future<?> submit(Runnable task) {
        showThreadPoolInfo("1. do submit");
        return super.submit(task);
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        showThreadPoolInfo("2. do submit");
        return super.submit(task);
    }

    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        showThreadPoolInfo("1. do submitListenable");
        return super.submitListenable(task);
    }

    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        showThreadPoolInfo("2. do submitListenable");
        return super.submitListenable(task);
    }
}

当任务达到量多,单线程情况处理较慢可采用多线程的方式提高效率,且任务不需要即时获取结果(调用第三方接口,等api),使用只需要将@Async标注在需要多线程执行的方法上,例如:

@Component
public class ThreadTask{
 //注解标注中指向使用哪个线程池
 @Async(TaskPoolConfig.TASK_EXECUTOR)
 public Future<String> startTask(String id) {
  //Future类是异步线程返回的执行结果,本文暂不做过多介绍
        try {
            //执行业务代码
        } catch (Exception e) {
            e.printStackTrace();
            return new AsyncResult<>("错误:"+e.getMessage());
        }
        return null;
    }
}

多线程的异步调用未成功大致分为以下三种问题:

  1. 没有在@SpringBootApplication启动类当中添加注解@EnableAsync注解。
  2. 异步方法使用注解@Async的返回值只能为void或者Future。
  3. 没有走Spring的代理类。因为@Transactional@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。

接下来查看详细测试过程:

直接添加@Transactional注解

可以很明显的看见,我的代码虽然报错了,但是事务依旧未生效,接下来尝试手动提交事务

可以看见,手动提交事务就可以使spring的事务管理器生效。这是为什么呢,抱着知其然知其所以然的心态我们再往源码层面探究:

当Spring发现@Transactional或者@Async时,会自动生成一个ProxyObject,如:

此时调用Class.transactionTask会调用ProxyClass产生事务操作。

然而当Class里的一个非事务方法调用了事务方法,ProxyClass是这样的:

到这里应该可以看明白了,如果调用了noTransactionTask方法,最终会调用到Class.transactionTask,而这个方法是不带有任何Transactional的信息的,也就是@Transactional根本没有生效哦。

简单来说就是:同一个类内这样调用的话,只有第一次调用了动态代理生成的ProxyClass,之后一直用的是不带任何切面信息的方法本身。

TransactionDefintion类常量解析:

事务的传播级别:

  • 事务传播级别1:当前如果有事务,Spring就会使用该事务;否则会开始一个新事务;(这也是默认设置和定义)
int PROPAGATION_REQUIRED = 0;
  • 事务传播级别2:如果有事务,Spring就会使用该事务;否则不会开始一个新事务
int PROPAGATION_SUPPORTS = 1;
  • 事务传播级别3:当前如果有事务,Spring就会使用该事务;否则会因为没有事务而抛出异常
int PROPAGATION_MANDATORY = 2;    
  • 事务传播级别4:总是要开启一个新事务。如果当前已经有事务,则将已有事务挂起
int PROPAGATION_REQUIRES_NEW = 3;
  • 事务传播级别5:代码总是在非事务环境下执行,如果当前有事务,则将已有事务挂起,再执行代码,之后恢复事务
int PROPAGATION_NOT_SUPPORTED = 4;
  • 事务传播级别6:绝对不允许代码在事务中执行。如果当前运行环境有事务存在,则直接抛出异常,结束运行
int PROPAGATION_NEVER = 5;
  • 事务传播级别7:该级别支持嵌套事务执行。如果没有父事务存在,那么执行情况与PROPAGATION_REQUIRED一样;典型的应用是批量数据入库,开启父事务对一批数据入库,而对于每条入库的数据都有一个子事务对应,那么当所有的子事务成功,父事务提交,才算成功,否则,就都失败
int PROPAGATION_NESTED = 6;

事务的隔离级别:

  • 事务隔离级别1:默认的隔离级别,同数据库一样的,如果不做特别设置,mysql默认的是可重复读,而oracle默认的是读提交
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;   
  • 事务隔离级别2:读未提交,即一个事务可以读取另外一个事务中未提交的数据,即脏读数据存在,性能最好,但是没啥用。
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
  • 事务隔离级别3:读提交,即一个事务只能读取到另一个事务提交后的数据,oracle数据库默认隔离级别;存在不可重复读问题,即交叉事务出现,A事务两次读取数据可能会读到B事务提交的修改后的数据,即在同一个事务中读到了不同的数据,也叫不可重复读
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;   
  • 事务隔离级别4:可重复读,即一个事务只能读取到在次事务之前提交的数据,而之后提交不能读取到,不管对方的事务是否提交都读取不到,mysql默认的隔离级别。此隔离级别有可能会遇到幻读现象,但是mysql

基于innodb引擎实现的数据库已经通过多版本控制解决了此问题,所以可以不考虑了。

int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE; 
  • 事务隔离级别5:序列化读,每次都是全表锁,性能最差,安全性最高,一般场景不适用,也没有这个必要。

在开发的过程中,用事务最多的方式是通过注解@Transaction来完成的,虽然大多数的业务场景都可以在这一个注解下完成即可。

但是为了适应某些特别的场景比方说只读方法的优化等,通过对@Transaction来添加参数来完成我们想要的事务传播特性和隔离级别,以及是否只对某些异常类做回滚,是否只读方法等。

TransactionStatus接口详解:

  • 是否是一个新的事物 boolean isNewTransaction();
  • 判断是否有回滚点 boolean hasSavepoint();
  • 将一个事务标识为不可提交的。在调用完setRollbackOnly()后只能被回滚
  • 在大多数情况下,事务管理器会检测到这一点,在它发现事务要提交时会立刻结束事务。
  • 调用完setRollbackOnly()后,数数据库可以继续执行select,但不允许执行update语句,因为事务只可以进行读取操作,任何在这里插入代码片修改都不会被提交。
  • void setRollbackOnly(); boolean isRollbackOnly(); @Override void flush(); 判断事物是否已经完成 boolean isCompleted();
  1. 创建回滚点 Object createSavepoint() throws TransactionException;
  2. 回滚到回滚点 void rollbackToSavepoint(Object savepoint) throws TransactionException;
  3. 释放回滚点 void releaseSavepoint(Object savepoint) throws TransactionException;

总结

在多线程中spring的事务管理器注解@Transactional会失效,因此@Async@Transactional不可使用在同一个方法上;

如在多线程并且多数据源的情况下使用事务,采用注入指定数据源的方式和手动提交事务及回滚事务;

多数据源情况下,线程使用的数据源来自主线程采用的数据;

原文链接:https://mp.weixin.qq.com/s/gOxps7l3Ma_qm7CV4XwGzA

相关推荐

甲骨文签署多项大型云协议,其一未来可贡献超300亿美元年收入

IT之家7月1日消息,根据甲骨文Oracle当地时间6月30日向美国证券交易委员会(SEC)递交的FORM8-K文件,该企业在始于2025年6月1日的202...

甲骨文获TEMU巨额合同,后者大部分基础设施将迁移至Oracle云

IT之家6月23日消息,Oracle甲骨文创始人、董事长兼首席技术官LarryEllison(拉里埃里森)在本月早些时候的2025财年第四财季和全财年财报电话会议上表示,Oracle...

Spring Boot 自定义数据源设置,这些坑你踩过吗?

你在使用SpringBoot进行后端开发的过程中,是不是也遇到过这样的问题:项目上线后,数据库连接总是不稳定,偶尔还会出现数据读取缓慢的情况,严重影响了用户体验。经过排查,发现很大一部分原因竟然...

一个开箱即用的代码生成器(一个开箱即用的代码生成器是什么)

今天给大家推荐一个好用的代码生成器,名为renren-generator,该项目附带前端页面,可以很方便的选择我们所需要生成代码的表。首先我们通过git工具克隆下来代码(地址见文末),导入idea。...

低代码建模平台-数据挖掘平台(低代码平台的实现方式)

现在来看一下数据连接。·这里是管理数据连接的空间,点击这里可以新增一个数据连接。·输入连接名称,然后输入url,是通过gdbc的方式去连接的数据库,目前是支持mysql、oracle以及国产数据库达梦...

navicat 17.2.7连接oracle数据库提示加载oracle库失败

系统:macOS15.5navicat版本:navicatpremiumlite17.2.7连接oracle测试报错:加载oracle库失败【解决办法】:放达里面找到程序,显示简介里面勾选“使...

开源“Windows”ReactOS更新:支持全屏应用

IT之家6月17日消息,ReactOS团队昨日(6月16日)在X平台发布系列推文,公布了该系统的最新进展,包括升级Explorer组件,支持全屏应用,从Wine项目引入了...

SSL 推出采用全模拟内置混音技术的模拟调音台Oracle

英国调音台传奇品牌SolidStateLogic宣布推出Oracle——一款采用全模拟内置混音技术的调音台,在紧凑的AWS尺寸机箱内集成了大型调音台的功能。该调音台提供24输入和...

47道网络工程师常见面试题,看看有没有你不会的!

你们好,我的网工朋友。网络工程师面试的时候,都会被问到什么?这个问题其实很泛,一般来说,你肯定要先看明白岗位需求写的是什么。基本上都是围绕公司需要的业务去问的。但不可否认的是,那些最基础的概念,多少也...

汉得信息:发布EBS系统安装启用JWS的高效解决方案

e公司讯,从汉得信息获悉,近日,微软官方宣布InternetExplorer桌面应用程序将于2022年6月15日正式停用。目前大部分客户都是使用IE浏览器打开EBS的Form界面,IE停用后,只能使...

36.9K star ! 推荐一个酷炫低代码开发平台!功能太强!

前言最近在逛github,看看能不能搜罗到一些对自己有帮助的开源软件。不经意间看到一个高star的java开源项目:jeecg-boot。进入在线演示版一看,感叹实在是太牛了!此开源项目不管是给来学习...

Linux新手入门系列:Linux下jdk安装配置

本系列文章是把作者刚接触和学习Linux时候的实操记录分享出来,内容主要包括Linux入门的一些理论概念知识、Web程序、mysql数据库的简单安装部署,希望能够帮到一些初学者,少走一些弯路。注意:L...

手把手教你在嵌入式设备中使用SQLite3

摘要:数据库是用来存储和管理数据的专用软件,使得管理数据更加安全,方便和高效。数据库对数据的管理的基本单位是表(table),在嵌入式linux中有时候它也需要用到数据库,听起来好难,其实就是几个函数...

JAVA语言基础(java语言基础知识)

一、计算机的基本概念什么是计算机?计算机(Computer)全称:电子计算机,俗称电脑。是一种能够按照程序运行、自动高速处理海量数据的现代化智能电子设备。由硬件和软件组成、没有安装过任何软件的计算机称...

再见 Navicat!一款开源的 Web 数据库管理工具!

大家好,我是Java陈序员。在日常的开发工作中,常常需要与各种数据库打交道。而为了提高工作效率,常常会使用一些可视化工具进行操作数据库。今天,给大家介绍一款开源的数据库管理工具,无需下载安装软件,基...

取消回复欢迎 发表评论: