# Optimalizace legacy kódu: jak zkrátit čas zpracování o 99 % v reálném případě
Implementace REST integrace s externím systémem často naráží na problémy s výkonem. V tomto materiálu rozebereme, jak za 4 kroky zrychlili zpracování 75 tisíc záznamů a snížili čas provedení o 99 %. Případ je založen na reálné úloze integrace bankovního systému s externím partnerem při přísných omezeních.
Problém: REST integrace s omezeními partnera
Počáteční podmínky:
- Nutnost odesílání dat o uživatelích do systému partnera prostřednictvím REST API
- Partner přijímá pouze jednotlivé požadavky (zákaz dávkového zpracování)
- Omezení času provedení: okno v 5 hodin ráno pro minimalizaci zátěže
- Objem dat: 75 000 záznamů ve stavu NEW
- Zákaz asynchronního zpracování a horizontálního škálování
Původní kód používal stránkové načítání prostřednictvím Spring Data Pageable s velikostí stránky 2000 záznamů. Kritická chyba spočívala v akumulaci všech změněných entit v paměti až do jediné operace saveAll(), což vedlo k:
- Automatickému flush kontextu před každým SELECT
- Generování 2000 UPDATE požadavků na každou stránku
- Blokování transakce na dobu HTTP volání
První měření ukázala čas zpracování ≈105 sekund na Apple M4 s 16 GB RAM. Při emulaci zpoždění externího systému 1 ms to byl již kritický ukazatel pro production prostředí.
Krok 1: Odstranění zbytečných operací s DB
Analýza SQL logů odhalila fatální chybu: Hibernate generoval 2000 samostatných UPDATE místo batch operací. Příčina — držení entit v persistence context mezi iteracemi smyčky. Řešení zahrnovalo dvě klíčové změny:
- Nahrazení ukládání na konci procesu intermediárními commitami:
content.forEach(this::registerUser);
userRepository.saveAll(content);
- Implementace native query pro hromadné aktualizace stavů:
@Modifying
@Query(value = """
UPDATE users SET status = CAST(:status AS text)
WHERE id IN (:ids)
""", nativeQuery = true)
int batchUpdateStatus(@Param("ids") Collection<Long> ids, @Param("status") String status);
Tyto změny přinesly první výsledky — snížení času o 6 % (na 98,4 sekundy). Klíčový závěr: standardní metody Spring Data JPA mohou vytvářet skryté réžie při práci s velkými objemy dat.
Krok 2: Jemné ladění parametrů zpracování
Další optimalizace se soustředila na velikost dávky (batch size). Testování ukázalo:
- Při velikosti 500: čas vzrostl o 8,41 % (106,68 s)
- Při velikosti 2000: základní hodnota 98,4 s
- Při velikosti 5000: zlepšení o 1,86 % (96,58 s)
Důležité je pochopit, že „stříbrná kule“ neexistuje. Optimální velikost závisí na:
- Charakteristikách hardware (IOPS disku, propustnost sítě)
- Složitosti business logiky v smyčce zpracování
- Nastavení poolu připojení k DB
Doporučený přístup: provést A/B testování na production-like prostředí s profilováním zdrojů (CPU, memory, disk I/O). V našem případě se optimální ukázala velikost 5000 záznamů, což snížilo počet commitů transakcí.
Krok 3: Bezpečné horizontální škálování
Pokus o spuštění zpracování ve dvou vláknech vedl k katastrofickému výsledku — duplikaci všech operací (150 000 místo 75 000 požadavků). Řešení vyžadovalo změnu strategie přístupu k datům:
- Implementace SELECT FOR UPDATE SKIP LOCKED:
SELECT * FROM users WHERE status = 'NEW' ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5000
- Realizace transakční smyčky:
@Transactional
public void processBatch() {
List<User> batch = userRepository.lockAndSelectBatch(5000);
batch.forEach(this::registerUser);
userRepository.batchUpdateStatus(
batch.stream().map(User::getId).collect(Collectors.toList()),
UserStatus.PROCESSED.name()
);
}
Tento přístup zajistil:
- Garantované jednorázové provedení zpracování
- Minimální blokování tabulky
- Možnost spuštění N paralelních instancí
Po implementaci se čas zpracování snížil na 1,05 sekundy — snížení o 99 % oproti původní verzi.
Klíčové etapy optimalizace
- Analýza skrytých réžií
- Zapnutí show-sql pro sledování generovaných požadavků
- Kontrola chování persistence context ve smyčce
- Přechod na batch operace
- Nahrazení saveAll() intermediárními commitami
- Použití native query pro hromadné aktualizace
- Experiment s parametry
- Testování různých velikostí dávek
- Monitorování systémových metrik během testů
- Příprava na škálování
- Implementace SKIP LOCKED pro konkurenční přístup
- Dekompozice transakcí na atomové bloky
Co je důležité
- Hibernate flush před SELECT — kritický problém při práci s velkými objemy dat. Každý požadavek na DB způsobí automatický flush, což vede k kaskádě UPDATE.
- Velikost dávky — není konstanta. Vyžaduje empirický výběr s ohledem na specifika zátěže. V testech se optimální ukázala velikost 5000 záznamů.
- Transakční izolace při škálování. SELECT FOR UPDATE SKIP LOCKED zajišťuje konkurenční zpracování bez duplikací, ale zvyšuje dobu držení zámků.
- Profilování — povinná etapa. Bez měření na production-like prostředí mohou optimalizace situaci zhoršit (jako při snížení velikosti dávky na 500).
- Kompatibilita s legacy omezeními. Řešení musí brát v úvahu externí omezení (např. zákaz partnera na dávkové požadavky), což vylučuje jednoduché cesty jako asynchronní zpracování.
Finální implementace ukázala stabilní čas zpracování 1,05 ± 0,05 sekundy při zátěžovém testování s 100 000 záznamy. Klíčový faktor úspěchu — kombinace technických optimalizací a hlubokého porozumění smlouvy s externím systémem. Přístup je aplikovatelný pro jakékoli scénáře integrace s REST API při omezeních velikosti požadavků a požadavcích na spolehlivost.
— Editorial Team
Zatím žádné komentáře.