Číselné řady objednávek
Konfigurace
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.
Rež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 |
idje identifikátor sekvence (první argument),lenje délka výsledného čísla doplněná zleva nulami. Např.sequenceNumber('order'+dateFormat('yyyy'),5)→ sekvenceorder2026, pětimístné číslo.
Rež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á:
java.lang.IllegalStateException: Cannot generate order number for expression: … and cart: … 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.
Rež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.
Vý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 |
Doporučení dle scénáře
- Single-pod →
sequenceNumber(rychlé, souvislé). - Multi-pod (k8s, ≥2 repliky) →
numberSeq(jediná funkční volba) —sequenceNumberzde generuje duplicity. - Faktury / striktně bezmezerová a unikátní řada →
numberSeq.
Příklady
Jednopodové řešení:
<numberExpression>dateFormat('yyyy') + sequenceNumber('order'+dateFormat('yyyy'),5) + random(1)</numberExpression>Vícepodové řešení (k8s):
<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.
Př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.
Změna číselné řady
Poslední použité číslo se při startu EdeeCMS odvozuje z databáze řetězcem implementací LastUsedSequenceNumberInitiator:
OrderLastUsedSequenceNumberInitiator– vezme vyšší z hodnotmax(sequenceNumber)přes tabulkyT_EDEESHOP_SHOPPING_CARTaT_ESHOP_ORDERpro danousequenceIdanodeId. 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 fallbackLastUsedSequenceNumberInitiatorImpl, který přečte sloupecnumberzT_EDEESHOP_NUMBER_SEQ(bez filtru na uzel).
Posunutí řady nahoru
- Nastavit
sequenceNumberu libovolného záznamu vT_EDEESHOP_SHOPPING_CARTneboT_ESHOP_ORDER(pro danousequenceId/nodeId) na požadovanou hodnotu vyšší než dosavadní maximum. - Pokud žádný takový záznam neexistuje, upravit hodnotu
numberv tabulceT_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 vT_EDEESHOP_NUMBER_SEQa hrozí kolizí s již použitými čísly (OrderNumberProviderověřuje unikátnost přesOrderDao.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:
sequenceNumberje 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í.
Migrace sequenceNumber → numberSeq
Postup přechodu (např. při nasazení projektu do multi-pod k8s).
Kritická prerekvizita:
sequenceNumbermód nikdy needitujeT_EDEESHOP_NUMBER_SEQ.number.numberSeqz něj naopak čte a inkrementuje. Pokud jenumberstale (nízké),numberSeqzačne generovat čísla, která už existují → kolize → retry 3× →IllegalStateException. Před přepnutím je nutnénumberdorovnat na aktuální maximum použitých čísel.
-
Dorovnat
numberpro danousequenceId(např.order2026) naMAX(sequenceNumber)přesT_ESHOP_ORDER∪T_EDEESHOP_SHOPPING_CART(globálně, bez filtru nanodeId), s rezervou:sql-- diagnostikaSELECT (SELECT MAX(sequenceNumber) FROM T_ESHOP_ORDER WHERE sequenceId='order2026') AS max_order, (SELECT MAX(sequenceNumber) FROM T_EDEESHOP_SHOPPING_CART WHERE sequenceId='order2026') AS max_cart, (SELECT number FROM T_EDEESHOP_NUMBER_SEQ WHERE sequenceId='order2026') AS seq_now; -- dorovnání (idempotentní; globální MAX přes obě tabulky + rezerva)INSERT INTO T_EDEESHOP_NUMBER_SEQ (sequenceId, number, updated)VALUES ('order2026', GREATEST( COALESCE((SELECT m FROM (SELECT MAX(sequenceNumber) m FROM T_ESHOP_ORDER WHERE sequenceId='order2026') x), 0), COALESCE((SELECT m FROM (SELECT MAX(sequenceNumber) m FROM T_EDEESHOP_SHOPPING_CART WHERE sequenceId='order2026') y), 0) ) + 100, NOW()) AS newON DUPLICATE KEY UPDATE number = GREATEST(T_EDEESHOP_NUMBER_SEQ.number, new.number), updated = new.updated;sequenceIdodpovídá prvnímu argumentu výrazu (např.'order'+dateFormat('yyyy')→order2026). -
Změnit
numberExpressionvedee/config/module-edeeshop/features/order/order.xml:sequenceNumber('order'+dateFormat('yyyy'),5)→numberSeq('order'+dateFormat('yyyy'),5). -
Redeploy konfigurace + restart EdeeCMS — in-memory řada
sequenceNumberse drží do restartu. -
Ověření: segment čísla objednávky skočí o rezervu (čili na
dorovnané+1);T_EDEESHOP_NUMBER_SEQ.numberse inkrementuje po každé objednávce; v Loki 0×IllegalStateExceptionzOrderNumberProvider. -
Rollback: vrátit
order.xmlnasequenceNumber, redeploy. Hodnotunumberponechat (neškodí).
Monitoring
- Kolize čísel (
sequenceNumberv multi-pod):IllegalStateExceptionzOrderNumberProviderImplv logu. - Vyčerpání poolu (
numberSeqpod náporem):SQLTransientConnectionExceptionzMonitoringPoolDataSource.getConnection(); rostoucíThreadsAwaitingConnectionna HikariCP pooluc.edee(datasourcec.edee).
Známé budoucí směry řešení (#2816)
Zatím nerozhodnuto, evidováno v #2816:
- Atomická DB rezervace bloku bez
nodeId(dlouhodobě preferováno) — refaktorNumberSequenceProvider, aby rezervace bylaINSERT … ON DUPLICATE KEY UPDATE next_free = next_free + pageSizev 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řesone.edee.darwin.locker.Locker,HOSTNAMEjako 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é).
