Performance de backend Spring : les techniques que tout dev devrait connaître
Découvrez les techniques essentielles pour booster les performances de vos backends Spring : bon paramétrage des entités JPA, lectures de données efficaces et N+1 Select, insertions par blocs, modifications par lot, logs et métriques d'Hibernate, pool de connexion, gestion des transactions, cache, virtual threads, mesures avec Gatling et autres.
• Application clienteC++
• Un traitement est long (plus de 10 secondes)
• Boucle qui concatène de très nombreuses petites chaines via strcat :
Strcat
@fbeaufume
char* inputs[] = { "foo", "bar", "acme", ... };
char result[100000] = "";
for (int i = 0; i < inputs_size; i++) {
strcat(result, inputs[i]); // Append inputs[i] to result
}
return result;
beaufume.fr
6.
• Implémentation simplifiéede strcat :
• Comportement pour plusieurs appels :
Strcat
char* strcat(char* destination, char* source) {
char* pos = destination; // The current position
while (*pos != '0') { pos++; } // Find the end of destination
while (*source != '0') {
*pos = *source; pos++; source++; // Copy the characters
}
*pos = '0';
return destination;
}
Itération 1
Itération 2
Itération 3
Find
Find
Find
Copy
Copy
Copy
1
2
✘
✘
@fbeaufume beaufume.fr
7.
• Alternative optimisée:
• Comportement pour plusieurs appels :
Strcat
char* strcat2(char* destination, char* source) {
char* pos = destination; // The current position
while (*pos != '0') { pos++; } // Find the end of destination
while (*source != '0') {
*pos = *source; pos++; source++; // Copy the characters
}
*pos = '0';
return destination pos;
}
Itération 1
Itération 2
Itération 3
Find Copy
Copy
Copy
@fbeaufume beaufume.fr
8.
Logs web
INFO c.a.s.AccessLogFilter: Served GET '/foo' to 'john.doe' as 200 in 16 ms
@Component
public class AccessLogFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
long duration = -System.currentTimeMillis();
...
try {
chain.doFilter(req, res);
} finally {
duration += System.currentTimeMillis();
String url = ... // From request object
String username = ... // From Spring Security
LOGGER.info("Served {} '{}' to '{}' as {} in {} ms",
httpReq.getMethod(), url, username, httpRes.getStatus(), duration);
}
}
}
Simplifié
@fbeaufume
9.
• Logs SQL:
• Log des requêtes SQL lentes :
Logs Hibernate
spring.jpa.show-sql=true
INFO org.hibernate.SQL_SLOW : SlowQuery: 137 milliseconds. SQL: select b.id (...)
spring.jpa.properties.hibernate.log_slow_query=100
Hibernate: select b.id, b.name from book b
Configuration Spring
Extrait de log
@fbeaufume
Extrait de log tronqué
beaufume.fr
10.
• Activation :
•API :
Métriques Hibernate
spring.jpa.properties.hibernate.generate_statistics=true
// To get the SessionFactory from the EntityManagerFactory, if needed
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
Statistics stats = sessionFactory.getStatistics();
// The number of sessions opened by this session factory so far
long openedSessionCount = stats.getSessionOpenCount();
// The number of inserts for the book entity
EntityStatistics bookEntityStats = stats.getEntityStatistics(Book.class.getName());
long bookInsertCount = bookEntityStats.getInsertCount();
// The average duration of a query
String[] queries = stats.getQueries();
QueryStatistics queryStats = stats.getQueryStatistics(queries[0]);
long averageDuration = queryStats.getExecutionAvgTime();
1. Identifier larequête dans les logs
2. Ajouter breakpoint conditionnel dans SqlStatementLogger.logStatement :
3. Exécuter le traitement
4. Remonter la pile d'appel
Trouver le code exécutant une requête SQL
@fbeaufume beaufume.fr
JPA - Relations
Fetchtype Mode de chargement Valeur par défaut pour
LAZY
Différé
A la demande
@OneToMany et @ManyToMany
EAGER
Immédiat
Systématique
@OneToOne et @ManyToOne
@fbeaufume beaufume.fr
15.
• "Configurer enlazy, mais charger en eager."
1. Configurer ses relations en lazy :
2. Cas par cas, charger en eager l'ensemble des données :
JPA - Chargement des relations
@Entity
public class Book {
@ManyToOne(fetch = FetchType.LAZY)
private Author author;
@Query("FROM Book b JOIN FETCH b.author")
List<Book> findAllWithAuthor();
@fbeaufume beaufume.fr
16.
• @OneToOne et@ManyToOne sont EAGER par défaut
• C'est généralement mauvais pour les performances
• Mesures en EAGER :
• Encore plus de requêtes quand plusieurs ToOne
JPA - Comportement de EAGER
Book Author
N 1
@ManyToOne
en EAGER
Cas Type de lecture Méthode Requêtes SQL
1 One bookRepository.findById 1
2 One @Query("FROM Book b WHERE b.id=:id") 2 ✘
3 One API Criteria 2 ✘
4 All bookRepository.findAll 1+N ✘
5 All @Query("FROM Book b") 1+N ✘
6 All API Criteria 1+N ✘
17.
• Avec (LEFT)JOIN FETCH :
• Avec entity graph :
• Ce qui charge les données en 1 requête plutôt que 1+N+M ✘ :
JPA - Chargement des relations
@Query("FROM Book b JOIN FETCH b.author a JOIN FETCH a.address")
List<Book> findAllWithAuthorAndAddress();
@EntityGraph(attributePaths = {"author", "author.address"})
@Query("FROM Book b")
List<Book> findAllWithAuthorAndAddress();
Book Author
N 1
@ManyToOne
en LAZY
Address
1
N
@ManyToOne
en LAZY
SELECT ... FROM book b JOIN author ... JOIN address ...
@fbeaufume beaufume.fr
18.
• Avec entitygraph nommé :
• Avec API Criteria :
JPA - Chargement des relations
root.fetch("author").fetch("address");
@EntityGraph("Book.withAuthorAndAddress")
@Query("FROM Book b")
List<Book> findAllWithAuthorAndAddress();
@Entity
@NamedEntityGraph(
name = "Book.withAuthorAndAddress",
attributeNodes = {
@NamedAttributeNode(value = "author", subgraph = "author-graph") },
subgraphs = {
@NamedSubgraph(name = "author-graph",
attributeNodes = @NamedAttributeNode("address")) })
public class Book {
BookRepository
@fbeaufume beaufume.fr
19.
• JOIN FETCH,entity graph, etc, fonctionnent aussi avec des ToMany
• Mais pour chaque ToMany les rows sont multipliés
• De plus pour plusieurs ToMany il faut un type Set
• Envisager une seule ToMany à la fois et laisser Hibernate fusionner
JPA - Relations ToMany
@fbeaufume beaufume.fr
20.
JPA - RelationsToMany
// FROM Driver d LEFT JOIN FETCH d.cars
driverRepository.findAllWithCars();
// FROM Driver d LEFT JOIN FETCH d.bikes
return driverRepository.findAllWithBikes();
Driver
Car
1
20
Bike
30
1
2 requêtes (200 rows puis 300 rows)
Sans optimisation :
21 requêtes ✘
1 (10 rows) + 10 (20 rows) + 10 (30 rows)
// FROM Driver d
// LEFT JOIN FETCH d.cars
// LEFT JOIN FETCH d.bikes
return driverRepository.findAllWithCarsAndBikes();
1 requête (10x20x30 = 6000 rows)
10x
Solution 1 : double JOIN FETCH Solution 2 : deux JOIN FETCH
@fbeaufume beaufume.fr
21.
JPA - Ecritures
#Active le batching des inserts et updates
spring.jpa.properties.hibernate.jdbc.batch_size=50
# Permet de réordonner les inserts multi-types pour les batcher
spring.jpa.properties.hibernate.order_inserts=true
# Permet de réordonner les updates multi-types pour les batcher
spring.jpa.properties.hibernate.order_updates=true
@fbeaufume beaufume.fr
22.
• Par auto-incrément:
• Par séquence :
JPA - Génération d'ID
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book-gen")
@SequenceGenerator(name = "book-gen",
sequenceName = "book_seq", allocationSize = 20)
private Long id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@fbeaufume beaufume.fr
23.
JPA - Insertiond'entités
insert into ...
IDENTITY SEQUENCE
(simplifié, pour INCREMENT et allocationSize à 20)
1
21
22
2
insert into ...
insert into ...
insert into ...
select nextval ... insert into ...
insert into ...
select nextval ... insert into ...
insert into ...
Total pour 100 entités : 100 SQL Total pour 100 entités : 105 SQL
3
insert into ... insert into ...
20
40
#1
#2
#3
#21
#22
#2
#3
#22
#1
#21
• Augmentation desalaire :
• Solution 1 :
• Solution 2 :
JPA - Modifications par blocs
@Modifying
@Query("UPDATE Employee e SET e.salary = e.salary * ?2 WHERE e.department = ?1")
int applyRaise(String department, float amount);
@Transactional
public void applyRaise(String department, float amount) {
employeeRepository.findByDepartment(department)
.forEach(e -> e.applyRaise(amount));
}
1 SELECT + N UPDATE ✘
1 UPDATE
@fbeaufume beaufume.fr
spring.jpa.properties.hibernate.jdbc.batch_size=50
1 SELECT + N/50 BATCH UPDATE
26.
Pool de connexionà la BD
# Nombre maximum de connexions dans le pool
# 10 par défaut
spring.datasource.hikari.maximum-pool-size=20
# Nombre minimum de connexions dans le pool
# 'maximum-pool-size' par défaut pour un pool de taille fixe
spring.datasource.hikari.minimum-idle=5
# Quand 'minimum-idle' < 'maximum-pool-size',
# durée avant liberation pour une connexion inutilisée
# 600000 (10 minutes) par défaut
spring.datasource.hikari.idle-timeout=300000
# Durée maximale d'attente par l'appli d'une connexion avant de lever une exception
# Compromis entre attendre et échouer
# 30000 (30 sec) par défaut
spring.datasource.hikari.connection-timeout=10000
Exemple de configuration Hikari pour Spring Boot, voir aussi https://github.com/brettwooldridge/HikariCP
@fbeaufume beaufume.fr
• Consommation mémoireou disque
• Structure :
• Perte de fraicheur des données
Cache - Limitations
@fbeaufume
Back #1 Back #2 Back #1 Back #2 Back #1 Back #2
Cache
Cache
Cache Cache
Incohérence Coordination Latence
Cache local Cache distribué Cache serveur
beaufume.fr
29.
• Cache Spring:
• Caches JPA/Hibernate :
• Et du paramétrage (taille de cache, expiration, etc)
Cache - APIs
@Cacheable("books")
public Book getByIsbn(String isbn) {
...
}
@QueryHints({@QueryHint(name = "org.hibernate.cacheable", value = "true")})
List<Book> findByGenre(String genre);
BookService
@Entity @Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Book {
BookRepository
Entité
Cache de requêtes
Cache d'entités
@fbeaufume beaufume.fr
30.
• Gains mémoireet CPU sous charge
• Spring Boot 3.2+ et Java 21+ mais préférer Java 24+
(contre problème de thread pinning sur synchronized)
• Activation :
Virtual threads
spring.threads.virtual.enabled=true
Traitement métier
Appel BD
Threads classiques Virtual threads
Thread X Y
X
@fbeaufume beaufume.fr
31.
• Envisager @Transactionalet @Before/AfterAll
plutôt que @Before/AfterEach :
Tests Spring - Initialisation des données
@SpringBootTest
@Transactional
public class ItemServiceTest {
@BeforeAll static void beforeAll(@Autowired ItemRepository itemRepository) {
itemRepository.saveAll(...);
}
@AfterAll static void afterAll(@Autowired ItemRepository itemRepository) {
itemRepository.deleteAll();
}
@Test void deleteItem() throws Exception {
...
}
// Other test methods
}
@fbeaufume beaufume.fr
32.
Tests Spring -Démarrage des contextes
Extrait de log "mvn test"
33.
• Spring Testréutilise les application contexts quand c'est possible
• Des application contexts différents :
Tests Spring - Cache des contextes
@SpringBootTest
class Sample1Test {
@SpringBootTest(webEnvironment = RANDOM_PORT)
class Sample3Test {
@WebMvcTest // Or @DataJpaTest
class Sample5Test {
@SpringBootTest
class Sample4Test {
@MockitoBean FooService foo;
@SpringBootTest
// Or @AutoConfigureRestTestClient for SB 4+
@AutoConfigureMockMvc
class Sample2Test {
1
2
3
4
5 Tests d'intégration
Tests unitaires
ou librairie mock-in-bean
En choisir un,
dans une classe mère
@fbeaufume beaufume.fr
Gatling - Résultats
Synthèsede la simulation
Temps de réponse des pages
Temps de réponse dans le temps
Distribution des temps de réponse
@fbeaufume beaufume.fr