我有一个游戏网站,我想更新用户的钱,但如果我在同一时间使用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();
感谢任何帮助。
答案 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_dt
和DATETIME(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;解决问题不是正确的方法。并且按照我在示例中演示的方式进行检查,并没有告诉我们购买不成功的原因(资金不足或在之前购买的指定时间范围内。)