我在信用日志表中跟踪用户信用,如下所示:
user quantity action balance
1001 20 SEND 550
1001 30 SEND 520
1001 5 SEND 515
现在,我首先尝试使用Active Record语法并选择最新余额,然后插入一个计算新余额的新行。然后我发现自己陷入了竞争状态:
user quantity action balance
1001 20 SEND 550
1001 30 SEND 520
1001 5 SEND 545 (the latest balance was not picked up because of a race condition)
下一个解决方案是使用单个查询来执行这两项操作:
INSERT INTO creditlog (action, quantity, balance, memberId)
VALUES (:action, :quantity, (SELECT tc.balance from creditlog tc where tc.memberId=:memberId ORDER by tc.id desc limit 1) - :quantity, :memberId);
我的脚本用10个reqs /秒来测试它会对2/10查询引发以下错误:
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction. The SQL statement executed was:
INSERT INTO creditlog (action, quantity, reference, balance, memberId)
VALUES (:action, :quantity, :reference, (SELECT balance from (SELECT tm.* FROM creditlog tm where tm.memberId=:memberId) tc where tc.memberId=:memberId ORDER by tc.id desc limit 1) -:quantity, :memberId, :recipientId);.
Bound with :action='send', :quantity='10', :reference='Testing:10', :memberId='10001043'.
引擎是否应该等待第一个操作释放表然后从第二个操作开始?
我的问题是否涉及:How to avoid mysql 'Deadlock found when trying to get lock; try restarting transaction'?
如何避免这种情况并将并发请求转换为顺序操作?
答案 0 :(得分:0)
这是一个有效的解决方案,可能不是最好的解决方案,请帮助改进。
由于事务不会阻止来自SQL SELECT的其他会话,因此我使用了以下方法:
LOCK TABLES creditlog WRITE;
//query 1 extracts oldBalance
"SELECT balance FROM creditlog where memberId=:memberId ORDER BY ID DESC LIMIT 1;";
//do my thing with the balance (checks and whatever)
//query 2
"INSERT INTO creditlog (action, quantity, balance, memberId)
VALUES (:action, :quantity, oldBalance- :quantity, :memberId);
UNLOCK TABLES;
结果:
mysql> select * from creditlog order by id desc limit 40;
+--------+-----------+----------+---------+----------+---------+---------------------+-------------+------------+
| id | memberId | action | quantity | balance | timeAdded |
+--------+-----------+----------+---------+----------+---------+---------------------+-------------+------------+
| 772449 | 10001043 | send | 10.00 | 0.00 | 2013-12-23 16:21:50 |
| 772448 | 10001043 | send | 10.00 | 10.00 | 2013-12-23 16:21:50 |
| 772447 | 10001043 | send | 10.00 | 20.00 | 2013-12-23 16:21:50 |
| 772446 | 10001043 | send | 10.00 | 30.00 | 2013-12-23 16:21:50 |
| 772445 | 10001043 | send | 10.00 | 40.00 | 2013-12-23 16:21:50 |
答案 1 :(得分:0)
解决方案#2
由于我已经在使用Redis,我尝试了一个redis互斥包装器: https://github.com/phpnode/YiiRedis/blob/master/ARedisMutex.php
这给了我更大的灵活性,允许我只锁定表的特定段(用户)。此外,没有死锁的风险,因为互斥锁在X(可配置)秒后自动过期。
这是最终版本:
$this->mutex = new ARedisMutex("balance:lock:".$this->memberId);
$this->mutex->block();
//execute credit transactions for this user
$this->mutex->unlock();