ThinkPHP利用Trait特性给模型增加乐观锁功能

发布时间:2019-01-04 10:00:07 来源: ThinkPHP

悲观锁和乐观锁

业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针对某个时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓的 “ 锁 ” ,即给我们选定的目标数据上锁,使其无法被其他程序修改。 通常有两种锁机制:即通常所说的 “ 悲观锁( Pessimistic Locking ) ”和 “ 乐观锁( Optimistic Locking ) ” 。

悲观锁( Pessimistic Locking )

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 通常是使用for update子句来实现悲观锁机制。

ThinkPHP5支持悲观锁机制,要启用悲观锁功能,可以通过使用lock锁定方法,例如:

// 使用悲观锁功能
Db::name('user')->lock(true)->find(1);

就会自动在生成的SQL语句最后加上FOR UPDATE或者FOR UPDATE NOWAIT(Oracle数据库)。

lock方法还支持传入字符串,以实现特殊的锁机制。

Db::name('user')->lock('LOCK  IN  SHARE  MODE')->find(1);

乐观锁( Optimistic Locking )

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个version字段来实现。

ThinkPHP5.1版本中并没有内置乐观锁功能,因此需要自己实现,本文就来利用Trait特性实现乐观锁的功能。

乐观锁的实现

要实现乐观锁功能,主要涉及三个地方:

记录乐观锁:第一次写入数据的时候自动记录version字段,当然也可以使用数据库默认值功能。

读取乐观锁:每次读取数据的时候都要单独记录下当前的version数据值。

检测乐观锁:每次更新数据的时候要重新检测下最新数据的version数据值,如果记录的版本号和最新的不一致,表示数据需要更新,否则把当前记录的版本号加1后更新到数据库。

而ThinkPHP5.1的模型save方法会统一调用checkBeforeSave方法,因此我们可以通过重写该方法来实现乐观锁的检测乐观锁功能。而每次查询后都会调用模型的newInstance方法,因此可以重写该方法添加读取乐观锁功能。

继承方式实现

我们可以创建一个公共的模型继承系统的think\Model类,当你的模型需要使用乐观锁功能的话就单独继承。

<?php
namespace app\common\model;use think\Exception;use think\Model;class OptimLock extends Model{
    protected $optimLock = 'version';

    /**
     * 创建新的模型实例
     * @access public
     * @param  array    $data 数据
     * @param  bool     $isUpdate 是否为更新
     * @param  mixed    $where 更新条件
     * @return Model
     */
    public function newInstance($data = [], $isUpdate = false, $where = null)
    {
        // 缓存乐观锁
        $this->cacheLockVersion($data);

        return (new static($data))->isUpdate($isUpdate, $where);
    }

    /**
     * 写入之前检查数据
     * @access protected
     * @param  array   $data  数据
     * @param  array   $where 保存条件
     * @return bool
     */
    protected function checkBeforeSave($data, $where)
    {
        if (!empty($data)) {
            // 数据对象赋值
            foreach ($data as $key => $value) {
                $this->setAttr($key, $value, $data);
            }

            if (!empty($where)) {
                $this->isUpdate(true, $where);
            }
        }

        // 数据自动完成
        $this->autoCompleteData($this->auto);

        // 事件回调
        if (false === $this->trigger('before_write')) {
            return false;
        }

        if ($this->isExists() && !$this->checkLockVersion()) {
            throw new Exception('record has update');
        }

        return true;
    }

    /**
     * 缓存乐观锁
     * @access protected
     * @param  array $data 数据
     * @return void
     */
    protected function cacheLockVersion($data): void    {
        $pk = $this->getPk();

        if ($this->optimLock && isset($data[$this->optimLock]) && is_string($pk) && isset($data[$pk])) {
            $key = $this->name . '_' . $data[$pk] . '_lock_version';

            $_SESSION[$key] = $data[$this->optimLock];
        }
    }

    /**
     * 检查乐观锁
     * @access protected
     * @param  array $data 数据
     * @return bool
     */
    protected function checkLockVersion()
    {
        // 检查乐观锁
        $id = $this->getKey();

        if (empty($id)) {
            return true;
        }

        $key = $this->name . '_' . $id . '_lock_version';

        if ($this->optimLock && isset($_SESSION[$key])) {
            $lockVer        = $_SESSION[$key];
            $vo             = $this->field($this->optimLock)->find($id);
            $_SESSION[$key] = $lockVer;
            $currVer        = $vo[$optimLock];

            if (isset($currVer)) {
                if ($currVer > 0 && $lockVer != $currVer) {
                    // 记录已经更新
                    return false;
                }

                // 更新乐观锁
                $lockVer++;

                if ($this->data[$this->optimLock] != $lockVer) {
                    $this->data[$this->optimLock] = $lockVer;
                }

                $_SESSION[$key] = $lockVer;
            }
        }

        return true;
    }}

对需要使用乐观锁的模型,可以使用

namespace app\index\model;

use app\common\model\OptimLock;

class User extends OptimLock{

}

利用Trait特性实现

但由于PHP不支持多继承,因此并不建议使用模型继承功能来扩展功能。我们可以利用Trait特性来更方便的引入OptimLock后开启乐观锁功能。

因为Trait机制的问题,我们对上面的代码进行了一些必要的调整。

<?php
namespace app\common\traits;

use think\Exception;

use think\Model;

trait OptimLock
{
    protected function getOptimLockField()
    {
        return property_exists($this, 'optimLock') && isset($this->optimLock) ? $this->optimLock : 'version';
    }

    /**
     * 创建新的模型实例
     * @access public
     * @param  array    $data 数据
     * @param  bool     $isUpdate 是否为更新
     * @param  mixed    $where 更新条件
     * @return Model
     */
    public function newInstance($data = [], $isUpdate = false, $where = null)
    {
        // 缓存乐观锁
        $this->cacheLockVersion($data);

        return (new static($data))->isUpdate($isUpdate, $where);
    }

    /**
     * 写入之前检查数据
     * @access protected
     * @param  array   $data  数据
     * @param  array   $where 保存条件
     * @return bool
     */
    protected function checkBeforeSave($data, $where)
    {
        if (!empty($data)) {
            // 数据对象赋值
            foreach ($data as $key => $value) {
                $this->setAttr($key, $value, $data);
            }

            if (!empty($where)) {
                $this->isUpdate(true, $where);
            }
        }

        // 数据自动完成
        $this->autoCompleteData($this->auto);

        // 事件回调
        if (false === $this->trigger('before_write')) {
            return false;
        }

        if ($this->isExists()) {
            if (!$this->checkLockVersion()) {
                throw new Exception('record has update');
            }
        } else {
            $this->recordLockVersion();
        }

        return true;
    }

    /**
     * 缓存乐观锁
     * @access protected
     * @param  array $data 数据
     * @return void
     */
    protected function cacheLockVersion($data): void    {
        $optimLock = $this->getOptimLockField();
        $pk        = $this->getPk();

        if ($optimLock && isset($data[$optimLock]) && is_string($pk) && isset($data[$pk])) {
            $key = $this->getName() . '_' . $data[$pk] . '_lock_version';

            $_SESSION[$key] = $data[$optimLock];
        }
    }

    /**
     * 检查乐观锁
     * @access protected
     * @param  array $data 数据
     * @return bool
     */
    protected function checkLockVersion()
    {
        // 检查乐观锁
        $id = $this->getKey();

        if (empty($id)) {
            return true;
        }

        $key       = $this->getName() . '_' . $id . '_lock_version';
        $optimLock = $this->getOptimLockField();

        if ($optimLock && isset($_SESSION[$key])) {
            $lockVer        = $_SESSION[$key];
            $vo             = $this->field($optimLock)->find($id);
            $_SESSION[$key] = $lockVer;
            $currVer        = $vo[$optimLock];

            if (isset($currVer)) {
                if ($currVer > 0 && $lockVer != $currVer) {
                    // 记录已经更新
                    return false;
                }

                // 更新乐观锁
                $lockVer++;

                $data = $this->getData();

                if ($data[$optimLock] != $lockVer) {
                    $this->data($optimLock, $lockVer);
                }

                $_SESSION[$key] = $lockVer;
            }
        }

        return true;
    }}

对需要使用乐观锁的模型,可以使用

namespace app\index\model;

use app\common\traits\OptimLock;

use think\Model;

class User extends Model
{
    use OptimLock;}
值得注意的是,5.2版本目前已经内置了一个OptimLock的Trait实现。

最新资讯

  1. 2019年乌镇大会,也说说互联网
  2. 响应式网站建设需要准备多少开发预算
  3. CMSJIA介绍
  4. 遵义信息技术服务、WEB软件开发公司
  5. 建设公司网站的作用体现
  6. 品牌保护中域名、商标同等重要
  7. 网站建设哪家公司好
  8. 建设开发手机网站的优势及作用
  9. 我们研究了100个小程序,得出了这些经验!
  10. 公司定制开发网站建设的优势
  11. 移动互联网建站:遵义做网站建设那家公司好
  12. 贵州腾巢网络成功入驻熊掌号,再现移动互联时代新域名