虽然悲观锁在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之后,重新加上自己的锁。换句话说, 此时悲观锁退化为乐观锁。
大致的原理性代码如下:
// 悲观锁AR基类,需要使用悲观锁的AR可以由此派生 class PLockAR extends \yii\db\BaseActiveRecord { // 声明悲观锁使用的标记字段,作用类似于 optimisticLock() 方法 public function pesstimisticLock() { return null; } // 定义锁定的最大时长,超过该时长后,自动解锁。 public function maxLockTime() { return 0; } // 尝试加锁,加锁成功则返回true public function lock() { $lock = $this->pesstimisticLock(); $now = time(); $values = [$lock => $now]; // 以下2句,更新条件为主键,且上次锁定时间距现在超过规定时长 $condition = $this->getOldPrimaryKey(true); $condition[] = ['<', $lock, $now - $this->maxLockTime()]; $rows = $this->updateAll($values, $condition); // 加锁失败,返回 false if (! $rows) { return false; } return true; } // 重载updateInternal() protected function updateInternal($attributes = null) { // 这些与原来代码一样 if (!$this->beforeSave(false)) { return false; } $values = $this->getDirtyAttributes($attributes); if (empty($values)) { $this->afterSave(false, $values); return 0; } $condition = $this->getOldPrimaryKey(true); // 改为获取悲观锁标识字段 $lock = $this->pesstimisticLock(); // 如果 $lock 为 null,那么,不启用悲观锁。 if ($lock !== null) { // 等下保存时,要把标识字段置0 $values[$lock] = 0; // 这里把原来的标识字段值作为更新的另一个条件 $condition[$lock] = $this->$lock; } $rows = $this->updateAll($values, $condition); // 如果已经启用了悲观锁,但是却没有完成更新,或者更新的记录数为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); if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } else { // 加入一个加锁的判断 if (!$model->lock()) { // 加锁失败 // ... ... } return $this->render('update', [ 'model' => $model, ]); } } }
上述方法实现的悲观锁,避免了使用数据库自身的锁机制,契合Web应用的特点, 具有一定的适用性,但是也存在一定的缺陷: