在 CodeIgniter 中处理并发请求
Posted
技术标签:
【中文标题】在 CodeIgniter 中处理并发请求【英文标题】:Dealing with concurrent requests in CodeIgniter 【发布时间】:2015-08-16 23:56:30 【问题描述】:假设我使用 CodeIgniter/php 和 mysql 创建了一个在线银行系统,并且有以下内容可以从我的银行账户中提取资金:
function withdraw($user_id, $amount)
$amount = (int)$amount;
// make sure we have enough in the bank account
$balance = $this->db->where('user_id', $user_id)
->get('bank_account')->balance;
if ($balance < $amount)
return false;
// take the money out of the bank
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
// put the money in the wallet
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
return true;
首先,我们检查用户是否可以执行提款,然后我们从账户中减去,然后我们添加到钱包中。
问题是我几乎可以同时发送多个请求(这对于使用curl
来说基本上是微不足道的)。每个请求都有自己的线程,所有线程都同时运行。因此,每个人都会检查我的银行是否有足够的钱(我会这样做),然后每个人都会进行取款。结果,如果我开始余额为 100,并且我同时发送两个 curl
请求导致提取 100,那么我的钱包中最终有 200 和银行帐户中的 -100,这应该是不可能的。
解决此类TOCTOU漏洞的正确“CodeIgniter”方法是什么?
【问题讨论】:
【参考方案1】:我将bank_account
和wallet
表的存储引擎设置为具有事务支持的InnoDB,然后在SELECT 语句中包含FOR UPDATE 子句以在持续时间内锁定bank_account
表交易。
然后代码将类似于以下内容。
function withdraw($user_id, $amount)
$amount = (int)$amount;
$this->db->trans_start();
$query = $this->db->query("SELECT * FROM bank_account WHERE user_id = $user_id AND balance >= $amount FOR UPDATE");
if($query->num_rows() === 0)
$this->db->trans_complete();
return false;
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
$this->db->trans_complete();
return true;
【讨论】:
根据文档 (codeigniter.com/userguide3/database/transactions.html),“他们要求您跟踪查询并根据查询的成功或失败确定是提交还是回滚”。因此,除非我弄错了,否则这将没有任何效果,因为所有命令都不会失败。 您误读了那句话并跳过了下一句...“这对于嵌套查询特别麻烦。相比之下,我们已经实现了一个智能交易系统,它会自动为您完成所有这些(您也可以如果您愿意,可以手动管理您的交易,但实际上没有任何好处)。” 我想我还是不明白。我不关心失败的查询和回滚,但关心的是check-then-withdraw 的非原子性质。我在之前的评论中链接到的 CI 文档对原子性或锁只字未提。事务是否保证原子性/句柄锁定/等?它如何知道要锁定哪些表? 根据 InnoDB 的文档,事务是原子的和隔离的,所以我想我应该继续测试它:我将bank_accont
和 wallet
都转换为 InnoDB 并替换了我的 lock tables
与$this->db->trans_start()
和我的unlock tables
与$this->db->trans_complete()
。我现在再次能够双重提款,因此可以确认数据库不将检查和提款视为原子操作。
@Mala,我在上面提供的 SELECT 语句中的 FOR UPDATE 至关重要。我上面的代码应该对 bank_account 表执行锁定。我应该在原始答案中突出显示 FOR UPDATE 以便更清楚。答案已更新。您是否在测试中包含 FOR UPDATE 子句?【参考方案2】:
我要找的是table locking。
为了保证这个函数的安全,我需要在函数开始时锁定表,然后在结束时释放它们:
function withdraw($user_id, $amount)
$amount = (int)$amount;
// lock the needed tables
$this->db->query('lock tables bank_account write, wallet write');
// make sure we have enough in the bank account
$balance = $this->db->where('user_id', $user_id)
->get('bank_account')->balance;
if ($balance < $amount)
// release the locks
$this->db->query('unlock tables');
return false;
// take the money out of the bank
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
// put the money in the wallet
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
// release the locks
$this->db->query('unlock tables');
return true;
这会使任何其他 MySQL 连接对所述表的写入尝试挂起,直到锁被释放。
【讨论】:
这是你的答案还是问题以上是关于在 CodeIgniter 中处理并发请求的主要内容,如果未能解决你的问题,请参考以下文章
如何在codeigniter普通mysql和自定义pdo中运行并发数据库连接[重复]