J'ai appris qu'il est possible d'exécuter des requêtes de référentiel de manière asynchrone en utilisant le traitement asynchrone de Spring, j'ai donc étudié comment l'implémenter. Cet article résume une implémentation simple des requêtes asynchrones et les résultats de leur opération.
environnement
référence
Créez une vue dont la recherche est longue pour faciliter la vérification de l'opération.
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
;
La recherche dans cette vue prendra environ 10 secondes pour renvoyer les résultats.
> 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;
}
}
Si le nombre de demandes dépasse MaxPoolSize + QueueCapacity, une exception appelée RejectedExecutionException sera levée par défaut.
Si vous souhaitez exécuter un traitement arbitraire pour le traitement du rejet lorsque la demande atteint la limite supérieure, transmettez la classe qui implémente l'interface RejectedExecutionHandler à ThreadPoolTaskExecutor comme indiqué ci-dessous. S'il n'est pas spécifié, la valeur par défaut est ThreadPoolExecutor # AbortPolicy. (Lève une exception RejectedExecutionException)
executor.setRejectedExecutionHandler((r,e) -> {
//Implémentez tout processus que vous souhaitez exécuter
throw new RejectedExecutionException("Task:" + r.toString() + " rejected from " + e.toString());
});
Une implémentation de l'entité qui correspond à la vue. Il n'y a pas de notes spéciales.
@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;
}
Une implémentation du référentiel qui émet la requête. Implémente pour exécuter des requêtes de manière asynchrone. Tout ce que vous avez à faire est d'ajouter l'annotation Async à la méthode.
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();
}
Dans cet exemple, CompletableFuture est utilisé, mais en plus de cela, Future et Spring ListenableFuture peuvent être utilisés. Pour plus de détails, voir Spring Data JPA - Documentation de référence - 3.4.7. Résultats de la requête Async Dans peut être confirmé.
La mise en œuvre de la classe de service pour la confirmation est la suivante.
@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();
//Faites quelque chose lors de l'exécution de la requête de manière asynchrone
log.debug("execute somethings");
PseudoDelay result = null;
try {
//Recevoir le résultat de l'exécution de la requête
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;
}
}
Vous pouvez voir que d'autres traitements ("*** exécuter quelque chose ***") sont exécutés immédiatement après l'émission de la requête.
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
Vous pouvez voir qu'il est bloqué jusqu'à ce que le résultat de la requête soit renvoyé.
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
Vous pouvez spécifier le délai. S'il ne se termine pas dans le délai spécifié, une exception TimeoutException sera levée.
result = future.get(5, TimeUnit.SECONDS);
Nous avons défini un délai d'expiration pour la transaction et examiné ce qui se passerait si une requête qui prenait plus de temps que ce délai était exécutée de manière asynchrone. Cette fois, j'ai défini le délai d'expiration de la transaction à 5 secondes et j'ai exécuté une requête qui a pris 10 secondes à la fois de manière asynchrone et synchrone comme dans l'exemple ci-dessus.
@Transactional(readOnly = true, timeout = 5)
** Pour les requêtes asynchrones **
Lorsque le délai d'attente a été dépassé, aucune exception n'a été déclenchée et le résultat attendu n'a pas été atteint. Je ne connais pas la cause, mais il semble que si vous l'exécutez dans un autre thread de la transaction, ce sera hors de la gestion des transactions. J'ai vérifié s'il y a une description dans le document officiel, mais je ne l'ai pas complètement vérifié. Pour le moment, il y avait Spring @Async et gestion des transactions dans un tel article.
** Pour les requêtes synchrones **
Je pense que cela dépend de l'implémentation du pilote JDBC, mais dans le cas de MySQL, l'erreur suivante se produira.
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]
/...réduction
Recommended Posts