Mysql并发SELECT / INSERT

时间:2013-12-23 12:18:51

标签: mysql transactions race-condition

我在信用日志表中跟踪用户信用,如下所示:

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个req​​s /秒来测试它会对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'

如何避免这种情况并将并发请求转换为顺序操作?

2 个答案:

答案 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();