如何在Spring Boot中正确实现@Async以确保线程安全?

huangapple go评论55阅读模式
英文:

How to implement @Async in spring boot correctly in term of thread safety?

问题

在Spring Boot中,当Web发送HTTP请求到一个REST控制器时,REST控制器可能会通过应用程序的不同层(例如服务层、存储库层)来转发请求。Spring保证这个过程是线程安全的。

由于我对多线程不太熟悉,我有以下问题。

我有一个特定的请求,其中服务层需要针对每个请求执行一些计算。为了不让用户等待,我创建了一个任务服务,并使用@Async注解标记了一个方法。

@Service
public class MyService {
    
    @Autowired
    private MyTask myTask;
    
    @Async("taskExecutor")    
    public void launchCalc(Integer myId) {        
        ...                
        myTask.execute(myId);            
    }        
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized void execute(Integer myId) {                                  
    ...
    someCalc(myId);                   
    ...
}

我能够在日志中看到像"TaskExecutor-6"、"TaskExecutor-8"等的条目,这意味着Spring每次都从线程池分配线程。

但是我遇到了以下错误:

[TaskExecutor-2] [36mo.h.engine.jdbc.spi.SqlExceptionHelper SQL Error: 1213, SQLState: 40001
[TaskExecutor-2] [36mo.h.engine.jdbc.spi.SqlExceptionHelper Deadlock found when trying to get lock; try restarting transaction

org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement

因此,我意识到多个线程在竞争资源,所以我将主方法标记为同步方法。所有问题都消失了,但现在用户不仅要等待(实际上并不需要等待,因为我转发到不同的页面,但他将无法看到计算的结果),而且还要等待其他用户完成他们的数据计算,这导致了可用性问题。

在这种情况下,使用@Async的好处很小,有没有更好的方法来处理这个问题?

英文:

In spring boot when the web send a http request to a rest controller, the rest controller then may forward the request through the layer/s of the application (service, repository).
This process is guaranteed to be thread safe by spring.

As I'm quite new to multithreading, I came with the following question.

I have a specific request where the service layer is required to execute some calculation per a request. in order to not holding the user, I created a task service with method annoated with @Async.

@Service
public class MyService {
			
	@Autowired
	private MyTask myTask;
			
	@Async("taskExecutor")	
	public void launchCalc(Integer myId) {		
		...				
		myTask.execute(myId);			
	}		
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized void execute(Integer myId) {				                                  
	...
	someCalc(myId);				               
	...
}

I'm able to see in the log entries like: TaskExecutor-6 TaskExecutor-8 etc which means spring allocating threads from the pool each time.

What I'm getting is errors such as the following:

    [ TaskExecutor-2][36mo.h.engine.jdbc.spi.SqlExceptionHelper SQL Error: 1213, SQLState: 40001
 [TaskExecutor-2][36mo.h.engine.jdbc.spi.SqlExceptionHelper Deadlock found when trying to get lock; try restarting transaction

org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement

So, I realized that multiple thread compete on resources so what I did is marking the main method
with syncronized.
All problems disappeared but now user will have to wait (not really wait becuase I forward to different page but he won't be able to see the results of the calc) not only to finished his data calculation but also others which cause a usabilty problem.

The benfit of using @Async is minimal in this case, is there a way to do it better?

答案1

得分: 0

经过反复多次运行几个小时后,我成功地分离出一些方法,并找到了以下内容。

@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized void execute(Integer myId) {                                                  
    ...
    someCalc(myId);                            
    ...
}

方法execute是唯一带有@Transactional注解的方法。
someCalc(myId)在同一类中调用其他方法,这意味着根据定义,这些方法使用相同的事务或正在使用相同的事务。

现在,第一个方法之一正在执行repository.findById并将实体保存到类的成员变量中。我认为这是主要问题所在。

在调试时,我看到实体的entityId与子实体的entityId不匹配。因此,看起来好像两个线程进入了一个方法,并使用不同的状态。

为了解决这些错误,我在调用的方法中(每个方法中)调用了repository.findById,并在适用时延迟加载了子实体。主要方法仅传递entityId

尽管我将此作为解决方案发布,但必须说明我并不自信,因为两个线程可以进入相同的方法,其中一个将根据id加载实体,而另一个可能会使用此实体。

测试仅包括20次循环。

英文:

After spending hours on running again and again, I was able to isolate some
methods and found the following.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized void execute(Integer myId) {                                                  
    ...
    someCalc(myId);                            
    ...
}

The method execute is the only method which has the annotation @Transactional.
someCalc(myId) is calling other methods within the same class which means by definition the same transaction applied or being used by those methods.

Now, one of the first method is doing repository.findById and save the entity to a member of the class. This was the main problem I think.

When debugging I saw that the entityId does not matches the entityId
in the children. So it looks like two threads come to a method and use different states.

In order to solve these errors what I did is calling repository.findById on the called methods (in each one of them) and load lazily the children when applicable.
The main method transfer the entityId only.

While I'm posting this as a solution I must say that I am not confident as two
threads can come to the same method one will load the entity by id and the other might use this entity.

The test is done by a loop of only 20.

huangapple
  • 本文由 发表于 2023年6月12日 14:29:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76454077.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定