// 如果已经启用了悲观锁,但是却没有完成更新,或者更新的记录数为0;
// 那就说明之前的加锁已经自动失效了,记录正在被修改,
// 或者已经完成修改,于是抛出异常。
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;
}
}
上面的代码对比乐观锁,主要不同点在于:
新增加了一个加锁方法,一个获取锁定最大时长的方法。
保存时不再是把标识字段+1,而是把标识字段置0。
在具体使用方法上,可以参照以下代码:
// 从PLockAR派生模型类
class Post extends PLockAR {
// 重载定义悲观锁标识字段,如 locked_at
public function pesstimisticLock() {
return 'locked_at';
}
// 重载定义最大锁定时长,如1小时
public function maxLockTime() {
return 3600000;
}
}
// 修改前要尝试加锁
class SectionController extends Controller {
public function actionUpdate($id)
{
$model = $this->findModel($id);
1 个回复
zkbhj - 凯冰科技站长
赞同来自:
在并发环境下,有可能会出现脏读(Dirty Read)、不可重复读(Unrepeatable Read)、 幻读(Phantom Read)、更新丢失(Lost update)等情况。具体的表现可以自行搜索。
为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念。 这里我们都不作解释了,拿这些关键词一搜,网上大把大把的。
但是,就于具体开发过程而言,一般分为悲观锁和乐观锁两种方式来解决并发冲突问题。
乐观锁
乐观锁(optimistic locking)表现出大胆、务实的态度。使用乐观锁的前提是, 实际应用当中,发生冲突的概率比较低。他的设计和实现直接而简洁。 目前Web应用中,乐观锁的使用占有绝对优势。
因此,Yii也为ActiveReocrd提供了乐观锁支持。
根据Yii的官方文档,使用乐观锁,总共分4步:
从本质上来讲,乐观锁并没有像悲观锁那样使用数据库的锁机制。 乐观锁通过在表中增加一个计数字段,来表示当前记录被修改的次数(版本号)。
然后在更新、删除前通过比对版本号来实现乐观锁。
声明版本号字段
版本号是实现乐观锁的根本所在。所以第一步,我们要告诉Yii,哪个字段是版本号字段。 这个由 yii\db\BaseActiveRecord 负责: 这个方法返回 null ,表示不使用乐观锁。那么我们的Model中,要对此进行重载。 返回一个字符串,表示我们用于标识版本号的字段。比如可以这样: 说明当前的ActiveRecord中,有一个 ver 字段,可以为乐观锁所用。 那么Yii具体是如何借助这个 ver 字段实现乐观锁的呢?
更新过程
具体来讲,使用乐观锁之后的更新过程,就是这么一个流程:
由于ActiveRecord的更新过程最终都需要调用 yii\db\BaseActiveRecord::updateInteranl() ,理所当然地,处理乐观锁的代码, 也就隐藏在这个方法中: 从上面的代码中,我们不难得出:
删除过程
与更新过程相比,删除过程的乐观锁,更简单,更好理解。代码仍在 yii\db\BaseActiveRecord 中: 比起更新过程,删除过程确实要简单得多。唯一的区别就是省去了版本号+1的步骤。 都要删除了,版本号+1有什么意义?
乐观锁失效
乐观锁存在失效的情况,属小概率事件,需要多个条件共同配合才会出现。如:
乐观锁此时的失效,根本原因在于应用所使用的主键ID管理策略, 正好与乐观锁存在极小程度上的不兼容。
两者分开来看,都是没问题的。组合到一起之后,大致看去好像也没问题。 但是bug之所以成为bug,坑之所以能够坑死人,正是由于其隐蔽性。
对此,也有一些意见提出来,使用时间戳作为版本号字段,就可以避免这个问题。 但是,时间戳的话,如果精度不够,如毫秒级别,那么在高并发,或者非常凑巧情况下, 仍有失效的可能。而如果使用高精度时间戳的话,成本又太高。
使用时间戳,可靠性并不比使用整型好。问题还是要回到使用严谨的主键成生策略上来。
悲观锁
正如其名字,悲观锁(pessimistic locking)体现了一种谨慎的处事态度。其流程如下:
悲观锁确实很严谨,有效保证了数据的一致性,在C/S应用上有诸多成熟方案。 但是他的缺点与优点一样的明显:
总体来看,悲观锁不大适应于Web应用,Yii团队也认为悲观锁的实现过于麻烦, 因此,ActiveRecord也没有提供悲观锁。
作为Yii的构成基因之一的Ruby on rails,他的ActiveReocrd模型,倒是提供了悲观锁, 但是使用起来也很麻烦。
悲观锁的实现
虽然悲观锁在Web应用上存在诸多不足,实现悲观锁也需要解决各种麻烦。但是, 当用户提出他就是要用悲观锁时,牙口再不好的码农,就是咬碎牙也是要啃下这块骨头来。
对于一个典型的Web应用而言,这里提供个人常用的方法来实现悲观锁。
首先,在要锁定的表里,加一个字段如 locked_at ,表示当前记录被锁定时的时间, 当为 0 时,表示该记录未被锁定,或者认为这是1970年时加的锁。
当要修改某个记录时,先看看当前时间与 locked_at 字段相差是否超过预定的一个时长T,比如 30 min ,1 h 之类的。
如果没超过,说明该记录有人正在修改,我们暂时不能打开(读取)他来修改。 否则,说明可以修改,我们先将当前时间戳保存到该记录的 locked_at字段。 那么之后的时长T内如果有人要来改这个记录,他会由于加锁失败而无法读取, 从而无法修改。
我们在完成修改后,即将保存时,要比对现在的 locked_at 。只有在 locked_at 一致时,才认为刚刚是我们加的锁,我们才可以保存。 否则,说明在我们加锁后,又有人加了锁正在修改, 或者已经完成了修改,使得 locked_at 归 0。
这种情况主要是由于我们的修改时长过长,超过了预定的T。原先的加锁自动解开, 其他用户可以在我们加锁时刻再过T之后,重新加上自己的锁。换句话说, 此时悲观锁退化为乐观锁。
大致的原理性代码如下:
上面的代码对比乐观锁,主要不同点在于:
在具体使用方法上,可以参照以下代码: 上述方法实现的悲观锁,避免了使用数据库自身的锁机制,契合Web应用的特点, 具有一定的适用性,但是也存在一定的缺陷: