Konkurens select, insert megakadályozása míg az adott insert folyik

Sziasztok!

PHP-ben fejlesztek egy leltár programot amiben van egy leltári szám, ami 2 féle képpen generálódik:
1. Év+Hónap+futó sorszám. Pl.: 11050001, 11050002, 11050003...
2. Csak futó sorszám, Pl.: 00000001, 00000002, 00000003...

Ezen az oszlopon index van, és mellette van egy auto increment primary key is.

A Probléma a következő: ebbe a táblába sok insert történik egy időben, mert a php progit sokan használják egyszerre. A php egy mysql userrel csatlakozik a mysql-hez és hajtja végre a lekérdezéseket. Ebben az oszlopban adott esetben előfordulhat duplikáció, mert van olyan hogy egy leltári számon kell tárolnom szorosan összefüggő termékeket. Ez miatt nem lehet unique, illetve a fentebb leírt leltári szám generálás miatt auto increment sem.

Amikor új elemet viszek a leltárba akkor ki kell olvasnom az adott számozási eljárásnak megfelelő legmagasabb leltári számot, majd az INSERT-nél ehhez adok egyet és beszúrom a többi adattal együtt. Ezzel az a probléma hogy ha az adott pillanatban más is elindít egy beszúrást, akkor ugyanazt a legmagasabb leltári számot fogja beolvasni, majd növelni egyel mint a konkurens user, és így ugyanarra a leltári számra kerülhet 2 teljesen külömböző termék.

Hogy lehet ezt kiküszöbölni?

Hozzászólások

Ha read lock-ot teszel rá, akkor mindenki tudja továbbra is olvasni, viszont te, mint zároló sem fogod tudni írni, kivéve a local read lock és nem konkurrens insert esetén. Ha viszont write lockot teszel rá, akkor csak te fogod tudni írni/olvasni, a többi tranzakció addig várakozik, míg te nem unlockolod a táblákat. Ha unlock tables-szel leveszed a lockot a táblákról, akkor az eddig várakozó thread-ek már az új, módosított adatokat kapják vissza és írhatnak/olvashatnak.

--
A gyors gondolat többet ér, mint a gyors mozdulat.

Köszi a választ!

Ezek szerint nekem write lockot kell használnom. A kérdés már csak az hogy hogy tudja megkülömböztetni a mysql, hogy én lockoltam-e a táblát vagy más. A program ugyanazt a mysql usert használja az adatok beszúrására lekérdezéseére függetlenül attól hogy magába a programba ki authentikált be.

Vagy ez úgy működik hogy amikor az apacsnak beérkezik egy kérés akkor ő teljesen új szálon fogja kiszolgálni azt mint az előző kérések, ez által maga a php kód is függetlenül fut a többitől? Ha így van akkor logikus hogy a mysql lekérés is új szálon fog beérkezni a mysqld-nek.

Bocsi ha túl triviális dolgokat kérdezek, de szeretném ezt tisztába tenni magamban.

Köszi a választ mégegyszer, sokat tanultam belőle!

Ezredmásodpercről van szó nem? (gondolom közvetlen a beszúrás előtt kérdezed le a legnagyobbat)

Erre szerinted van esély, hogy kétszer adod ki ugyanazt?
Ha van, akkor nem ezredmásodpercről beszélünk, mert user interakció van a lekérdezés és a beszúrás között, az meg a Te hibád.

Akárhogy akarod csinálni, ne Oregon tanácsát kövesd, mert az tényleg nem bolond biztos, csak bolondoknak való..:))

Egyébként meg semmi szükség tábla szintű LOCK-ra, sőt a legrosszabb megoldás amit valaki ajánlani tud... ha jól gondolom MySQL alatt is van szerver oldali tárolt eljárás és SELECT .. FOR UPDATE, ha egy táblából szeded a növekményt.

Részemről én is SELECT ... FOR UPDATE párti vagyok

Egy külön táblát szoktam fenntartani az egyes folyamatok lockolásához, a tábla tartalmazza azokat a folyamatokat amelyek zárolást igényelnek, pl: számlázás, raktár készlet változás, ... Egy folyamat alatt ebből szükség szerint akár többet is zárolok.

Lépések:
1. Tranzakció nyitása
2. Megpróbálom zárolni a szükséges folyamatot (select ... for update)
2a. Ha nem sikerül (dead lock) akkor várok egy kicsit és újra próbálom, ha pl. 5 alkalommal nem jön össze akkor hibaüzenet
3. max-al a legnagyobb érték lekérése, adatok felírása, ....
4a. Tranzakció lezárása (a lezárás automatikusan feloldja a zárolást is)
4b. Ha bárhol hiba van rollback (zárolás automatikusan feloldódik)

- Szerintem nem célszerű a sorszámot külön táblában tárolni, ha pl évente újra kell indítani a sorszámot akkor elég a "select max(sorszam) +1 from tabla where ev = :aktualisev" igy maris automatikusan 1-est oszt ki
- Tárolt eljárások jók, de amennyiben a program és az adatbázis egy gépen van, szerintem több problémát okoz mint amennyit használ, sebességben nem lesz sokkal gyorsabb, főleg ha csak sima adatfeltöltögetésekről van szó, a kód viszont nehezebben karbantartható, hiszen a query-k egy része php-ben vannak másik része meg az adatbázisban

> - Szerintem nem célszerű a sorszámot külön táblában tárolni..
A módszerednek (szerintem) több rákfenéje van.. így hirtelen:
.. az egyik, hogy nem elég rugalmas: pölö.: ha havonta kell új sorszám és analitika végett ki kell mutatni mekkora a "növekmény".. a select count(*) költséges..
.. a másik szintén költség, a megoldásodnál a max() jóval erőforrás igényesebb mint, ha külön tárolja, akár van rá index akár nem..
.. a select for update record szintű zárolásra legjobb.. a Te esetben őszintén meg mondom gőzöm sincs mit lockolhatsz, lehet szintén az egész táblát, ami ugye azért gáz,
mert akkor túl sok mindent blokkolsz

> - Tárolt eljárások jók, de amennyiben..
Ezzel sem értek egyet.. nyilván programozási nyelv és adatbázis kérdése is, de nagyságrendekkel gyorsabb mint nélkülük, a karbantarthatóság részben bukik, mert szerintem pont hogy jól szétválik a két réteg, így ha kell X adatbázisra tudod könnyen teljesen optimalizálni, nem kell egy gagyi köztes réteg, hogy más DB-n is használj. Egy esetben lehetnek komoly problémák, ha adatbázis klaszterrel dolgozol.. más esetekben, szerintem teljesen korrekt, főleg, ha a "programod" nem csak egy nyelven működik/működhet.

Konkrétan tudok olyan - egyébként klasszis - dbkezelőt, amelynek optimalizálója számára a LIMIT (pontosabban az annak megfelelő elem) felér egy szívlapátos fejbeveréssel. Míg a MAX minden motor (és optimalizáló, ha van olyanja) számára egyértelműen elmondja, hogy egyetlen értékre kell felkészülni

.. az egyik, hogy nem elég rugalmas: pölö.: ha havonta kell új sorszám és analitika végett ki kell mutatni mekkora a "növekmény".. a select count(*) költséges..
Ezt nem értem, minek a növekményéről beszélünk? Ha külön táblában tárolod a legmagasabb sorszámot az miben segít kimutatás készítésekor?

.. a másik szintén költség, a megoldásodnál a max() jóval erőforrás igényesebb mint, ha külön tárolja, akár van rá index akár nem..
Már miért lenne erőforrás igényes egy max() egy indexelt tábla esetén? Az hogy egy ilyen jellegű sorszámra index kell szerintem egyértelmű.

.. a select for update record szintű zárolásra legjobb.. a Te esetben őszintén meg mondom gőzöm sincs mit lockolhatsz, lehet szintén az egész táblát, ami ugye azért gáz, mert akkor túl sok mindent blokkolsz
Valóban lehet nem voltam egyértelmű, természetesen rekord szintű lock-ra gondolok
Egy tábla amiben a folyamatokat reprezentáló rekordok vannak, ezeket a rekordokat lock-olom nem az adatokat tároló táblát


|--------------------|
|Locktabla           |
|--------------------|
| 1. Számlázás       |
| 2. Raktár kezelés  |
| 3. ...             |
|                    |
|--------------------|

Tárolt eljárások, nem azt mondom, hogy egyértelműen kerülendő, hiszen ez feladat és környezet függő. Abban hogy az előnyök és a hátrányok milyen súllyal esnek latba nem értünk egyet, szerintem maradjunk ennyiben :)

> miben segít kimutatás készítésekor?
Hozzádobsz egy valamely dátum mezőt és máris meg van, hogy X évben Y hónapban mennyit vittek be, de schema tárolásra is szokták használni (miképp képezik a "id"-t), stb..

> Már miért lenne erőforrás igényes egy max()..
Adatbázisa válogatja, de a max, count és aggregátor társai "nem mindig" használják az index-t, hiába teszel rá.. tény nem tudom mysql esetében, hogy van,
én már jártam pórul.
Egyébként jó a max-os megvalósítás is.. csak nekem nem tetszik valamiért, nem érzem jónak.. a megérzésekre meg hallgatni kell.:)

Így már érthető mit lockolsz..:)
Te is egy külön táblában lockolsz, az már részlet kérdés másra is van-e használva.

Mysql-ben altalaban rendezett btree indexeid vannak, ezert a max egy nagyon olcso muvelet. Ha indexelve van a max, akkor a jobb szelso leaf node-ot fogja csak elolvasni a select az indexbol. Ha hash indexed van memory vagy ndb engine-ju tablan, akkor mar nyilvan nem ez a szitu, hasonloan hogyha mas adatbaziskezeloben probal az ember ilyet csinalni nem btree index-szel.

Kicsit fapad, de miért nem csinálsz egy táblát, amiben 1 db autoincrementes mező van, arra csinálsz insert-et majd kéred le mysql_insert_id -vel az utolsó beszúrt ID-t?

csinálhatnád úgy is hogy az adatbázisban a leltári számot két mező adja. Egyik Év+hó a másik meg mehet egy autoincrement futo szám.

Ha a két generálási módban a futo szám nem ugyanazt a számot jelenti akkor hagyd figyelmen kívül amit írtam.

"Ezzel az a probléma hogy ha az adott pillanatban más is elindít egy beszúrást, akkor ugyanazt a legmagasabb leltári számot fogja beolvasni, majd növelni egyel mint a konkurens user, és így ugyanarra a leltári számra kerülhet 2 teljesen külömböző termék."

"Hogy lehet ezt kiküszöbölni?"

Pl. a jo oreg tranzakciokkal? :)

--
Fontos feladatot soha ne bizz olyan gepre, amit egyedul is fel tudsz emelni!

Ha jól értem lekéred php val az legújabb leltári számot majd generálsz egy annál 1 el nagyobbat és vissza inserteled. Ugye? És a problémád az, hogy ha párhuzamosan történik mindez akkor azonos számra kerülhet két cikk aminek nem kéne. Megoldás az, ha az egészet mysql-ben oldod meg. Kezeli az időt és egy insert en belül lehet select is. Nézz utána.

Ha lockolod a táblát akkor meg a másik rögzítés megy pocsékba! Kivéve ha phpval ellenőrzöd, hogy sikerült-e és ha nem akkor újra próbálod n szer. ( Ez egyszerűbb talán )

Sok sikert!

az ilyet anno az 1900-as evekben btrieve alapokon rekordszintu lockolassal oldottuk meg.

set_lock();
while get_last_record();
inc(egyediazon);
insert_new_record();
ulock();

masik megfejtes volt a kulon fileben/tablaban tartott szamlalo.

param_lock();
szamlalo_kiolvas-megnovel-visszair();
param_unlock();

azota biztos vannak fejlettebb modszerek is, tizenx ( de lehet 20 ) eve nem programoltam.

Igy nem tudod?

INSERT INTO xxx (leltari_szam, ...) VALUES ((SELECT max ... FROM ...) + 1, ...)

Ez egy tranzakcioban fog lefutni, es garantalja hogy nincs concurrent select/insert.

Bocs az ekezetek hianya miatt, telefonrol irok.

Srácok bocs de hogy jönnek ide a tranzakciók? Azon kívül, hogy vannak olyan mellékhatások, amiket fel lehet használni a probléma megoldására, de kb. oldalakon keresztül lehetne sorolni, hogy melyik adatbáziskezelő melyik verziója milyen izolációs szint / cursor stability mellett mit is lockol és mit nem..

Azt is leírta az OP, hogy a szekvencia nem megfelelő..

Javasolt megoldás:

Egy segédtáblában a teljes leltári szám a mező tartalmát képezi (meg esetleg lehetnek egyéb mezők amik segíthetnek a selectben, pl. dátum) és minden felhasznált leltári számot beinsertálsz. Ha új leltári szám kell, kiválasztod a megfelelő max értéket, hozzáadsz egyet, beinsertálod. Ha nem sikerült (unique!), akkor közben már valaki berakott valamit, hozzáadsz még egyet és újra megpróbálod. Ciklusból. Az alkalmazásból. Mivel tudod, hogy az alkalmazásodnak mire van szüksége és mit csinál, ez egy teljesen korrekt megoldás.

Mielőtt pure sqlből próbálnád megoldani:
http://forums.oracle.com/forums/thread.jspa?threadID=681931
google select max for update (bug, problems, stb)

"Srácok bocs de hogy jönnek ide a tranzakciók?"

Hogyhogyhogy? Egy serializable tranzakcio mellett nem fordulhat elo ez: "ha az adott pillanatban más is elindít egy beszúrást, akkor ugyanazt a legmagasabb leltári számot fogja beolvasni, majd növelni egyel mint a konkurens user, és így ugyanarra a leltári számra kerülhet 2 teljesen külömböző termék".

Ami ugyebar maga a Problema.

--
Fontos feladatot soha ne bizz olyan gepre, amit egyedul is fel tudsz emelni!

Szerinted a serializable azt jelenti, hogy ha nyomtál egy begint, akkor a következő begin várni fog, amíg be nem fejezed? :) Magában nem, lockolnod kell

Egyébként meg serializable szintnél kell a kódba egy ugyanolyan ellenőrzés az unable to serialize esetre, mint amikor ráfutsz a segédtáblás módszernél már létező sorszámra, a különbség, hogy ennek, ha ránéz valaki a kódra, semmi köze nem lesz az üzleti logikához, ezért nem annyira átlátható. Aki kihagyja az ellenőrzést (~mindenki), az meg fail

Ezt írtam:

"Szerinted a serializable azt jelenti, hogy ha nyomtál egy begint, akkor a következő begin várni fog, amíg be nem fejezed? :) Magában nem, lockolnod kell"

"de, igen, hatásában azt jelenti a serializable, sőt, sokkal többet."

Szóval nem kell kulon lockolni?

Serializable mod:



Thread1                           Thread2
begin                              
select * from blabla              begin                 # csak share lock
leltarid eloallitasa programban   select * from blabla  # csak share lock
                                  leltarid eloallitasa programban
insert into blabla
                                  insert into blabla    # var, amig a masik tranz. lezarul
commit
                                  commit                # ket ugyanolyan leltarid lesz

???

Ha a fejed tetejére állsz, a select akkor is csak share lockot fog rátenni. A doksi szerint is. Ami a jelen problémán nem segít. Vagy mire gondoltál?

Ha exkluziv lockot nyitsz (for update), az oke, de arrol volt szo, hogy nem kell.

Edit: de ne ezen ne vitatkozzunk, értem, mit akarsz mondani, és így rossz a példám is. Visszatérve az eredetileg feltett kérdéshez, szerinted nem jobb, ha a leltárid-ket inkább letárolja unique módon?

Nemrég terveztem egy anyázó postot hogyan is mennek néha ezek a lockolások DB2-ben.. főleg amióta MVCC-znek.. de aztán elfáradtam.

Egyébként ha papírforma szerint csinálod akkor serializable szinten bejön a deadlock és unable to serialize kezelés (ami ezen az izolációs szinten nem hiba, hanem normál működés), ezt meg lehet úszni pl a fentebb írt megoldással, iagz, ebben az elszigetelt problémában ezt nem lenne jelentős kezelni, de van alkalmazás, ahol igen

Nem vagyok formában, most esett le csak igazán miről van szó.. ott a pont megint.:)
A módszered, hogy külön táblát használsz és abban az össze id-t letárolod megoldás, jobb mint amit a linkeden olvastam (sequence, sequence_within_parent).
Egyébként valahol röhej, hogy a "bombabiztos" megoldáshoz 2010-ben még mindig mennyit "kell" gályázni.

Az "elvileg" részhez: http://pastebin.ca/2066326

Ez a kód egy random itteni PostgreSQL-en (8-as) a következő outputot adja:


% ./test.pl
1
2
3
4
4

9-esben elvileg unable to serialize lenne (helyesen). MySQL 5.5 -ben mindig deadlock-ra fut (tehát a második művelet elindul, ahogy a doksijuk is írja), van egy korábbi MySQL verzióm, ahol viszont hasonló duplikátum lesz az eredmény.

Ha valami fatálisat elírtam a kódban akkor mea maxima culpa és szóljatok!

Srácok ez szarabb mint gondoltam. MySQL esetén csak azért várt az első insert, mert ebben a speciális tartalmú táblában page lockra konvertálódott (optimalizálódott?) a dolog innodb statisztikában mókolás alapján. Raktam bele jó sok cuccot meg mindenféle selectet, így simán jönnek a duplikációk. 5.0.51-en alapból is lefut és duplikál, 5.1.54 -en megint a page lockos történet de ott van egy row lock is, de most egyelőre meguntam a reszelgetést

Szerk. 5.1.54-en is reprodukálva, csak véletlenül benne hagytam egy for update-et. Csak azért áll meg és lesz lock wait timeout alapból (.. nem igazán korrekt), mert az insert a teljes page-et ahova rakni akarja, ellátná egy exkluzív lock-kal, amely page-ben előfordulhat (de nem biztos, sőt!), hogy ott vannak a másik thread select-jéből származó shared row lock-ok. Tehát még DBM-en belül és verzión belül se kiszámítható, hogy mi lesz. :)

Szerk2. ez alapján készült egy változat, ami az összes mysql verzión működik. (nyilván éles adatokon magától előjönne könnyen, mert a tábla kellően diverzifikált lenne)

"Szerinted a serializable azt jelenti, hogy ha nyomtál egy begint, akkor a következő begin várni fog, amíg be nem fejezed? :)"

A hatasat tekintve majdnem pontosan ezt jelenti ("sorosithato az utemezes, ha valamennyi hatasa ekvivalens valamely soros utemezessel"), a megvalositast tekintve pedig nyilvan azok a tranzakciok parhuzamosan futnak, amelyek nem nyulnak keresztbe egymasnak.

"Egyébként meg serializable szintnél kell a kódba egy ugyanolyan ellenőrzés az unable to serialize esetre"

Vagy altalanosabban: kell egy ellenorzes akarmilyen okbol torteno tranzakcio-abortalas esetere. Tranziens hibak akarmikor fellephetnek, tehat ilyen esetekben altalaban amugy sem hulyeseg ujraprobalni.

--
Fontos feladatot soha ne bizz olyan gepre, amit egyedul is fel tudsz emelni!

Szervusz !

az egyik feladatomnál egy ilyen jellegű problémát úgy oldottam meg, hogy indításnak kiadtam egy INSERT-et, és a felhasználó session_id-je + datetime kombináció MD5-jével kapott egy ideiglenes azonosítót. ezt követően erre az ideiglenes azonosítóra hivatkozva SELECTáltam, megkapva a rekord sorszámát (auto_increment)m és a következő körben végeztem el az általad vázolthoz hasonló paraméterek beírását.

Így nincs LOCK-olás, és _elvileg_ nem keveredik az adat, mert az ideiglenes kódolási algoritmus teljesen más jellegű karakterláncot ad, mint a végleges; emellett a session_id és az INSERTálás időpontja is behatárolja a felhasználót és kérését.

CSZ

"Gyakorlatilag viszont..."

Fu, mennyi ilyen esetet lattam, ahol valtig gyozkodtek, hogy gyakorlatilag nem fordulhat elo, mert nekik sose volt. Aztan hopp, egyszer megis elofordult. Es akkor csodalkoznak, hogy nem mindig a legprimitivebb megoldast valasztom, hanem a bonyolultabb, uzembiztosabbat.

----------------
Lvl86 Troll

Tisztelettel mondom, hogy ezt nem tudhatod. Nálam hasonló megoldásból 2 embernek napi párszáz tételéből évi 2 összeakadás volt és nem volt az inserteknél interakcióra várakozás vagy bármi ami miatt nagyobb lenne a valószínűsége.

Olvastam ezt a hash dolgot, szerintem felesleges hasht gyártani. Én egy sima autoincrement unique oszlopra gondolok, mint insertID, bedobálja az ember az adatokat ez alapján és a végén lockolva beszúrom a következő sorszámot. Lockolva lehetne mindjárt a sorszám alapján kezdeni, de ha az utolsó mozzanatként teszed be, akkor tudod azt is, hogy a teljes sor érvényes és befejezett insertek után van, szerintem ez hasznos.

Valami nem stimmel a feladat megfogalmazásában.
Szabál: Minden tételnek kell hogy legyen egyedi azonosítója - unique.
"mert van olyan hogy egy leltári számon kell tárolnom szorosan összefüggő termékeket" - akkor a felhasználónak, először ki kell hogy válassza melyik leletári számra is akarja testálni a tételt.
"Futó sorszám" - ha egyidejűleg visznek be több tételt, és folytonos sorszám ... mihez képest? Ha X felhasználó azt "látta" hogy amikor Ő bevitte a tételt, akkor ez a 9342 -es volt de miután frissült a kijelző akkor ez mégis a 9345 lett, akkor mi van? Az autoincrement (szerintem) csak úgy lehet "foghíjas" ha közben törölnek belőle - a "futó sorszámot" az autoincrementből kell származtatni, ha muszáj akkor az insert után egy update -el betenni.
Attól hogy tranzakcióba teszed, szerintem csak azt éred el, hogy ha nem jön össze akkor rollback?
Radikális megoldásként, azt lehetne csinálni, hogy a központi oldalon az insert kéréseket sorba állítod - nem ezt csinálja az SQL szerver?

* Én egy indián vagyok. Minden indián hazudik.

Mindent összefoglalva nekem eddig az INSERT-be ágyazott SELECT utasítás tűnik a legegyszerűbb járható útnak. Ebben az esetben ha jól értem nem fordulhat elő, hogy két felhasználóm ugyanazt a leltári számot próbálja beszúrni a táblába.

Kérlek javítsatok ki ha rosszul gondolom. Köszi!

Nem tudom, milyen izolációs szinteket tud a mysql, de általánosságban elmondható, hogy rosszul megválasztott szintnél a commitálatlan (A) inserttel párhuzamosan futó (B) insert select része ugyanazt a maxot látja és inkrementálja, mint az (A), jól megválasztott szintnél meg kénytelen megvárni, amíg (A) befejezi, és új maxot kap.

Szóval ahogy írta itt a kolléga, ez már nem a <? ?> területe.

Az elemek egyedi azonosítót kapjanak (itemID), és ezt rendeld hozzá a leltári számhoz. Mindkettő lehet autoincrement, a kapcsolótáblában meg a pároknak együttesen kell egyedinek lenni.

"a kapcsolótáblában meg a pároknak együttesen kell egyedinek lenni."

Hmm, ez nekem új. Ha jól értem valami ilyenre gondolsz:

------------------
|temékek |
------------------
|id |
|név |
------------------

------------------
|kapcsoló tábla |
------------------
|termek_id |
|leltari_szam |
------------------

utóbbi táblában a termék id mindig egyedi, mivel a termékek táblából jön ,ahol az egy auto increment érték.
A leltári szám viszont lehet azonos.

És a kapcsoló tábla sorainak kell unique-nak lenniük. Létezik ilyen hogy két (vagy több) mezőt együttvéve definiálok unique-nak?

Más adatbázis enginekben régóta használom, de hogy fest ennek a szintaxisa/deklarációja? Nem tudom hogy kell ezt SQL -ben deklarálni (restellem de nagyon kíváncsivá tettél).
Légyszíves írj egy példát - vagy link is jó lesz.

* Én egy indián vagyok. Minden indián hazudik.