php+swoole开发笔记之-协程锁

310 阅读2分钟

问题提出

我们知道,在PHP+Swoole的协程框架中,默认情况下,所有变量赋值等操作都是安全的。协程切换只会发生在IO等待的时候。在大多数情况下,我们完全可以用同步的方式来写异步程序。

在项目开发实施过程中我发现会有导致数据库死锁的情况出现。这通常发生在两个数据库事务中。当2个事务同时执行,然后去修改同一条记录的时候。虽然我们可以通过在事务开始之前通过 获取记录锁来避免。但这种方法给人感觉不够轻量级。

解决方法

在我设计的框架中 每一个客户端的请求都是在一个单独的协程下执行的。同时为了减轻数据库负载,我将所有当前业务数据编写成一个PHP对象放在内存中并且可以全局索引获取到。我们可以利用协程的特性来避免数据库死锁这一情况的发生。只要我将需要更新的对象在当前协程中获取锁。避免其他协程对这个对象进行更新操作即可。

我为对象设计了 update 方法, 它会自动比较成员属性值,生成 changed并保存一份changed 并保存一份 oldvalue, 同时返回数据库更新Task。 在数据库事务执行过程中,如果执行失败会滚,内存中PHP对象的属性值也可以同时会滚。而Task对象在数据库执行完成后,可以交给同步对象进行同步操作。确保服务端和终端数据的一致性。

例如,当一个商户的商品销售。通常我们需要做如下操作:

  1. 计算价格、检查库存。
  2. 完成数款(现金、付款码、扫码收款等。)
  3. 创建订单
  4. 生成商品出货记录
  5. 减去商品库存
  6. 生成资金流水记录
  7. 更新商户账户余额。

在上述步骤中, 步骤6 和步骤8 是可能产生并发冲突的。我们需要对其进行锁定。并让其它要操作相同数据的协程等待即可。实现方法也比较简单,利用 PHP 的 traits 将需要用到锁的对象 use它即可。实现代码如下:

<?php
namespace lib\data\traits;


trait WaitLock {
    private int $_cid_ = -1;
    private int $_wait_counter = 0; # 等待计数器
    protected ?\Swoole\Coroutine\Channel $_wait_lock_channel_ = null;
    
    /**
    * 防并发锁.
    */
    public function waitLock() :static {
        $cid = \Swoole\Coroutine::getCid();
        if($cid === $this->_cid_){
            return $this;
        }

        # 如果对象正被其它协程使用, 则自动进行等待.
        if($this->_cid_ !== -1){
            $this->_wait_counter++;
            if(null === $this->_wait_lock_channel_){
                $this->_wait_lock_channel_ = new \Swoole\Coroutine\Channel(1);
            }
            $this->_wait_lock_channel_->pop();
            $this->_wait_counter--;
            if(0 == $this->_wait_counter){
                $this->_wait_lock_channel_->close();
                $this->_wait_lock_channel_ = null;
            }
        }
        
        # 获取锁, 并在协程结束后自动释放.
        $this->_cid_ = $cid;
        \Swoole\Coroutine::defer(function(){
            $this->_cid_ = -1;
            if($this->_wait_lock_channel_ != null){
                $this->_wait_lock_channel_->push(true);
            }
        });
        return $this;
    }
}