PHP PDO事务复制

时间:2017-11-22 05:01:23

标签: php mysql pdo

我有一个游戏网站,我想更新用户的钱,但如果我在同一时间使用2个电脑,这个代码将执行两次,用户将留下负钱。我怎么能阻止这种情况发生?它让我发疯了。

$db = getDB();  
$sql = "UPDATE users SET money = money- :money WHERE username=:user";
$stmt = $db->prepare($sql);                                  
$stmt->bindParam(':money', $amount, PDO::PARAM_STR);       
$stmt->bindParam(':user', $user, PDO::PARAM_STR);       
$stmt->execute(); 

感谢任何帮助。

3 个答案:

答案 0 :(得分:0)

负余额将代码更改为

$sql = "UPDATE users SET money = money- :money WHERE username=:user AND money >= :money";

答案 1 :(得分:0)

第一个想法:

如果您使用的是InnoDB,则可以使用事务来提供细粒度的互斥。例如:

START TRANSACTION;
UPDATE users SET money = money- :money WHERE username=:user;
COMMIT;

如果您正在使用MyISAM,则可以使用LOCK TABLE来阻止B访问该表,直到A完成更改。例如:

LOCK TABLE t WRITE;
UPDATE users SET money = money- :money WHERE username=:user;

第二个想法:

如果更新不起作用,您可以删除并插入新行(如果您有自动增量ID,则不会有重复项。)

答案 2 :(得分:0)

回应@GarryWelding的评论:数据库更新并不是代码中处理所描述用例的合适位置。在用户表中锁定一行不是正确的修复。

备份一步。听起来我们想要对用户购买进行细致的控制。好像我们需要一个存储用户购买记录的地方,然后我们可以检查一下。

如果不深入数据库设计,我会在这里抛出一些想法......

除了"用户"实体

user
   username
   account_balance

似乎我们对用户购买的一些信息感兴趣。我摒弃了一些关于我们可能感兴趣的信息/属性的想法,而不是声称这些都是您的用例所需要的:

user_purchase
   username that made the purchase
   items/services purchased
   datetime the purchase was originated
   money_amount of the purchase
   computer/session the purchase was made from
   status (completed, rejected, ...)
   reason (e.g. purchase is rejected, "insufficient funds", "duplicate item"

我们不想尝试跟踪"帐户余额中的所有信息"特别是因为用户可以进行多次购买。

如果我们的用例比这简单得多,并且我们只跟踪用户最近的购买,那么我们可以在用户实体中记录。

user
  username 
  account_balance ("money")
  most_recent_purchase
     _datetime
     _item_service
     _amount ("money")
     _from_computer/session

然后,每次购买,我们都可以记录新的account_balance,并覆盖之前的"最近的购买"信息

如果我们所关心的只是防止多次购买"同时",我们需要定义......这是否意味着在同一精确的微秒内?在10毫秒内?

我们是否只想防止"重复"从不同的计算机/会话购买?在同一个会话中有两个重复请求怎么办?

这是我将如何解决问题。但是要回答你问的问题,如果我们选择一个简单的用例 - "防止两个毫秒内的两次购买",我们希望在UPDATE {{{}}中执行此操作1}}表

给出如下的表定义:

user

使用用户表中记录的最近购买的日期时间(下至微秒)(使用数据库返回的时间)

user
  username                 datatype    NOT NULL PRIMARY KEY 
  account_balance          datatype    NOT NULL
  most_recent_purchase_dt  DATETIME(6) NOT NULL COMMENT 'most recent purchase dt)

然后我们可以检测到该语句影响的行数。

如果我们将零行影响,则找不到UPDATE user u SET u.most_recent_purchase_dt = NOW(6) , u.account_balance = u.account_balance - :money1 WHERE u.username = :user AND u.account_balance >= :money2 AND NOT ( u.most_recent_purchase_dt >= NOW(6) + INTERVAL -1000 MICROSECOND AND u.most_recent_purchase_dt < NOW(6) + INTERVAL +1001 MICROSECOND ) ,或:user大于帐户余额,或:money2在+ /的范围内 - 现在1毫秒。我们无法分辨哪个。

如果影响的行数超过零,那么我们就知道发生了更新。

修改

强调一些可能被忽视的关键点......

示例SQL期望支持小数秒,这需要MySQL 5.7或更高版本。在5.6及更早版本中,DATETIME分辨率仅下降到第二。 (注意示例表中的列定义和SQL将分辨率指定为微秒...... most_recent_purchase_dtDATETIME(6)

示例SQL语句期望NOW(6)username表中的PRIMARY KEY或UNIQUE键。在示例表定义中注明(但未突出显示)。

示例SQL语句会覆盖user对于彼此 1毫秒内执行的两个语句的更新。要进行测试,请将毫秒级分辨率更改为更长的时间间隔。例如,将其更改为一分钟。

也就是说,将user的两次出现更改为1000 MICROSECOND

其他一些注意事项:使用60 SECOND代替bindValue(因为我们正在为语句提供值,而不是从语句中返回值。

还要确保PDO设置为在发生错误时抛出异常(如果我们不检查代码中PDO函数的返回),那么代码就不会将其放入&#39 ; s(比喻)小拇指指向我们口中的Dr.Evil风格&#34;我只是假设它将全部去计划。什么&#34;?)

bindParam

并强调我之前提出的观点,&#34;锁定行&#34;解决问题不是正确的方法。并且按照我在示例中演示的方式进行检查,并没有告诉我们购买不成功的原因(资金不足或在之前购买的指定时间范围内。)