codez Posted March 30, 2010 Report Share Posted March 30, 2010 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? Quote Link to comment Share on other sites More sharing options...
2easy Posted March 30, 2010 Report Share Posted March 30, 2010 (edited) 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 March 30, 2010 by 2easy Quote Link to comment Share on other sites More sharing options...
codez Posted March 30, 2010 Author Report Share Posted March 30, 2010 (edited) 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 March 30, 2010 by codez Quote Link to comment Share on other sites More sharing options...
2easy Posted March 30, 2010 Report Share Posted March 30, 2010 (edited) 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 March 30, 2010 by 2easy Quote Link to comment Share on other sites More sharing options...
codez Posted March 30, 2010 Author Report Share Posted March 30, 2010 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. Quote Link to comment Share on other sites More sharing options...
2easy Posted March 30, 2010 Report Share Posted March 30, 2010 (edited) 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 March 30, 2010 by 2easy Quote Link to comment Share on other sites More sharing options...
codez Posted March 30, 2010 Author Report Share Posted March 30, 2010 (edited) Notestēt var pavisam vienkārši, ieliec aiz SELECT 1 sekundes pauzi un tad requestus palaist "vienlaicīgi" nebūs problēma. Edited March 30, 2010 by codez Quote Link to comment Share on other sites More sharing options...
Aleksejs Posted March 30, 2010 Report Share Posted March 30, 2010 Vai šis nav tas gadījums, kad jādomā par transakcijām? Quote Link to comment Share on other sites More sharing options...
codez Posted March 30, 2010 Author Report Share Posted March 30, 2010 (edited) 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 March 30, 2010 by codez Quote Link to comment Share on other sites More sharing options...
Aleksejs Posted March 30, 2010 Report Share Posted March 30, 2010 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? Quote Link to comment Share on other sites More sharing options...
Kaklz Posted March 30, 2010 Report Share Posted March 30, 2010 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. Quote Link to comment Share on other sites More sharing options...
codez Posted March 30, 2010 Author Report Share Posted March 30, 2010 (edited) 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 March 30, 2010 by codez Quote Link to comment Share on other sites More sharing options...
codez Posted March 30, 2010 Author Report Share Posted March 30, 2010 (edited) Bank::transferMoney($user1, $user2, $n); kas jau, savukārt, ir normāla transakcija. Un kā tad izskatīsies iekšā tas Bank::transferMoney? (kveriji kādi?) Edited March 30, 2010 by codez Quote Link to comment Share on other sites More sharing options...
Gints Plivna Posted March 30, 2010 Report Share Posted March 30, 2010 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/ Quote Link to comment Share on other sites More sharing options...
codez Posted March 30, 2010 Author Report Share Posted March 30, 2010 (edited) 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 March 30, 2010 by codez Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.