在CodeIgniter中处理并发请求


Dealing with concurrent requests in CodeIgniter

假设我使用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"方法是什么?

我将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;
}

我要找的是表锁定。

为了使这个函数安全,我需要在函数开始时锁定表,然后在结束时释放它们:

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连接对所述表的写操作都挂起,直到锁被释放。