# TP5实战源码---乐观锁的应用 [TOC] ## 乐观锁的概念 >[info] 乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 ------- 百度百科 ## 乐观锁示例 >[info] 如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过 程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作 员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。 乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本 ( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。 1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 1. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。 1. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 1. 4 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 ## 乐观锁Model通用代码(TP5) 此方法为Model基类通用方法 ~~~ /** * 带有乐观锁的修改 * Power: Mikkle * Email:776329498@qq.com * @param $save_data   修改的数据 * @param string $edit_pk 修改的ID字段名称 * @param string $version_field  乐观锁版本号字段名称 * @return array */ public function editDateWithLock($save_data,$edit_pk="",$version_field=""){ if (empty($version_field)){ $version_field = isset($this->versionField) ? $this->versionField : "edit_version"; } if (empty($edit_pk)){ $edit_pk = isset($this->editPk) ? $this->editPk : $this->getPk(); } //判断PK字段是否存在 if (!isset($save_data[$edit_pk])||!isset($save_data[$version_field])){ return self::showReturnCodeWithOutData(1003,"参数缺失"); }else{ //设置升级检索条件 PK和版本号 $map[$edit_pk] = $save_data[$edit_pk]; $map[$version_field] = $save_data[$version_field]; //剔除PK unset($save_data[$edit_pk]); } try{ //检测版本字段 if($this->hasColumn($version_field)){ throw new Exception("乐观锁版本字段[$version_field]不存在"); } $original_data = $this->where($map)->find(); if (empty($original_data)){ throw new Exception("此条信息已经变动了,请重新操作!"); } foreach ($save_data as $item=>$value){ if (isset($original_data[$item])){ //修改的数值不变时候 剔除 if ($original_data[$item]==$value){ unset( $save_data[$item]); }elseif($item!=$version_field){ unset( $original_data[$item]); } }else{ //修改的字段不存在 剔除 unset( $save_data[$item]); } } if(empty($save_data)){ throw new Exception("修改的数值无变化"); } //版本号升级 $save_data[$version_field]=(int)$original_data[$version_field]+1; if (1!=$this->allowField(true)->save($save_data,$map)){ throw new Exception("修改信息出错:".$this->getError()); } //记录修改日志 $this->saveEditLog($original_data,$save_data); return self::showReturnCodeWithOutData(1001); }catch (Exception $e){ $msg=$e->getMessage(); return self::showReturnCodeWithOutData(1003,$msg); } } ~~~ ## 控制器调用方法 最简单的调用方式 ~~~ $model =new Log(); $data =[ "id"=>1, "text"=> time(), "edit_version"=>2 ]; $model->editDateWithLock($data); ~~~ 使用doModelAction调用 ~~~ public function addStructureData() { if($this->request->isAjax()){ $data = $this->request->post(); $data['project_guid']=$this->request->param('project_guid'); $validate_name='base/ProjectStructure.edit'; $model_name='base/ProjectStructure'; return json($this->doModelAction($data,$validate_name,$model_name,"editDateWithLock")); } } ~~~ ## 使用到的其他方法 * doModelAction方法参见 TP5实战技巧---整合基类 化繁为简 教程 * saveEditLog方法示例 (位置模型基类) ~~~ /** * 保存修改信息 * Power: Mikkle * Email:776329498@qq.com * @param $original_data * @param $save_data * @return bool */ protected function saveEditLog($original_data,$save_data){ if (empty($original_data)&&empty($save_data)){ $this->error="保存的修改信息不存在"; return false; } $log_data=[ "uuid"=>Session::get('uuid', 'Global'), "model_data"=>$this->name, "original_data"=>$original_data, "save_data"=>$save_data, "update_time"=>time(), ]; try{ Db::table("update_log")->insert($log_data); return true; }catch (Exception $e){ $log_data["error"]="保存修改信息出错"; Log::write(json_encode($log_data),"error"); return false; } } ~~~ * hasColumn 判断字段是否存在(位置模型基类) ~~~ /** * 判断字段是否存在 * Power: Mikkle * Email:776329498@qq.com * @param $column * @param string $table * @return bool */ protected function hasColumn($column,$table=""){ $table = isset($table)?$table:$this->table; if (empty($table)||$column){ $this->error="hasColumn方法参数缺失"; return false; } $sql = "SELECT * FROM information_schema.columns WHERE table_schema=CurrentDatabase AND table_name = '{$table}' AND column_name = '{$column}'"; return $this->query($sql) ? true : false; } ~~~ 有问题的请留言