Question

It sometimes happens that two admins in our support team are trying to do the same sensitive operation on db table row (let's say, modifying the value in the row). We need to prevent that. (Row locking is not possible because tables are "myisam")

I have thought of several solutions:

setting the old value in the form and comparing it with the current one on submit

   <input name="money"><input type="hidden" name="old_money" value="10">

and then before updating:

   $currentmoney=value_from_query("select money from mytable","money");
   if($currentmoney!=$_REQUEST["old_money"]){
      return "value changed to $currentmoney while you were editing it, are you sure you still want to change it?!??!?!?!?";
   }
   else{
     mysql_query("update everyonesmoney set money='".intval($_REQUEST["money"])."' where user='$user_id'");
     return true;
   }

but there can be following situation:

  1. user needs money value to be changed from 9$ to 10$

  2. admin1 changes his money to 10$

  3. user smartly spends 1$, so his current money becomes 9$ again!

  4. admin2 changes his money to 10$ with no warning.

creating timestamp (updated_at column) setting in the row

And doing same as in solution 1. This has advantage that it's saying more than simple data comparison. We can say for sure if data was changed while we were fiddling with the form or no. disadvantage - we cannot track which column exactly was changed, unless we combine it with solution 1

   <input type="hidden" name="formtimestamp" value="<? echo time();?>">

and then while updating:

   $query_add = ($overriden ? "" : " and updated_at>'".securevalue($_REQUEST["formtimestamp"])."'");
   if(mysql_affected_rows(mysql_query("update everyonesmoney set money='".intval($_REQUEST["money"])."', updated_at=NOW() where user='$user_id' ".$query_add))==0){
      return "some values were changed by someone else while you were editing it, are you sure you still want to change it?!??!?!?!?";
   }
   else{
     return true;
   }

creating the temporary 0-length file with object/action-specific name

Creating/locking it during the update, and checking for its existence/datestamp before update.

Before update:

   $myfname="/tmp/user{$user_id}EDITMONEY.tmp";
   $timedifference=((time()-filectime($myfname)); //in seconds
   if(file_exists($myfname) and ($timedifference<60) and (!$overriden)){ // a minute difference
      $currentmoney=value_from_query("select money from mytable","money");
      return "money were edited by someone else $timedifference seconds ago and set to {$currentmoney}, are you sure you still want to change it?!??!?!?!?";
   }else{
      $fp = fopen("/tmp/user".intval($_REQUEST["user_id"])."EDITMONEY.tmp", "r+");         
      if (flock($fp, LOCK_EX)) { // do an exclusive lock
         mysql_query("update everyonesmoney set money='".intval($_REQUEST["money"])."' where user='$user_id'")
        flock($fp, LOCK_UN); // release the lock
        return true;
     } else {
        return "Couldn't get the lock, it's possible that someone tried to execute query simultaneously!";
     }

   fclose($fp);

   }

For now file creation is my preferred approach because:

  1. I think it's faster to create local file than access database.

  2. I don't need to add one more column(timestamp) to the table

  3. I can easily modify the filename to check for specific column modification, ie create file "money_user{$userid}_modified" when mysqlupdate is done.

Is that right or is there something I misunderstand?

Was it helpful?

Solution

You can specify the old value in the UPDATE operation's WHERE clause, and then look at the number of rows affected:

Given

id  name          amount
--- ------------- ---------
1   Joe User      10

Thread 1 executes

UPDATE accounts SET amount=9 WHERE id=1 AND amount=10;
=> Query Okay, 1 row(s) affected

Thread 2 executes

UPDATE accounts SET amount=9 WHERE id=1 AND amount=10;
=> Query Okay, 0 row(s) affected

Other than that, I'd probably implement the exclusion a bit earlier, by assigning tasks to individual admins first, in order to reduce the amount of time wasted.

OTHER TIPS

In your case I suppose locking is the best approach. You can use MySQL locks: GET_LOCK, RELEASE_LOCK, IS_FREE_LOCK. Transactions in my opinion does not guarantee that row would not changed while another process performing its task on fetched data.

Although, your particular case has nothing to do with locking in traditional sense. IMHO, you need to log your transactions with corresponding credentials and descriptions, so your admins could read them and not to dub the same balance modification. Lock can protect from simultanious row modification, but not from intentional change in case of dubbing.

I think database's row-level locking is not fulfill the situation that you mentioned in first method. But I don't think the file creation is faster than accessing database system, neither. File creation is obviously heavier than CRUD on database.

So, I suggest a similar approach with logging table.

  • Every table has its own primary key (like pid)
  • Record the table name and the pid into log table with timestamp when someone attempt to fiddle a row.
  • Check the log table before running a query.

Have a look at InnoDB and transactions. They are more suitable for sensitive changes (ie balance).

Databases are generally better since they are a centralized solution. If you have to scale because of traffic or generally work load, it is not easy to sync those files. Unless you don't expect any need for scaling and I/O rates are good, it is ok.

Allow me to mention 2 possible solutions, which you might also have mentioned above.

You could add an "assigned_id" with the id of your admin account combined with a timestamp so that your application will show a warning if someone else is editing it.

Another possible solution is to check if any changes have been made while you were filling your forms. A last_edited timestamp could be utilized here.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top