深入理解Yii2.0乐观锁与悲观锁的原理与使用

时间:2017-08-11

本文介绍了深入理解Yii2.0乐观锁与悲观锁的原理与使用,分享给大家,具体如下:

Web应用往往面临多用户环境,这种情况下的并发写入控制, 几乎成为每个开发人员都必须掌握的一项技能。

在并发环境下,有可能会出现脏读(Dirty Read)、不可重复读(Unrepeatable Read)、 幻读(Phantom Read)、更新丢失(Lost update)等情况。具体的表现可以自行搜索。

为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念。 这里我们都不作解释了,拿这些关键词一搜,网上大把大把的。

但是,就于具体开发过程而言,一般分为悲观锁和乐观锁两种方式来解决并发冲突问题。

乐观锁

乐观锁(optimistic locking)表现出大胆、务实的态度。使用乐观锁的前提是, 实际应用当中,发生冲突的概率比较低。他的设计和实现直接而简洁。 目前Web应用中,乐观锁的使用占有绝对优势。

因此,Yii也为ActiveReocrd提供了乐观锁支持。

根据Yii的官方文档,使用乐观锁,总共分4步:

  • 为需要加锁的表增加一个字段,用于表示版本号。 当然相应的Model也要为该字段的加入,作出适当调整。比如, rules() 中要加入该字段。
  • 重载 yii\db\ActiveRecord::optimisticLock() 方法,返回上一步中的字段名。
  • 在记录的修改页面表单中,加入一个 <input type="hidden"> 用于暂存读取时的记录的版本号。
  • 在保存代码的地方,使用 try ... catch 看看是否能捕获一个 yii\db\StaleObjectException 异常。如果是,说明在本次修改这个记录的过程中, 该记录已经被修改过了。简单应对的话,可以作出相应提示。智能点的话, 可以合并不冲突的修改,或者显示一个diff页面。

从本质上来讲,乐观锁并没有像悲观锁那样使用数据库的锁机制。 乐观锁通过在表中增加一个计数字段,来表示当前记录被修改的次数(版本号)。

然后在更新、删除前通过比对版本号来实现乐观锁。

声明版本号字段

版本号是实现乐观锁的根本所在。所以第一步,我们要告诉Yii,哪个字段是版本号字段。 这个由 yii\db\BaseActiveRecord 负责:

public function optimisticLock()
{
  return null;
}

这个方法返回 null ,表示不使用乐观锁。那么我们的Model中,要对此进行重载。 返回一个字符串,表示我们用于标识版本号的字段。比如可以这样:

public function optimisticLock()
{
  return 'ver';
}

说明当前的ActiveRecord中,有一个 ver 字段,可以为乐观锁所用。 那么Yii具体是如何借助这个 ver 字段实现乐观锁的呢?

更新过程

具体来讲,使用乐观锁之后的更新过程,就是这么一个流程:

  1. 读取要更新的记录。
  2. 对记录按照用户的意愿进行修改。当然,这个时候不会修改 ver 字段。 这个字段对用户是没意义的。
  3. 在保存记录前,再次读取这个记录的 ver 字段,与之前读取的值进行比对。
  4. 如果 ver 不同,说明在用户修改过程中,这个记录被别人改动过了。那么, 我们要给出提示。
  5. 如果 ver 相同,说明这个记录未被修改过。那么,对 ver +1, 并保存这个记录。这样子就完成了记录的更新。同时,该记录的版本号也加了1。

由于ActiveRecord的更新过程最终都需要调用 yii\db\BaseActiveRecord::updateInteranl() ,理所当然地,处理乐观锁的代码, 也就隐藏在这个方法中:

protected function updateInternal($attributes = null)
{
  if (!$this->beforeSave(false)) {
    return false;
  }
  // 获取等下要更新的字段及新的字段值
  $values = $this->getDirtyAttributes($attributes);
  if (empty($values)) {
    $this->afterSave(false, $values);
    return 0;
  }
  // 把原来ActiveRecord的主键作为等下更新记录的条件,
  // 也就是说,等下更新的,最多只有1个记录。
  $condition = $this->getOldPrimaryKey(true);

  // 获取版本号字段的字段名,比如 ver
  $lock = $this->optimisticLock();

  // 如果 optimisticLock() 返回的是 null,那么,不启用乐观锁。
  if ($lock !== null) {
    // 这里的 $this->$lock ,就是 $this->ver 的意思;
    // 这里把 ver+1 作为要更新的字段之一。
    $values[$lock] = $this->$lock + 1;

    // 这里把旧的版本号作为更新的另一个条件
    $condition[$lock] = $this->$lock;
  }
  $rows = $this->updateAll($values, $condition);

  // 如果已经启用了乐观锁,但是却没有完成更新,或者更新的记录数为0;
  // 那就说明是由于 ver 不匹配,记录被修改过了,于是抛出异常。
  if ($lock !== null && !$rows) {
    throw new StaleObjectException('The object being updated is outdated.');
  }
  $changedAttributes = [];
  foreach ($values as $name => $value) {
    $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
    $this->_oldAttributes[$name] = $value;
  }
  $this->afterSave(false, $changedAttributes);
  return $rows;
}

  • 共4页:
  • 上一篇1/4
  • 下一页
  • 上一篇:php判断str字符串是否是xml格式数据的方法示例 下一篇:PHP编程实现微信企业向用户付款的方法示例

    相关文章

    最新文章