I learned that it is possible to execute repository queries asynchronously using Spring's asynchronous processing, so I investigated how to implement it. This article summarizes a simple implementation of asynchronous queries and the results of their operation.
environment
reference
Create a view that takes a long time to search to make it easier to check the operation.
CREATE OR REPLACE VIEW async_test_view (
id
, sleep
, create_at ) AS
SELECT MD5(UUID()) AS id
, SLEEP(10) AS sleep
, NOW() AS create_at
;
Searching for this view will take about 10 seconds to return results.
> select * from pseudo_delay_view;
+----------------------------------+-------+---------------------+
| id | sleep | create_at |
+----------------------------------+-------+---------------------+
| da863db6ff1b064ebff03f00efdd224b | 0 | 2017-12-23 17:27:08 |
+----------------------------------+-------+---------------------+
1 row in set (10.00 sec)
@SpringBootApplication
// 1
@EnableAsync
public class DemoGradleApplication {
public static void main(String[] args) {
SpringApplication.run(DemoGradleApplication.class, args);
}
// 2
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 3
executor.setQueueCapacity(2);
// 4
executor.setCorePoolSize(2);
executor.setMaxPoolSize(3);
// 5
executor.setKeepAliveSeconds(10);
executor.afterPropertiesSet();
return executor;
}
}
If the number of requests exceeds MaxPoolSize + QueueCapacity, an exception called RejectedExecutionException will be thrown by default.
If you want to execute arbitrary processing for reject processing when the request reaches the upper limit, pass the class that implements the RejectedExecutionHandler interface to ThreadPoolTaskExecutor as shown below. If not specified, the default is ThreadPoolExecutor # AbortPolicy. (Throws a RejectedExecutionException exception)
executor.setRejectedExecutionHandler((r,e) -> {
//Implement any process you want to execute
throw new RejectedExecutionException("Task:" + r.toString() + " rejected from " + e.toString());
});
An implementation of the entity that corresponds to the view. There are no special notes.
@Entity
@Table(name="pseudo_delay_view")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PseudoDelay implements Serializable {
private static final long serialVersionUID = -9169553858944816379L;
@Id
private String id;
@Column(name="sleep", nullable=false)
private Integer sleep;
@Column(name="create_at", nullable=false)
private LocalDateTime createAt;
}
An implementation of the repository that issues queries. Implement to execute the query asynchronously. All you have to do is add the Async annotation to the method.
public interface PseudoDelayRepository extends JpaRepository<PseudoDelay, String> {
// 1
@Async
@Query("SELECT p FROM PseudoDelay AS p")
CompletableFuture<PseudoDelay> findAsync();
// 2
@Query("SELECT p FROM PseudoDelay AS p")
PseudoDelay findSync();
}
In this example, CompletableFuture is used, but in addition to this, Future and Spring ListenableFuture can be used. For details, see Spring Data JPA --Reference Documentation --3.4.7. Async query results In can be confirmed.
The implementation of the service class for confirmation is as follows.
@Service
@Slf4j
public class AsyncTestServiceImpl implements AsyncTestService {
private PseudoDelayRepository repository;
public AsyncTestServiceImpl(PseudoDelayRepository repository) {
this.repository = repository;
}
// 1
@Transactional(readOnly = true)
@Override
public PseudoDelay async() {
log.debug("start async");
CompletableFuture<PseudoDelay> future = repository.findAsync();
//Do something while running the query asynchronously
log.debug("execute somethings");
PseudoDelay result = null;
try {
//Receive the query execution result
result = future.thenApply(res -> {
log.debug("async result : {}", res);
return res;
})
.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
log.debug("end async");
return result;
}
// 2
@Transactional(readOnly = true)
@Override
public PseudoDelay sync() {
log.debug("start sync");
PseudoDelay result = repository.findSync();
log.debug("sync result : {}", result);
log.debug("end sync");
return result;
}
}
You can see that other processing ("*** execute somethings ***") is being executed immediately after issuing the query.
2017-12-23 19:55:36.194 DEBUG 5304 --- [nio-9000-exec-4] : start async
2017-12-23 19:55:36.195 DEBUG 5304 --- [nio-9000-exec-4] : *** execute somethings ***
2017-12-23 19:55:46.198 DEBUG 5304 --- [ taskExecutor-2] : async result : PseudoDelay(id=9904388341a9d8dbdfb230fb5b675224, sleep=0, createAt=2017-12-23T19:55:36)
2017-12-23 19:55:46.199 DEBUG 5304 --- [nio-9000-exec-4] : end async
You can see that it is blocked until the result of the query is returned.
2017-12-23 19:57:49.465 DEBUG 5304 --- [nio-9000-exec-8] : start sync
2017-12-23 19:57:59.467 DEBUG 5304 --- [nio-9000-exec-8] : sync result : PseudoDelay(id=3a19a242c0207cd9ddad551ec2ccae66, sleep=0, createAt=2017-12-23T19:57:49)
2017-12-23 19:57:59.467 DEBUG 5304 --- [nio-9000-exec-8] : end sync
You can specify the timeout. If it does not complete within the specified time, a TimeoutException exception will be thrown.
result = future.get(5, TimeUnit.SECONDS);
We set a timeout for the transaction and looked at what would happen if a query that took longer than that timeout was executed asynchronously. This time I set the transaction timeout to 5 seconds and ran a query that took 10 seconds both asynchronously and synchronously as in the example above.
@Transactional(readOnly = true, timeout = 5)
** For asynchronous queries **
No exception was raised when the timeout was exceeded, and the expected result was not achieved. I do not know the cause, but it seems that transaction management is out of order if it is executed in another thread within the transaction. I checked if there is any description in the official document, but I have not checked it completely. For the time being, there was Spring @Async and transaction management in such an article.
** For synchronous queries **
I think it depends on the JDBC driver implementation, but in the case of MySQL, the following error will occur.
2017-12-23 20:17:18.297 ERROR 2260 --- [nio-9000-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : Statement cancelled due to timeout or client request
2017-12-23 20:17:18.319 ERROR 2260 --- [nio-9000-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/app] threw exception [Request processing failed; nested exception is org.springframework.orm.jpa.JpaSystemException: could not extract ResultSet; nested exception is org.hibernate.exception.GenericJDBCException: could not extract ResultSet] with root cause
com.mysql.jdbc.exceptions.MySQLTimeoutException: Statement cancelled due to timeout or client request
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2827) ~[mysql-connector-java-5.1.44.jar:5.1.44]
/...abridgement
Recommended Posts