Spring Boot 异步结果集已关闭
Posted
技术标签:
【中文标题】Spring Boot 异步结果集已关闭【英文标题】:Spring Boot Async ResultSet is closed 【发布时间】:2020-06-24 17:01:25 【问题描述】:我正在使用 Spring Boot 实现通知,并使用 @Async
在不同的线程中通知用户。
没有这个注解,一切正常,但是当我把它放在我用来通知的方法上时,只有一个可观察的实体,观察者不会得到通知,我得到这个堆栈跟踪:
Unexpected exception occurred invoking async method: public void pt.ulisboa.tecnico.socialsoftware.tutor.notifications.NotificationServic
e.notifyObservers(package.notifications.Observable,package.notifications.domain.Notification,ppackage.user.User)
org.hibernate.exception.GenericJDBCException: could not initialize a collection: [package.course.CourseExecution.users#11]
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:47) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.loader.collection.plan.AbstractLoadPlanBasedCollectionInitializer.initialize(AbstractLoadPlanBasedCollectionInitializer.java:97) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:707) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.event.internal.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:76) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:108) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final] at org.hibernate.internal.SessionImpl.initializeCollection(SessionImpl.java:2145) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:589) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:264) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.collection.internal.PersistentSet.toString(PersistentSet.java:327) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at java.base/java.lang.String.valueOf(String.java:2951) ~[na:na]
at java.base/java.io.PrintStream.println(PrintStream.java:897) ~[na:na]
at package.course.CourseExecution.Notify(CourseExecution.java:210) ~[classes/:na]
at package.notifications.NotificationService.notifyObservers(NotificationService.java:82) ~[classes/:na]
at package.notifications.NotificationService$$FastClassBySpringCGLIB$$d43e740c.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:366) ~[spring-tx-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99) ~[spring-tx-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE] at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: org.postgresql.util.PSQLException: This statement has been closed.
at org.postgresql.jdbc.PgStatement.checkClosed(PgStatement.java:705) ~[postgresql-42.2.8.jar:42.2.8]
at org.postgresql.jdbc.PgPreparedStatement.setInt(PgPreparedStatement.java:270) ~[postgresql-42.2.8.jar:42.2.8]
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.setInt(HikariProxyPreparedStatement.java) ~[HikariCP-3.4.1.jar:na]
at org.hibernate.type.descriptor.sql.IntegerTypeDescriptor$1.doBind(IntegerTypeDescriptor.java:46) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final] at org.hibernate.type.descriptor.sql.BasicBinder.bind(BasicBinder.java:73) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final] at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:276) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final] at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:271) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final] at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.bindPositionalParameters(AbstractLoadPlanBasedLoader.java:320) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.bindParameterValues(AbstractLoadPlanBasedLoader.java:291) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.prepareQueryStatement(AbstractLoadPlanBasedLoader.java:210) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeQueryStatement(AbstractLoadPlanBasedLoader.java:162) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:104) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
at org.hibernate.loader.collection.plan.AbstractLoadPlanBasedCollectionInitializer.initialize(AbstractLoadPlanBasedCollectionInitializer.java:87) ~[hibernate-core-5.4.9.Final.jar:5.4.9.Final]
... 25 common frames omitted
Caused by
异常消息总是相同的,另一个,不总是
trace中提到的方法有:
通知程序(异步方法) @Async("notifyExecutor") // Even with the default executor, the error occurs
@Retryable(
value = SQLException.class ,
backoff = @Backoff(delay = 5000))
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void notifyObservers(Observable observable, Notification notification, User exclude)
observable.Notify(notification, exclude);
方法调用通知器(需要的服务是@Autowired
)
@Retryable(value = SQLException.class , backoff = @Backoff(delay = 5000))
@Transactional(isolation = Isolation.REPEATABLE_READ)
public AnnouncementDto createAnnouncement(AnnouncementDto announcementDto)
checkIfConsistentAnnouncement(announcementDto);
User user = getTeacher(announcementDto.getUserId());
CourseExecution courseExecution = getCourseExecution(announcementDto.getCourseExecutionId());
if (announcementDto.getCreationDate() == null)
announcementDto
.setCreationDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
// Announcement has a CourseExecution as attribute
Announcement announcement = new Announcement(user, courseExecution, announcementDto);
entityManager.persist(announcement);
NotificationDto notification = NotificationsCreation.create(ADD_ANNOUNCEMENT_TITLE,
List.of(announcement.getUser().getName()), ADD_ANNOUNCEMENT_CONTENT,
List.of(announcement.getTitle(), user.getName()), Notification.Type.ANNOUNCEMENT);
this.notify(courseExecution, notification, user);
return new AnnouncementDto(announcement);
// Calls the Async method
private void notify(CourseExecution course, NotificationDto notification, User user)
notificationService.notifyObservers(course, notificationService.createNotification(notification), user);
发生错误的方法(仅在访问元素时)
@Override
public void Notify(Notification notification, User user)
for (Observer observer : this.users) // Error occurs here, doesn't get inside the loop
if (((User) observer).getId() == user.getId())
continue;
observer.update(this, notification);
我已经看到了这些问题的答案,但它们不适用于这里。我认为非常奇怪的是,这只发生在Announcement
而不是其他可观察对象。因为我想要CourseExecution
中所有观察者的公告,所以我将CourseExecution
设为可观察,当有新公告时,我们会通知所有CourseExecution
观察者。
有人可以帮帮我吗?
【问题讨论】:
你指向了for循环声明,它使用了this.users
字段,这在sn-ps代码中是不存在的。能否请您描述有关此字段的更多信息?
它是一组用户,有一个 int 作为 Id,有一组 CourseExecution
(有问题的可观察对象)和其他一些这里不需要的东西,实现方法 @987654334 @ 将通知添加到列表中
您的DataSource
配置是什么样的?
【参考方案1】:
我敢打赌这是数据库连接和异步的问题。数据库连接保存在本地线程中,并且异步方法不会在同一个线程中执行,并且由于并行执行的可能性不能真正共享相同的连接/事务。您是否尝试过传播需要新的异步方法?
更有经验的成员可能能够澄清异步和未完成数据库连接的交互,但这可能是罪魁祸首。
【讨论】:
问题是这只发生在这个特定的 observable 上,其他的都可以正常工作 您观察到的唯一问题在这里,但这并不意味着它是唯一的问题。一旦你有真正的并发负载,连接就不能被多个线程共享而不会中断。 嗯,我明白了...所以,我需要以某种方式为每个线程建立一个连接? 在大多数情况下,事务注释管理这个,但不清楚它是否与异步正确集成。为了简单起见,设置传播以在您的异步方法上打开新事务应该可以保护您。 如果没有事务注释,错误是一样的,而对于其他 observables,行为是一样的(一切正常)【参考方案2】:我想 Spring tracker 上的某个人可能能够更详细地回答这个问题。
正如Deadron 在他的回答中提到的,这似乎是 TX 管理的问题。 @Async
使得控制流立即返回到调用函数,这(从事务管理器的角度来看)意味着语句成功执行并且可能发生提交。 标记为 Async 的方法与调用者的事务上下文不同,即使标记为 propagation=REQUIRED
(只会返回一个新事务)
@Transactional(isolation = Isolation.REPEATABLE_READ)
尝试检查现有事务(因为默认传播级别是REQUIRED
,如果现有事务存在则支持现有事务,如果不存在则创建新事务)。此事务在方法调用时确实存在,但在 @Async
方法必须在事务中执行任何工作时不存在(即绑定参数,如您的堆栈跟踪中所示)。此时,无法访问该原始事务,因为它是原始线程的本地事务。
根据this的回答:
在 REPEATABLE READ 下,第二个 SELECT 保证至少显示从第一个 SELECT 返回的行不变。并发事务可以在那一分钟内添加新行,但不能删除或更改现有行。
另外,下面一段Isolation
Javadocs:
/**
* A constant indicating that dirty reads and non-repeatable reads are
* prevented; phantom reads can occur. This level prohibits a transaction
* from reading a row with uncommitted changes in it, and it also prohibits
* the situation where one transaction reads a row, a second transaction
* alters the row, and the first transaction rereads the row, getting
* different values the second time (a "non-repeatable read").
* @see java.sql.Connection#TRANSACTION_REPEATABLE_READ
*/
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
考虑到上述@Transactional
和@Async
交互,将有助于解释这种行为,特别是如果您的Observable
持有对用户的引用,而用户又对该行有某种引用(即新事务是为@Async
方法创建的,但原始事务尚未完全提交)。您可以尝试以下方法:
-
尝试将传播级别设置为
MANDATORY
。 notifyObservers
方法是否被成功调用,或者您是否收到事务未激活的错误?如果你得到一个错误,那将确认上一段。
如果您确实收到错误,当您将传播级别设置为 REQUIRES_NEW
时,行为是否会改变?
如果将隔离级别设置为默认级别会怎样?
如果您在返回之前人为地在 createAnnouncement
方法中引入减速,会发生什么情况?例如。 Thread.sleep(5000)
,同时什么都不改变?错误是否仍然存在?如果不是,那将证明上一段与REPEATABLE_READ
一起提到的时间问题。
【讨论】:
【参考方案3】:问题是使用 Async 时事务没有正确传播。
它应该可以正常工作:
@Async("notifyExecutor")
@Retryable(
value = SQLException.class ,
backoff = @Backoff(delay = 5000))
@Transactional(propagation = Propagation.REQUIRES_NEW) // Make separate transaction
public void notifyObservers(Observable observable, Notification notification, User exclude)
observable.Notify(notification, exclude);
【讨论】:
【参考方案4】:由 Spring @Transactionnal 管理的事务不跨越多个线程
你在这里面临的问题是:
CourseExecution
有一个惰性未初始化集合 (users
)
事务包装 createAnnouncement
在 notificationService.notifyObservers
执行之前结束(因为它是异步的)
与CourseExecution
实例关联的连接/实体管理器在createAnnouncement
事务结束时关闭
当休眠尝试初始化用户集合时:连接/语句被关闭
在线程之间传递托管实体通常是个坏主意,尤其是当您想在不同线程中延迟加载集合时
【讨论】:
以上是关于Spring Boot 异步结果集已关闭的主要内容,如果未能解决你的问题,请参考以下文章
Jsp--java.sqlsqlException结果集已耗尽
oracle数据库java.sql.SQLException: 结果集已耗尽,总是跳不出while(rs.next())循环,请求高手帮忙解决!
java连接Oracle数据库,从ResultSet中提取数据出现java.sql.sqlException结果集已耗尽