Číselné řady objednávek

Číselné řady objednávek

Zkopírovat odkaz na sekciKonfigurace

Formát čísla objednávky se konfiguruje SpEL výrazem numberExpression v rozšíření OrderFeature (viz konfigurace objednávek). Výraz musí obsahovat právě jedno pořadové číslo. Číselná řada začíná vždy od 0; pro start od jiného čísla je nutné upravit hodnotu number v tabulce T_EDEESHOP_NUMBER_SEQ (resp. posunout řadu, viz Změna číselné řady).

V dokumentu je „uzel" používán jako synonymum pro pod / JVM instanci.

Zkopírovat odkaz na sekciRežimy generování čísla: sequenceNumber vs numberSeq

Pořadové číslo lze získat ve dvou režimech — liší se podle použitého tokenu ve výrazu:

token ve výrazuzdroj pořadového číslatransakce / zámek
sequenceNumber(id, len)in-memory rezervace bloků čísel per uzelbez DB zámku v confirm transakci
numberSeq(id, len)atomický DB inkrement number + 1exkluzivní row-lock na řádku <id> držený celou confirm transakci
nextNumber(id, len)alias — chová se jako sequenceNumber v kontextu generateSequenceCode, jako numberSeq v kontextu generateCodedle kontextu

id je identifikátor sekvence (první argument), len je délka výsledného čísla doplněná zleva nulami. Např. sequenceNumber('order'+dateFormat('yyyy'),5) → sekvence order2026, pětimístné číslo.

Zkopírovat odkaz na sekciRežim sequenceNumber (in-memory)

Pořadové číslo poskytuje služba NumberSequenceProvider, která si rezervuje celé bloky čísel v paměti; při vyčerpání si automaticky rezervuje další. Tím poskytuje čísla velmi rychle a minimalizuje deadlocky na databázi. Poslední použité číslo pro danou sekvenci a uzel se při startu inicializuje řetězcem LastUsedSequenceNumberInitiator (viz Změna číselné řady). Stav rezervací se perzistuje do T_EDEESHOP_NUMBER_SEQ.sequenceDescriptor (JSON nodeId → NodeSequence).

Multi-pod: proč selhává. Disjunktnost rezervací (a tím unikátnost čísel) platí jen pokud má každý JVM unikátní nodeId. V k8s je tato invarianta porušená: CodeGeneratorFeatureFeatureSpringConfig.java:77 čte nodeId z @Value("${hostname:default}"), ale CPS sitemap (edee/config/sitemap.xml) plní ${hostname} hodnotou prod/test per environment, ne per pod. Oba pody tedy běží se stejným nodeId="prod", při startu načtou identické rezervační bloky a rozdávají stejná čísla. Generování se 3× opakuje (viz Přiřazení čísla objednávky), ale další čísla z fronty kolidují stejně → padá:

1 java.lang.IllegalStateException: Cannot generate order number for expression: … and cart:2    at com.fg.cps.eshop.order.service.OrderNumberProviderImpl.lambda$generateCode$0(OrderNumberProviderImpl.java:75)

Prosté nodeId=${HOSTNAME} (název podu) problém neřeší: HOSTNAME se mění s každým restartem podu → není to stabilní identifikátor JVM. Důsledky: descriptor JSON roste s každým novým podem, bootstrap nového podu spadne na fallback čtoucí T_EDEESHOP_NUMBER_SEQ.number (který sequenceNumber mód neaktualizuje), a inicializační hledání volných čísel je limitované capem pageSize * 50. Detaily a další zvažované směry viz #2816.

Zkopírovat odkaz na sekciRežim numberSeq (atomická DB sekvence)

numberSeq získává každé číslo samostatnou atomickou DB transakcí (CodeGenerator#getNextNumberNumberSequenceDao#upNumberSequence): INSERT … ON DUPLICATE KEY UPDATE number = number + 1 + SELECT number. Žádný nodeIdmulti-node safe „out of the box", bezmezerové a unikátní.

Pozor na držení zámku. ShoppingCartService.confirmOrder() je @Transactional. Uvnitř volá generateCodegetNextNumber, který vytvoří TransactionTemplate bez nastavení propagace = default PROPAGATION_REQUIRED → připojí se do vnější confirm transakce. Upsert proto bere exkluzivní row-lock na řádku <id> (např. order2026) a drží ho až do commitu celého confirmu (přes zápis objednávky). Souběžné checkouty se na tom řádku serializují; čekající vlákna přitom drží connection z poolu c.edee. Při vysoké souběžnosti se pool vyčerpá → getConnection() 5s timeout → SQLTransientConnectionException všem DB requestům (i nesouvisejícím, např. browsing).

Tento náklad se projeví až hluboko nad reálným provozem a strop propustnosti drží jinde (frontend), takže pro běžný traffic je akceptovatelný. Možné optimalizace bez ztráty garance (per-node app-lock před otevřením confirm tx / navýšení poolu c.edee) jsou evidovány jako budoucí dluh v #2816.

Zkopírovat odkaz na sekciVýhody a nevýhody

vlastnostsequenceNumber (in-memory)numberSeq (DB)
rychlost / propustnostvysoká (čísla z paměti)nižší (1 DB transakce na objednávku)
single-pod: unikátnost + souvislá řada
multi-pod: unikátnost✗ — generuje duplicity → IllegalStateException (viz výše)
bezmezerová souvislá řadajen single-pod (multi-pod se bloky prokládají, vznikají mezery)✔ vždy
chování pod vysokou souběžnostípool nedusírow-lock přes celou confirm tx → při náporu vyčerpá DB pool c.edee (viz výše)
vhodné projen single-pod (výkon)multi-pod, faktury, striktně bezmezerová + unikátní řada

Zkopírovat odkaz na sekciDoporučení dle scénáře

  • Single-podsequenceNumber (rychlé, souvislé).
  • Multi-pod (k8s, ≥2 repliky)numberSeq (jediná funkční volba)sequenceNumber zde generuje duplicity.
  • Faktury / striktně bezmezerová a unikátní řadanumberSeq.

Zkopírovat odkaz na sekciPříklady

Jednopodové řešení:

xml
1 <numberExpression>dateFormat('yyyy') + sequenceNumber('order'+dateFormat('yyyy'),5) + random(1)</numberExpression>

Vícepodové řešení (k8s):

xml
1 <numberExpression>dateFormat('yyyy') + numberSeq('order'+dateFormat('yyyy'),5) + random(1)</numberExpression>

Co který režim používá v T_EDEESHOP_NUMBER_SEQ (klíč = sequenceId):

sloupecnumberSeq používásequenceNumber používá
number✔ (atomicky inkrementuje)✗ (nikdy needituje — jen fallback init)
sequenceDescriptor✔ (JSON rezervací per nodeId)
updated

Pozn. k random(1): unikátnost zajišťuje pořadové číslo, náhodná číslice je jen dodatek formátu — vizuálně ale rozbíjí souvislost řady.

Zkopírovat odkaz na sekciPřiřazení čísla objednávky

Přiřazení čísla objednávky se provádí v první fázi vytvoření objednávky a zajišťuje ho služba com.fg.cps.eshop.order.service.OrderNumberProvider. Defaultní implementace generuje kód pomocí CodeGenerator#generateSequenceCode podle nastavení OrderFeatureConfig#getOrderNumberExpression. Výraz musí obsahovat právě jedno pořadové číslo — podle použitého tokenu se uplatní jeden ze dvou režimů (sequenceNumber / numberSeq / nextNumber).

Vygenerované číslo se ověří na unikátnost (OrderDao#isNumberAlreadyUsed); při kolizi se generování 3× opakuje a po vyčerpání pokusů padá IllegalStateException z OrderNumberProviderImpl.

Zkopírovat odkaz na sekciZměna číselné řady

Poslední použité číslo se při startu EdeeCMS odvozuje z databáze řetězcem implementací LastUsedSequenceNumberInitiator:

  1. OrderLastUsedSequenceNumberInitiator – vezme vyšší z hodnot max(sequenceNumber) přes tabulky T_EDEESHOP_SHOPPING_CART a T_ESHOP_ORDER pro danou sequenceId a nodeId. Nejde tedy o „poslední záznam", ale o maximum přes všechny záznamy dané sekvence a uzlu.
  2. Pokud ani v jedné tabulce hodnota není (např. objednávky vzniklé před zavedením SequenceCode), použije se jako fallback LastUsedSequenceNumberInitiatorImpl, který přečte sloupec number z T_EDEESHOP_NUMBER_SEQ (bez filtru na uzel).

Zkopírovat odkaz na sekciPosunutí řady nahoru

  1. Nastavit sequenceNumber u libovolného záznamu v T_EDEESHOP_SHOPPING_CART nebo T_ESHOP_ORDER (pro danou sequenceId/nodeId) na požadovanou hodnotu vyšší než dosavadní maximum.
  2. Pokud žádný takový záznam neexistuje, upravit hodnotu number v tabulce T_EDEESHOP_NUMBER_SEQ.
  3. Restart EdeeCMS – číselná řada se drží v paměti a inicializuje se až při startu.

Pozor – snížení řady: Protože se bere max(sequenceNumber), nelze řadu snížit úpravou jednoho záznamu – přebije ho vyšší existující číslo. Snížení by vyžadovalo snížit maximum ve všech relevantních záznamech (cart i order) i v T_EDEESHOP_NUMBER_SEQ a hrozí kolizí s již použitými čísly (OrderNumberProvider ověřuje unikátnost přes OrderDao.isNumberAlreadyUsed()). V takovém případě je výrazně bezpečnější založit novou sekvenci s požadovanou počáteční hodnotou.

Další poznámky:

  • sequenceNumber je surová číselná hodnota sekvence, nikoli výsledné formátované číslo objednávky (number).
  • Vyhodnocuje se per uzel (nodeId); při víceuzlové architektuře řada nemusí být souvislá a inicializace proběhne na každém uzlu zvlášť.
  • Hodnota se projeví až po restartu; v paměti rezervovaná řada se zahodí.

Zkopírovat odkaz na sekciMigrace sequenceNumber → numberSeq

Postup přechodu (např. při nasazení projektu do multi-pod k8s).

Kritická prerekvizita: sequenceNumber mód nikdy needituje T_EDEESHOP_NUMBER_SEQ.number. numberSeq z něj naopak čte a inkrementuje. Pokud je number stale (nízké), numberSeq začne generovat čísla, která už existují → kolize → retry 3× → IllegalStateException. Před přepnutím je nutné number dorovnat na aktuální maximum použitých čísel.

  1. Dorovnat number pro danou sequenceId (např. order2026) na MAX(sequenceNumber) přes T_ESHOP_ORDERT_EDEESHOP_SHOPPING_CART (globálně, bez filtru na nodeId), s rezervou:

    sql
    1 -- diagnostika2SELECT3  (SELECT MAX(sequenceNumber) FROM T_ESHOP_ORDER            WHERE sequenceId='order2026') AS max_order,4  (SELECT MAX(sequenceNumber) FROM T_EDEESHOP_SHOPPING_CART WHERE sequenceId='order2026') AS max_cart,5  (SELECT number             FROM T_EDEESHOP_NUMBER_SEQ     WHERE sequenceId='order2026') AS seq_now;6
    7-- dorovnání (idempotentní; globální MAX přes obě tabulky + rezerva)8INSERT INTO T_EDEESHOP_NUMBER_SEQ (sequenceId, number, updated)9VALUES ('order2026',10  GREATEST(11    COALESCE((SELECT m FROM (SELECT MAX(sequenceNumber) m FROM T_ESHOP_ORDER            WHERE sequenceId='order2026') x), 0),12    COALESCE((SELECT m FROM (SELECT MAX(sequenceNumber) m FROM T_EDEESHOP_SHOPPING_CART WHERE sequenceId='order2026') y), 0)13  ) + 100,14  NOW()) AS new15ON DUPLICATE KEY UPDATE number = GREATEST(T_EDEESHOP_NUMBER_SEQ.number, new.number), updated = new.updated;

    sequenceId odpovídá prvnímu argumentu výrazu (např. 'order'+dateFormat('yyyy')order2026).

  2. Změnit numberExpression v edee/config/module-edeeshop/features/order/order.xml: sequenceNumber('order'+dateFormat('yyyy'),5)numberSeq('order'+dateFormat('yyyy'),5).

  3. Redeploy konfigurace + restart EdeeCMS — in-memory řada sequenceNumber se drží do restartu.

  4. Ověření: segment čísla objednávky skočí o rezervu (čili na dorovnané+1); T_EDEESHOP_NUMBER_SEQ.number se inkrementuje po každé objednávce; v Loki 0× IllegalStateException z OrderNumberProvider.

  5. Rollback: vrátit order.xml na sequenceNumber, redeploy. Hodnotu number ponechat (neškodí).

Zkopírovat odkaz na sekciMonitoring

  • Kolize čísel (sequenceNumber v multi-pod): IllegalStateException z OrderNumberProviderImpl v logu.
  • Vyčerpání poolu (numberSeq pod náporem): SQLTransientConnectionException z MonitoringPoolDataSource.getConnection(); rostoucí ThreadsAwaitingConnection na HikariCP poolu c.edee (datasource c.edee).

Zkopírovat odkaz na sekciZnámé budoucí směry řešení (#2816)

Zatím nerozhodnuto, evidováno v #2816:

  1. Atomická DB rezervace bloku bez nodeId (dlouhodobě preferováno) — refaktor NumberSequenceProvider, aby rezervace byla INSERT … ON DUPLICATE KEY UPDATE next_free = next_free + pageSize v jedné transakci vracející rozsah; aplikace pak dispenzuje z paměti. Bez identifikace uzlu, multi-node safe by design.
  2. Distribuovaný lock + ephemeral nodeId — serializace rezervace přes one.edee.darwin.locker.Locker, HOSTNAME jako ephemeral nodeId (vyžaduje cleanup job na descriptor JSON).
  3. Centralizovaný issuer — leader-elected pod jako jediný koordinátor rezervací (umožňuje souvislou řadu, ale komplikované).