Records worden soms te laat opgeslagen

    This site uses cookies. By continuing to browse this site, you are agreeing to our Cookie Policy.

    • Records worden soms te laat opgeslagen

      Hallo,

      Ik heb het volgende probleem:
      De records worden soms niet op tijd opgeslagen wanneer een gebruiker erg snel handelingen uitvoerd.

      Stel gebruiker Henk heeft 1.000 op zak, en klikt rustig op een bepaalde optie (Die bijvoorbeeld 100 kost) en gaat de gebruiker steeds 100 omlaag.
      Maar wanneer dit erg snel gebeurt is soms de check of je meer als 100 hebt geldig, terwijl de speler vervolgens op een negatief saldo komt.

      PHP Source Code

      1. <?php
      2. if($gebruikerinfo->contant < 75000){
      3. echo "Je hebt niet genoeg geld op zak.";
      4. // Hier nog meer code
      5. exit();
      6. }
      7. mysqli_query($connection, "update users set `contant`=`contant`-75000 where user='".$ingame."'");
      8. // Hier nog meer code
      9. if($gewonnen){
      10. mysqli_query($connectie, "update users set `contant`=`contant`+250000 where user='".$ingame."'");
      11. }
      12. ?>
      Display All
      Hierboven even een kort voorbeeldje van de situatie.

      Er word gebruik gemaakt van een MyISAM tabel in MySQL.
      Het veld dat word aangepast betreft een INT veld.
      Er word gebruik gemaakt van een VPS, dus alle settings zijn aan te passen in de configs.

      Er is maar 1 persoon actief op een account, dus er zouden niet meerdere aanvragen tegelijk moeten komen.
      Echter als een speler erg snel de pagina gaat vernieuwen lijkt het erop dat deze soms tegelijk worden uitgevoerd.

      Zelf vermoed ik dat het het mogelijk aan locking (Of het gebrek aan) ligt, of dat het ophalen van de gebruikersgegevens word gecached.
      $gebruikerinfo is een verzameling van gegevens die uit de database word gehaald aan het begin van de pagina laden, is het mogelijk dat deze read opdracht word gecached omdat het in dezelfde seconde (Of erg korte periode) plaats vind als de vorige aanvraag ?

      Hoe zou ik het beste het probleem kunnen vinden ?
      En waarop zou ik kunnen zoeken om tot de juiste functie of instelling te komen om dit probleem op te lossen ?

      Alvast bedankt,
      Pekelterror
    • Ah, een klassiek voorbeeld van een gebrek aan ondeelbaarheid van (database-)operaties.

      Dit heeft niet zozeer te maken met de manier van programmeren (deze doet er echt niet toe) maar meer met hoe je omgaat met je database. Je hebt hiervoor echt een soort van locking mechanisme nodig en idealiter gebruik je hiervoor transacties. Jammergenoeg worden (database-)transacties niet ondersteund door MyISAM.

      Ik weet verder niet wat voor applicatie dit betreft (mafia game?) maar meestal hebben deze alle kenmerken van een "administratief" systeem - een database met een veelvoud aan tabellen met daartussen allerlei verbanden.

      Wat dat betreft snap ik sowieso niet waarom je daarvoor ooit MyISAM zou willen gebruiken omdat er met die engine geen onderlinge afstemming tussen tabellen kan plaatsvinden. Je kunt dan eigenlijk met geen goed fatsoen verwachten dat alle data in die tabellen (onderling) kloppend is en (onderling) kloppend blijft. Alle data hangt letterlijk als los zand aan elkaar. Voor een administratief systeem ben je veel beter af met InnoDB. Die heeft namelijk al deze voorzieningen (transacties, foreign keys et cetera) wel.

      Wat natuurlijk ook niet meehelpt is dat je op verschillende manieren refereert aan het saldo contant geld. Je hebt $gebruikerinfo->contant, een property van een of ander object? en users.contant. En dan zit het user id blijkbaar in $ingame, die zou ik dan eerder verwachten in $gebruikerinfo->id ofzo. Ik heb een onderbuikgevoel van spaghetticode.

      Dan speelt er mogelijk nog het volgende: je refresht de pagina waarschijnlijk niet direct, anders zou dit minder snel (maar nog steeds, het is geen remedie) voor problemen zorgen, dus waarschijnlijk ben je tegelijkertijd een of ander formulier of andere data aan het verwerken terwijl je vrolijk een pagina aan het uitdraaien bent. Ben ik een beetje warm? Je bent dus waarschijnlijk zogenaamde "state changes" on-the-fly aan het uitvoeren wat uit algemeen oogpunt van programma-ontwerp een heel slecht idee is.

      Betreft dit legacy code of ben je dit op dit moment aan het ontwikkelen?
    • Wat je sowieso kan doen, maar is meer een plakbandje. Is ook tijdens het uitvoeren van de query controleren of de gebruiker wel genoeg geld heeft.

      SQL-Query

      1. mysqli_query($connection, "update users set `contant`=`contant`-75000 where user='".$ingame."' AND contant >= '75000'");
    • @FangorN Bedankt voor je bericht.
      Het beste zou ik dus kunnen omschakelen van MyIsam naar InnoDB ?
      En dan transacties gebruiken ?

      Het betreft inderdaad een mafia game.

      $gebruikerinfo is een fetch_object van een X aantal velden die uit de gebruiker worden opgehaald van de user.
      Dit omdat we elke aanvraag een X aantal sowieso al nodig zijn.

      $ingame bevat de session user (escaped) zodat ik deze makkelijk overal kan gebruiken.
      Dit zou uiteindelijk dus $gebruikerinfo->user kunnen worden, maar die waarde staat toch gelijk aan $ingame aangezien $ingame (Oftewel de $_SESSION['user'] escaped) word gebruikt in de Select query die word gebruikt bij $gebruikerinfo.

      We zien door de logging dat er soms 5 aanvragen per seconde worden gedaan naar de pagina die dit probleem geeft.
      Er word niet in andere formulieren tegelijk gewerkt, maar de verwerking gaat vermoedelijk te langzaam voor de volgende aanvraag weer komt.

      Het bevat wel veel legacy code aangezien er op een eerder project is verder gewerkt dat ik jaren terug heb gemaakt.
      Had alleen niet nagedacht over de types engines van de database en locking, die was ik (in mijn ogen) nooit nodig, maar zal er toch eens naar kijken.
      Gelukkig kan ik die altijd nog aanpassen, zal alleen even moeten kijken of MyIsam naar InnoDB geen problemen oplevert (Maar verwacht ik niet).

      @Luc Bedankt voor het meedenken.
      Alleen zou dit het probleem erger maken.
      Nu gaat de gebruiker na een tijdje in de min, tot de database weer bij is, dit moeten ze dan weer aanvullen tot ze in de plus zitten.
      Met jou kleine fix zou juist de afschrijving misschien nog een keer minder gebeuren.

      TLDR; Ik zal me eens inlezen op het verschil van InnoDB/MyIsam, een locking mechanisme en transacties.
      Hebben jullie (Indien er meerdere soorten zijn), nog een locking mechanisme of transactie methode die jullie persoonlijk graag gebruiken, of zouden aanraden in deze situatie ?
    • pekelterror wrote:

      $ingame bevat de session user (escaped) zodat ik deze makkelijk overal kan gebruiken.
      Oef. Ik weet niet of ik dat zou doen. Dit klinkt namelijk sterk als "escape on input". En escaping van data hangt af van de context. Waarvoor is dit (op voorhand) ge-escaped dan? Voor SQL? Voor HTML? Voor alles?

      Indien het de bedoeling is dat data ontdaan moet worden van een mogelijk speciale betekenis in een bepaalde context, dan doe je dat tijdens het gebruik van deze data in die context. Het is zaak dat je output escaped.

      Nu is het nogal ongewis dat $ingame ge-escaped is (en voor welke context precies). Als je dit altijd consequent doet tijdens het gebruik dan is dat compleet ondubbelzinnig en hoef je niet elke keer na te denken of na te gaan of dit wel het geval was. Nu moet je elke keer nadenken "is dit veilig of niet". Maak het jezelf makkelijk en escape gewoon consequent alle DATA in je SQL. Of maak gebruik van prepared statements ofzo, in welk geval je beter PDO kunt gebruiken want prepared statements in MySQLi zijn nogal een gribus.

      Trouwens, de enige informatie die een sessie qua user data hoeft te propageren is in principe het user id. Alle actuele user data zou elke page request opnieuw berekend moeten worden om ervoor te zorgen dat deze altijd up-to-date is. Die zou je dan in een user object kunnen stoppen waar je vervolgens aan refereert bij gebruik in je code, zoals je nu ook al doet met via $gebruikerinfo.

      Ik weet niet hoeveel user data er nu in de sessie zit? Als je op die manier gebruikersgegevens tussen requests propageert introduceer je een synchronisatieprobleem. Immers, elke keer dat er iets in de user data verandert zou je dit moeten updaten in je sessie. En stel nu dat een admin rechten van een (andere) gebruiker intrekt, wanneer wordt dit dan gereflecteerd in de sessie van die gebruiker zelf? Je kunt namelijk niet rechtstreeks andermans sessie inhoudelijk wijzigen.

      Bedien je van het motto: filter input, escape output, en je komt een heel eind. Mits je weet wat dit inhoudt :p.

      Escape-on-input is heel vaak geen goed idee, maar zoals met alles zijn er ook uitzonderingen. Denk bijvoorbeeld aan "session flash messages". Stel dat je na het bijwerken van een artikel "bladibla" iemand direct redirect naar het overzicht van artikelen, maar dat je wel op een of andere manier een terugkoppeling wilt geven dat het artikel succesvol is gewijzigd. Deze informatie zou je tijdelijk in (een subarray van) je sessie kunnen stoppen waarin het bericht "artikel 'bladibla' succesvol bijgewerkt" staat. Je weet in dat geval dat dit bericht in de HTML-context gebruikt gaat worden, en je wilt niet dat je complete pagina breekt als je voor de gein </div> in je titel zet ofzo. Dus in dat geval is het geoorloofd om op voorhand output te escapen. Maar dit is dus een uitzondering op de regel.

      Als je niet op voorhand weet in welke context data gebruikt gaat worden heeft het ook geen zin om te escapen en is het ook niet verstandig om deze data op voorhand voor alle mogelijke contexten onschadelijk te maken. Dit kan zelfs fouten in de hand werken zoals het bovenstaande artikel mooi illustreert :).

      The post was edited 1 time, last by FangorN ().

    • Het is inderdaad escape on input.
      $ingame bevat een session waarde (Die gezet word bij het inloggen, dus zou ivm een match al veilig moeten zijn).

      Ondanks dat escape ik het toch liever voor het gebruik van de querys.

      PHP Source Code

      1. <?php
      2. $ingame = mysqli_real_escape_string($connectie, $_SESSION['user']);

      Deze $ingame word verder niet gebruikt om het te tonen, dat gaat eigenlijks via het $gebruikerinfo 'object' (een fetch_object van een query), en deze bevat dan een heleboel waardes die vaak worden gebruikt zoals ze in de database staan van de gebruiker.

      Ik ben in elk geval alvast overgestapt op InnoDB, maak gebruik van SQL_NO_CACHE in de select query voor $gebruikerinfo om te zorgen dat ik de meest recente versie heb, en heb enkele onnodige querys verwijderd uit de scripts in de hoop dat het daardoor minder zwaar is.
      Verder schrijf ik nu wel wat debug weg om te kijken of dit probleem zich nog voordoet.

      Het lijkt op dit moment goed te gaan.
      Mocht het toch nog weer gebeuren zal ik je advies over Locking en Transacties (aangezien we nu toch InnoDB gebruiken) beter bestuderen.

      Bedankt voor de hulp!