### 2018 年 12 月 29 日 发布 ## 悲观锁和乐观锁 业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针对某个时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓的 “ 锁 ” ,即给我们选定的目标数据上锁,使其无法被其他程序修改。 通常有两种锁机制:即通常所说的 “ 悲观锁( 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`字段来实现。 ThinkPHP`5.1`版本中并没有内置乐观锁功能,因此需要自己实现,本文就来利用`Trait`特性实现乐观锁的功能。 ## 乐观锁的实现 要实现乐观锁功能,主要涉及三个地方: **记录乐观锁**:第一次写入数据的时候自动记录`version`字段,当然也可以使用数据库默认值功能。 **读取乐观锁**:每次读取数据的时候都要单独记录下当前的`version`数据值。 **检测乐观锁**:每次更新数据的时候要重新检测下最新数据的`version`数据值,如果记录的版本号和最新的不一致,表示数据需要更新,否则把当前记录的版本号加1后更新到数据库。 而ThinkPHP`5.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; } ``` >[info] 值得注意的是,`5.2`版本目前已经内置了一个`OptimLock`的`Trait`实现。