阻止同时访问数据库表行


Preventing simultaneous db table row access

我们的支持团队中有时会有两名管理员试图对db表行执行相同的敏感操作(比如修改行中的值)。我们需要防止这种情况发生。(行锁定是不可能的,因为表是"myisam")

我想到了几个解决方案:

在表单中设置旧值,并在提交时将其与当前值进行比较

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

然后在更新之前:

   $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;
   }

但可能存在以下情况:

  1. 用户需要将货币价值从9美元更改为10美元

  2. admin1将他的钱改为10$

  3. 用户聪明地花了1美元,所以他现在的钱又变成了9美元!

  4. admin2在没有任何警告的情况下将他的钱改为10美元。

在行中创建时间戳(updated_at列)设置

并执行与解决方案1中相同的操作。这样做的好处是,它所说的不仅仅是简单的数据比较。我们可以肯定地说,数据是在处理表单时更改的,还是没有。缺点是,除非我们将其与解决方案1 相结合,否则我们无法跟踪到底哪一列发生了更改

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

然后在更新时:

   $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;
   }

使用特定于对象/操作的名称创建临时0长度文件

在更新期间创建/锁定它,并检查其存在/更新前的日期戳。

更新前:

   $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);
   }

目前,创建文件是我的首选方法,因为:

  1. 我认为创建本地文件比访问数据库更快。

  2. 我不需要在表中再添加一列(时间戳)

  3. 我可以很容易地修改文件名来检查特定的列修改,即在mysqlupdate完成后创建文件"money_user{$userid}_modified"。

是这样吗?还是我误解了什么?

您可以在UPDATE操作的WHERE子句中指定旧值,然后查看受影响的行数:

给定

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

线程1执行

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

线程2执行

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

除此之外,我可能会更早地实现排除,首先将任务分配给各个管理员,以减少浪费的时间。

在您的情况下,我认为锁定是最好的方法。您可以使用MySQL锁:GET_LOCK、RELEASE_LOCK、IS_FREE_LOCK。在我看来,事务并不能保证当另一个进程对提取的数据执行任务时,该行不会发生更改。

尽管如此,您的特殊情况与传统意义上的锁定无关。IMHO,你需要用相应的凭据和描述记录你的交易,这样你的管理员就可以阅读它们,而不是重复同样的余额修改。锁定可以防止同时修改行,但在配音的情况下不能防止故意更改。

我认为数据库的行级锁定不能满足您在第一个方法中提到的情况。但我不认为创建文件比访问数据库系统快,也不认为。文件创建显然比数据库上的CRUD更重。

所以,我建议对日志表采用类似的方法。

  • 每个表都有自己的主键(如pid
  • 当有人试图篡改一行时,将表名和pid记录到带有时间戳的日志表中
  • 在运行查询之前,请检查日志表

看看InnoDB和事务。它们更适合敏感的变化(即平衡)。

数据库通常更好,因为它们是一个集中的解决方案。如果由于流量或通常的工作负载而不得不进行扩展,那么同步这些文件并不容易。除非你不希望有任何扩展需求,而且I/O速率很好,否则没关系。

请允许我提及两种可能的解决方案,你可能也在上面提到过。

您可以添加一个"assigned_id",其中包含您的管理员帐户id和时间戳,这样,如果其他人正在编辑应用程序,您的应用程序就会显示警告。

另一种可能的解决方案是在填写表格时检查是否有任何更改。此处可以使用last_eudited时间戳。