Jump to content
php.lv forumi

Kā pareizāk veidojat db modeļus?


codez

Recommended Posts

Ir radusies viena dilemma ar kaut cik universāla modeļa izveidi.

Aplikācija ar datiem pārsvarā strādā trīs veidos

 

1)read->modify->display

2)read->modify->save->display

3)read->modify->save

 

3. veids parasti tiek izmantots ajax pieprasījumu apstrādē, kad lietotājs maina kādus datus un ir jau pie 'read' soļa zinām, ka dati tiks saglabāti, tāpēc var uztiasīt read ar row locking.

 

Vislielākā problēma ir 1. un 2. variants, kur tikai 'modify' solī noskaidrojās, vai dati ir jāsaglabā vai nav, bet no tā ir atkarīgs kā dati jālasa no db.

 

1. gadījumā tie jālasa ar

 SELECT * FROM users WHERE id=123

 

2. gadījumā tie jālasa ar

 SELECT * FROM users WHERE id=123 FOR UPDATE

lai notiktu row lockings un kāds cits pieprasījums pa to starpu (...->modify->save)nevarētu izmainīt datus.

 

FOR UPDATE visu laiku lietot būtu ļoti nefektīvi, jo 1. gadījums ir >90% no visām griezšanās reizēm pie modeļa.

 

Vai kāds ir sastapies ar šādu problēmu?

Kādi būtu jūsi ieteikumi to risināt?

Link to comment
Share on other sites

  • Replies 35
  • Created
  • Last Reply

Top Posters In This Topic

Top Posters In This Topic

hmm, ja jau 2. gadījums ir rets, tad vnm lasi kā 1. gadījumā, bet ja atklājas, ka vajag save, tad uztaisi vēl vienu select ar to FOR UPDATE

 

protams, ka neko gudru atkal nepateicu, taču jāoptimizē ir tā, lai biežāk lietotā darbība būtu visātrākā

 

a varbūt tajā gadījumā var iztikt vsp bez lokošanas? mosh tā tabula varētu būt parasta MyISAM, nevis InnoDB?

Edited by 2easy
Link to comment
Share on other sites

Piemēram, ir kods, kurš veic naudas pārsūtīšanu.

 

$n=100;
$user1 = new User();
$user2 = new User();

if ($user1->loadById(1) && $user2->loadById(2)){
 if ($user1->money>=$n) {
   $user1->addMoney(-$n);
   $user2->addMoney($n);
   $user1->save();
   $user2->save();
 }
}

 

Gadījums, kad vienlaicīgi useris 1 un 2 sūta naudu 3-šajam.

users

id | m

1 | 200

2 | 300

3 | 400

 

konekcija 1

SELECT * FROM users WHERE id=1

1 | 200

 

konekcija 2

SELECT * FROM users WHERE id=2

1 | 300

 

konekcija 1

SELECT * FROM users WHERE id=3

1 | 400

 

konekcija 2

SELECT * FROM users WHERE id=3

1 | 400

 

 

konekcija 1

atņēma userim 1 100 naudas vienības un saglabā. Paliek 200-100=100

UPDATE users SET m=100 WHERE id=1

 

konekcija 2

atņēma user 2 100 vienības. Paliek 300-100=200

UPDATE users SET m=200 WHERE id=2

 

konekcija 1

pielika userim 3 100 naudas vienības un saglabā. Paliek 400+100=500

UPDATE users SET m=500 WHERE id=3

 

konekcija 2

pielika userim 3 100 naudas vienības un saglabā. Paliek 400+100=500

UPDATE users SET m=500 WHERE id=3

 

Rezultātā

users

id | m

1 | 100

2 | 200

3 | 500

 

No sistēmas pazudušas 100 naudas vienības, respektīvi 3 userim bija jābūt 600.

Tā, ka bez lockinga nekādi.

Edited by codez
Link to comment
Share on other sites

jaa ar naudu tā nevar jokoties :D:D:D

 

paldies, tas vsp ir labs piemērs par lokošanas vajadzību/pielietojumu ;)

 

vienīgi, ja grib uztaisīt reālu testu, lai patiešām tas būtu vienlaicīgi, tad kā to var izdarīt? imho, pat vienlaicīgi laižot abus kverijus, tie kkā saliekas secīgi, jo katrs notiek ļoti ātri :(

cik vsp tā varbūtība ir liela, ka kkas patiešām notiek vnlaicīgi? un kā to notestēt. tipa real code, real example?

 

tavā piemērā ir tas pats thread/process, tāpēc tad vēl tā laikam var, taču ja nāk 2x requesti, kā tad to notestēt?

Edited by 2easy
Link to comment
Share on other sites

Kas attiecas uz to, ka 'save' gadījumā tiek uztaisīts datu 'read' ar lockingu, tad sanāk diezgan muļķīgi, jo principā visa modify daļa ir atkal jāveic pa jaunu, jo tie dati, kamēr tas lock navis bijis varēja jau izmainīties un līdz ar to aprēķini būt savādāki.

Link to comment
Share on other sites

tad sanāk diezgan muļķīgi, jo principā visa modify daļa ir atkal jāveic pa jaunu, jo tie dati, kamēr tas lock navis bijis varēja jau izmainīties un līdz ar to aprēķini būt savādāki

nju bet tāda ir dzīve...

ni4ego ne podelae6 :D:D:D

 

pats teici, ka tas ir daudz retāk. un 90% ir parastie selekti bez save/update, kurus būtu neizdevīgi lokot. tā ka izvēlies labāko no diviem "sliktajiem" risinājumiem :P vismaz izdari tikai minimālos aprēķinus, pirms noskaidro, ka vajadzēs pārprasīt ar lock un pārrēķināt. protams, atnāks... hmm kas varētu atnākt un pateikt, kā būtu labāk? :D

Edited by 2easy
Link to comment
Share on other sites

Izmantoju transakcijas defaultā, bet transakcijas nodrošina tikai to, ka vienas konekcijas ietvaros db ir nemainīga, nevis row-u lokošanu starp dažādām konekcijām.

Tāpēc mans demonstrētais variants ar naudas pārsūtīšanām strādā(nestrādā) tāpat arī pie transakciju izmantošanas.

 

Galvenā doma šeit ir, ka kamēr iet cikls read->modify->save, neviens cits tāds pats cikls nedrīkst nolasīt tos pašus datus, tāpēc tas row-s tiek lock-ots, bet kā jau rakstīju iepriekš, pie lasīšanas ne vienmēr var zināt, vai dati būs jāsaglabā vai nē, tāpēc sanāk vai nu taisīt vienmēr lockošanu, kas nozīmē ļoti daudz lieku nolockotu rowu, vai arī kā ieteica 2easy, ja sastopas, ka dati ir jāmaina, tad lasa vēlreiz ar lockošanu.

 

Bet tad principā kods sāk palikt tāds pavisam nesmuks - tas pats piemērs ar naudas pārsūtīšanu

 

$n=100;
$user1 = new User();
$user2 = new User();

if ($user1->loadById(1) && $user2->loadById(2)){
 if ($user1->money>=$n) {
   if ($user1->loadById(1,FORUPDATE) && $user2->loadById(2,FORUPDATE)){
     if ($user1->money>=$n) {
       $user1->addMoney(-$n);
       $user2->addMoney($n);
       $user1->save();
       $user2->save();
     }
   }
 }
}

Pie tam šis ir vienkārš variants, kurā tas vai dati būs jasaglabā nosakās vienā vietā - pārbaudē vai $user1 ir pietiekami naudas, taču man praksē ir gadījumi, kad šādas vietas 'modify' daļā ir desmit un vairāk.

Edited by codez
Link to comment
Share on other sites

Nu jā... tas mani arī tā kā ir interesējis - kā to dara PHP. Teiksim Javiskajās webaplikācijās ir aplikāciju serveris, kas visas konkrētās lietotāja sesijas ietvaros tur vaļā pieslēgumu datu bāzei - līdz ar to ir iespējams uzsākt transakciju, nolasīt datus, aizsūtīt lietotājam uz outputu un (PHP gadījumā pēc aizsūtīšanas parasti pieslēgums tiek iznīcināts un datiem pienākot te sākas viss no jauna) saņemt datus atpakaļ un saglabāt datubāzē, pabeidzot transakciju. Kā to dara PHP? Kaut kā sesijā glabā atvērtās transakcijas kaut kādu identifikatoru?

Link to comment
Share on other sites

kā būtu iesākumam aizmirst par šo:

       $user1->addMoney(-$n);
       $user2->addMoney($n);
       $user1->save();
       $user2->save();

 

un tā vietā lietot

 

Bank::transferMoney($user1, $user2, $n);

 

kas jau, savukārt, ir normāla transakcija.

Link to comment
Share on other sites

Man šķiet, ka tavā piemērā JAVĀ arī ar transakcijām vien nepietieks, jo notiks tas pats, ko es nodemonstrēju piemērā ar diviem paralēliem requestiem pārsūtīt naudu 3. lietotājam.

 

Abi requesti nolasa, ka 3.lietotājam nauda ir 400, kamēr neviens no viņiem vēl nav veicis update.

Tad abi palielina šo 400 par 100 uz 500 un viens pēc otra pārglabā 400 vietā 500. Kaut arī pēc abām operācijām vajadzēja būt 600.

Edited by codez
Link to comment
Share on other sites

Kā jau Aleksejs teica šis noteikti varētu būt tas gadījums, kad jādomā par transakcijām, jo ir svarīgi, ka izpildās _____vai nu abi____ SQL teikumi ____vai neviens____.

Vispār mani šausmina šī MySQL pieeja, ka noklusēti transakciju nav :S

Otra lieta, ka iespējams šādos gadījumos var lietot tā saucamo optimistisko bloķēšanu (optimistic locking). Parasti tas ir vai nu timestamp kolona (nedrošāks risinājums) vai monotoni augoša integer kolona, kurā pie katras izmaiņas ieraksta nosacītu transakcijas idu.

Tad atlasam datus nolasot arī transakcijas ida kolonu un to atceramies.

Tad veicot izmaiņas, WHERE klauzā jāraksta ne tikai WHERE id = X, bet jāraksta

 

SET transaction_id = transaction_id + 1

WHERE id = X AND transaction_id = T.

 

Ja updeits ir noticis, tad viss štokos, neviens cits lietotājs neko nav pamainījis. Ja updeits nav noticis, tad izmet lietotājam kļūdu, ka kāds cits, kamēr viņš blenza ekrānā vai dzēra kafiju, šo ierakstu jau ir pamainījis, lai atjauno ekrānformu.

 

Ja ir arī transakcijas, tad ir vērts transakcijai sākumā piekārtot db līmeņa transakcijas_id, kas visu laiku aug. Oraclē to viegli ir izdarīt izmantojot sekvenci, MySQLā tāda objekta diemžēl nav, bet var izmantot varbūt vienu tabulu, kurā ir viens monotoni augošs ieraksts vai kaut ko tamlīdzīgu.

 

Timestamp kolona ir nedrošāks risinājums tāpēc, ka teorētiski vienā sekundē vai pat tās daļā ļoti mierīgi var notikt vairākas izmaiņas.

 

Gints Plivna

http://datubazes.wordpress.com/

Link to comment
Share on other sites

Kā jau teicu, defaultā izmantoju transakcijas un innodb engini, bet tas neatrisina šo problēmu (kuru nodemonstrēju ar naudas transfērošanu), rowi ir jāloko vienalga, bet ir otra problēma, ka updeitošana reāli notiek mazāk par 10% pretī visiem selektiem, kas nozīmē, ka liekot defaultā lokošanu notiek visamz 10x vairāk lieku locku nekā būtu nepieciešams.

 

Tāpat arī runa nav par to, ka kaut kādi dati nonāk pie lietotāja, paiet laiks un tad tos updeito. Runa ir par parastu requestu, kurā notiek read->modify->save darbības, precīzāk par šādiem paralēliem requestiem un viņu db konekcijām, kur viena konekcija nedrīkst lasīt kādus datus, ja kāda cita konekcija šos datus ir nolasījusi, bet vēl nav saglabājusi.

Edited by codez
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

×
×
  • Create New...