在 CodeIgniter 中处理并发请求

Posted

技术标签:

【中文标题】在 CodeIgniter 中处理并发请求【英文标题】:Dealing with concurrent requests in CodeIgniter 【发布时间】:2015-08-16 23:56:30 【问题描述】:

假设我使用 CodeIgniter/phpmysql 创建了一个在线银行系统,并且有以下内容可以从我的银行账户中提取资金:

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_accountwallet 表的存储引擎设置为具有事务支持的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_accontwallet 都转换为 InnoDB 并替换了我的 lock tables$this-&gt;db-&gt;trans_start() 和我的unlock tables$this-&gt;db-&gt;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中运行并发数据库连接[重复]

如何在golang中顺序处理并发请求? [复制]

使用 JWT 在 Code Igniter 4 中创建登录系统的过程

如何在 Django 中处理并发请求?

springboot 单例 怎么实现并发请求处理的

处理两个不同数据库时,Code Igniter 中 mysql pconnect 的正确设置