php动态+trait,ThinkPHP利用Trait特性给模型增加乐观锁功能

悲观锁和乐观锁

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值