Zpět na domů

Optimalizace legacy kódu: snížení času zpracování o 99 %

Rozbor skutečného případu optimalizace zpracování 75 tisíc záznamů v legacy systému. Ukázáno, jak za 4 kroky zkrátit čas provedení z 105 sekund na 1,05 sekundy prostřednictvím odstranění nadbytečných operací, nastavení batch zpracování a bezpečné škálování.

Skutečný případ: optimalizace legacy kódu z 105 s na 1,05 s
Advertisement 728x90

# 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:

Google AdInline article slot
  • 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.

Google AdInline article slot

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í.

Google AdInline article slot

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

Advertisement 728x90

Číst dál