Čí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ýrazu | zdroj pořadového čísla | transakce / zámek |
|---|---|---|
| sequenceNumber(id, len) | in-memory rezervace bloků čísel per uzel | bez DB zámku v confirm transakci |
| numberSeq(id, len) | atomický DB inkrement number + 1 | exkluzivní 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 generateCode | dle 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#getNextNumber → NumberSequenceDao#upNumberSequence): INSERT … ON DUPLICATE KEY UPDATE number = number + 1 + SELECT number. Žádný nodeId → multi-node safe „out of the box", bezmezerové a unikátní.
Pozor na držení zámku. ShoppingCartService.confirmOrder() je @Transactional. Uvnitř volá generateCode → getNextNumber, 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
| vlastnost | sequenceNumber (in-memory) | numberSeq (DB) |
|---|---|---|
| rychlost / propustnost | vysoká (čí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á řada | jen 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é pro | jen single-pod (výkon) | multi-pod, faktury, striktně bezmezerová + unikátní řada |
Zkopírovat odkaz na sekciDoporučení dle scénáře
- Single-pod → sequenceNumber (rychlé, souvislé).
- Multi-pod (k8s, ≥2 repliky) → numberSeq (jediná funkční volba) — sequenceNumber zde generuje duplicity.
- Faktury / striktně bezmezerová a unikátní řada → numberSeq.
Zkopírovat odkaz na sekciPříklady
Jednopodové řešení:
1 <numberExpression>dateFormat('yyyy') + sequenceNumber('order'+dateFormat('yyyy'),5) + random(1)</numberExpression>Vícepodové řešení (k8s):
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):
| sloupec | numberSeq 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:
- 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.
- 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
- 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.
- Pokud žádný takový záznam neexistuje, upravit hodnotu number v tabulce T_EDEESHOP_NUMBER_SEQ.
- 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.
-
Dorovnat number pro danou sequenceId (např. order2026) na MAX(sequenceNumber) přes T_ESHOP_ORDER ∪ T_EDEESHOP_SHOPPING_CART (globálně, bez filtru na nodeId), s rezervou:
sql1 -- 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).
-
Změnit numberExpression v edee/config/module-edeeshop/features/order/order.xml: sequenceNumber('order'+dateFormat('yyyy'),5) → numberSeq('order'+dateFormat('yyyy'),5).
-
Redeploy konfigurace + restart EdeeCMS — in-memory řada sequenceNumber se drží do restartu.
-
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.
-
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:
- 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.
- Distribuovaný lock + ephemeral nodeId — serializace rezervace přes one.edee.darwin.locker.Locker, HOSTNAME jako ephemeral nodeId (vyžaduje cleanup job na descriptor JSON).
- Centralizovaný issuer — leader-elected pod jako jediný koordinátor rezervací (umožňuje souvislou řadu, ale komplikované).