CodeIgniter框架数据库事务处理的设计缺陷和解决方案


Posted in PHP onJuly 25, 2014

起因:

在我们线上的某个业务中,使用较老版本的CodeIgniter框架,其中的DB类中,对DB事物处理部分存在着一个设计上的缺陷,或许也算不上缺陷吧。但他却影响了我们生产环境,导致连锁反应。对业务产生较大影响,且不容易排查。这个问题,我在今年的3月中旬,曾向codeigniter中国的站长Hex 报告过,之后,我也忘记这件事情了。直到今天,我们线上业务又一次以为这个问题,害的我又排查一次。具体原因,各位且先听我慢慢说完。(这个问题同样存在于最新版本Version 2.1.0中)

分析:

以CodeIgniter框架Version 2.1.0为例,在system\database\DB_driver.php的CI_DB_driver类中第58行有个$_trans_status属性。

//system\database\DB_driver.php

var $trans_strict = TRUE;

var $_trans_depth = 0;

var $_trans_status = TRUE; // Used with transactions to determine if a rollback should occur

var $cache_on  = FALSE;

同时,这个类的query方法中,有赋值此属性的代码,见文件306、307行

// This will trigger a rollback if transactions are being used

$this->_trans_status = FALSE;

这里也给了注释,告诉我们,如果使用了事物处理,那么这属性将成为一个回滚的决定条件。

在520行的事物提交方法trans_complete中,如下代码:

/**

 * Complete Transaction

 *

 * @access public

 * @return bool

 */

function trans_complete()

{

 if ( ! $this->trans_enabled)

 {

  return FALSE;

 }
 // When transactions are nested we only begin/commit/rollback the outermost ones

 if ($this->_trans_depth > 1)

 {

  $this->_trans_depth -= 1;

  return TRUE;

 }
 // The query() function will set this flag to FALSE in the event that a query failed

 if ($this->_trans_status === FALSE)

 {

  $this->trans_rollback();
  // If we are NOT running in strict mode, we will reset

  // the _trans_status flag so that subsequent groups of transactions

  // will be permitted.

  if ($this->trans_strict === FALSE)

  {

   $this->_trans_status = TRUE;

  }
  log_message('debug', 'DB Transaction Failure');

  return FALSE;

 }
 $this->trans_commit();

 return TRUE;

}

在535行中,如果_trans_status属性如果是false,那么将发生回滚,并且返回false。

在我们的业务代码中,由于程序员疏忽,没有判断trans_complete()方法是否正确执行,直接告诉用户操作成功,但实际上,程序已经向DB下达回滚指令,并未成功更新DB记录。当用户执行下一步操作时,程序又发现相应记录并未更新,又提醒用户上个操作没有完成,通知用户重新执行。如此反复…

CodeIgniter框架数据库事务处理的设计缺陷和解决方案

排查的过程,也是挺有意思的,起初从PHP代码中,总是不能确定问题所在,并没有把焦点放到trans_complete()方法的返回上。直到后来strace抓包分析,才知道是因为此属性而导致了回滚。

22:54:08.380085 write(9, "_\0\0\0\3UPDATE `cfc4n_user_info` SET `cfc4n_user_lock` = 1\nWHERE `cfc4n_user_id` = \'6154\'\nAND `cfc4n_user_lock` = 0", 99) = 99    //执行更新命令

22:54:08.380089 read(9, ":\0\0\1\377\36\4#42S22Unknown column \'cfc4n_user_lock\' in \'where clause\'", 16384) = 62    //不存在字段,SQL执行错误

22:54:08.381791 write(9, "\21\0\0\0\3SET AUTOCOMMIT=0", 21) = 21    //禁止自动提交

22:54:08.381891 read(9, "\7\0\0\1\0\0\0\0\0\0\0", 16384) = 11

22:54:08.382186 poll([{fd=9, events=POLLIN|POLLPRI}], 1, 0) = 0

22:54:08.382258 write(9, "\v\0\0\0\2jv01_roles", 15) = 15

22:54:08.382343 read(9, "\7\0\0\1\0\0\0\0\0\0\0", 16384) = 11

22:54:08.382631 poll([{fd=9, events=POLLIN|POLLPRI}], 1, 0) = 0

22:54:08.382703 write(9, "\22\0\0\0\3START TRANSACTION", 22) = 22   //开始事务处理

22:54:08.401954 write(9, "\v\0\0\0\2database_demo", 15) = 15

22:54:08.402043 read(9, "\7\0\0\1\0\0\0\1\0\1\0", 16384) = 11

22:54:08.417773 write(9, "\v\0\0\0\2database_demo", 15) = 15

22:54:08.417872 read(9, "\7\0\0\1\0\0\0\1\0\0\0", 16384) = 11

22:54:08.418256 write(9, "[\0\0\0\3UPDATE `cfc4n_user_info` SET `silver` = CAST( silver + (5) as signed )\nWHERE `cfc4n_user_id` = \'6154\'", 95) = 95    //执行其他SQL语句

22:54:08.418363 read(9, "0\0\0\1\0\1\0\1\0\0\0(Rows matched: 1  Changed: 1  Warnings: 0", 16384) = 52    //成功更新,影响条数1.

22:54:08.430212 write(9, "\v\0\0\0\2database_demo", 15) = 15

22:54:08.430314 read(9, "\7\0\0\1\0\0\0\1\0\0\0", 16384) = 11

22:54:08.430698 write(9, "B\0\0\0\3UPDATE `cfc4n_user_info` SET `exp` = exp + 26\nWHERE `cfc4n_user_id` = \'6154\'", 70) = 70     //执行其他SQK语句

22:54:08.430814 read(9, "0\0\0\1\0\1\0\1\0\0\0(Rows matched: 1  Changed: 1  Warnings: 0", 16384) = 52    //成功更新,影响条数1.

22:54:08.432130 write(9, "\v\0\0\0\2database_demo", 15) = 15

22:54:08.432231 read(9, "\7\0\0\1\0\0\0\1\0\0\0", 16384) = 11

22:54:08.432602 write(9, "\244\0\0\0\3UPDATE `cfc4n_user_quest` SET `rew` = 1, `retable` = retable + 1, `re_time` = 1335797648\nWHERE `cfc4n_user_id` = \'6154\'\nAND `quest_id` = \'300001\'\nAND `rew` = 0", 168) = 168    //执行其他SQK语句

22:54:08.432743 read(9, "0\0\0\1\0\1\0\1\0\0\0(Rows matched: 1  Changed: 1  Warnings: 0", 16384) = 52    //成功更新,影响条数1.

22:54:08.433517 write(9, "\v\0\0\0\2database_demo", 15) = 15

22:54:08.433620 read(9, "\7\0\0\1\0\0\0\1\0\0\0", 16384) = 11

22:54:08.433954 write(9, "\t\0\0\0\3ROLLBACK", 13) = 13    //回滚事务 #注意看这里

22:54:08.434041 read(9, "\7\0\0\1\0\0\0\0\0\0\0", 16384) = 11

22:54:08.434914 write(9, "\v\0\0\0\2database_demo", 15) = 15

22:54:08.434999 read(9, "\7\0\0\1\0\0\0\0\0\0\0", 16384) = 11

22:54:08.435342 write(9, "\21\0\0\0\3SET AUTOCOMMIT=1", 21) = 21  //恢复自动提交

22:54:08.435430 read(9, "\7\0\0\1\0\0\0\2\0\0\0", 16384) = 11

22:54:08.436923 write(9, "\1\0\0\0\1", 5) = 5

可以看到,在22:54:08.380085时间点处,发送更新SQL语句指令,在22:54:08.380089时间读取返回结果,得到SQL执行错误,不存在字段”cfc4n_user_lock”;22:54:08.381791和22:54:08.382703两个时间点,PHP发送停止“自动提交”与“开始事务处理”指令,在 22:54:08.433954 发送“事务回滚”指令。

配合如上的代码分析,可以清楚的知道,因为“UPDATE `cfc4n_user_info` SET `cfc4n_user_lock` = 1 WHERE `cfc4n_user_id` = '6154′ AND `cfc4n_user_lock` = 0”这句SQL执行错误,导致$_trans_status属性被设置为FALSE,当代码提交事务时,被trans_complete()方法判断,认为“上一个事务处理”(下面将仔细分析)中存在SQL语句执行失败,决定回滚事务,不提交。

刚刚提到“上一个事务处理”,可能有些朋友不能理解,我们先继续回到代码中来,继续看该属性,同样在trans_complete方法中,542-545行:

// If we are NOT running in strict mode, we will reset

// the _trans_status flag so that subsequent groups of transactions

// will be permitted.

if ($this->trans_strict === FALSE)

{

 $this->_trans_status = TRUE;

}

也可以很容易的从注释中看明白,设置CI的设计者,为了更严谨的处理 同一个脚本中,存在多个事务时,事务间彼此关系重要,一荣俱荣,一损俱损。这里的trans_strict属性,是个开关,当 trans_strict为false,便是非严格模式,意味着多个事务之间,彼此关系不重要,不影响。当前一个事务中有SQL语句执行失败,影响不到自己。便将_trans_status 设置为TRUE。
毫无疑问,这是个非常周全的考虑。考虑了多个事务之间的关系,保证业务跑在更严谨的代码上。

可是,我们的代码中,错误的SQL语句是执行在事务处理以外的,并不是事务之内。按照我们对事务的认识,可以很清晰的知道,事务之外的SQL相比事务之内的SQL来说,事务之内的SQL更重要,之外的可以允许出错,但事务之内的,务必要正确,不受外界干扰。但CI的框架中,因为一个事务以外的语句执行失败,却导致整个事务回滚…当然,我们的程序员没有对事务提交方法的返回做判断,这也是个问题。

问题已经很清晰了,那么解决方法想必对你来说,是多么的简单。
比如在trans_start方法中,对_trans_status 属性赋值,设置为TRUE,不理会事务外的问题。

function trans_start($test_mode = FALSE)

{

 if ($this->trans_strict === FALSE)

 {

  $this->_trans_status = TRUE;    //在开始事务处理时,重新设定这个属性的值为TRUE

 }

    //2012/05/01 18:00 经过CI中文社区网友 http://codeigniter.org.cn/forums/space-uid-5721.html指正,这里修改为增加trans_strict 属性判断 ,在决定是否重设_trans_status 为好。

 if ( ! $this->trans_enabled)

 {

  return FALSE;

 }
 // When transactions are nested we only begin/commit/rollback the outermost ones

 if ($this->_trans_depth > 0)

 {

  $this->_trans_depth += 1;

  return;

 }
 $this->trans_begin($test_mode);

}

结束:

在不明白对方设计意图的情况下,不能盲目的定义对方的代码评价,不管程序作者的水平如何。比自己强,也不能盲目崇拜;比自己弱,更不能乱加指责;理解读懂设计意图,学习他人优秀的设计思路、代码风格、算法效率,这才是一个好习惯。当然codeigniter框架是优秀的。

PHP 相关文章推荐
PHP 中执行系统外部命令
Oct 09 PHP
PHP4(windows版本)中的COM函数
Oct 09 PHP
php中DOMElement操作xml文档实例演示
Mar 26 PHP
php登陆页的密码处理方式分享
Oct 14 PHP
ThinkPHP中__initialize()和类的构造函数__construct()用法分析
Nov 29 PHP
php限制上传文件类型并保存上传文件的方法
Mar 13 PHP
PHP基于SMTP协议实现邮件发送实例代码
Apr 27 PHP
php usort 使用用户自定义的比较函数对二维数组中的值进行排序
May 02 PHP
php 删除指定文件夹的实例讲解
Jul 25 PHP
使用XHProf查找PHP性能瓶颈的实例
Dec 13 PHP
Yii 访问 Gii(脚手架)时出现 403 错误
Jun 06 PHP
PHP中soap用法示例【SoapServer服务端与SoapClient客户端编写】
Dec 25 PHP
Codeigniter框架的更新事务(transaction)BUG及解决方法
Jul 25 #PHP
PHP中可以自动分割查询字符的Parse_str函数使用示例
Jul 25 #PHP
PHP获取短链接跳转后的真实地址和响应头信息的方法
Jul 25 #PHP
PHP实现根据设备类型自动跳转相应页面的方法
Jul 24 #PHP
PHP结合JQueryJcrop实现图片裁切实例详解
Jul 24 #PHP
PHP 5.3新增魔术方法__invoke概述
Jul 23 #PHP
php实现与erlang的二进制通讯实例解析
Jul 23 #PHP
You might like
肝肠寸断了解下!盘点史上最伤心的十大动漫
2020/03/04 日漫
PHP5 安装方法
2007/01/15 PHP
Thinkphp实现短信验证注册功能
2016/10/18 PHP
记录一次排查PHP脚本执行卡住的问题
2016/12/27 PHP
PHP时间处理类操作示例
2018/09/05 PHP
jquery ajax执行后台方法
2010/03/18 Javascript
jQuery对表单元素的取值和赋值操作代码
2011/05/19 Javascript
关于jquery css的使用介绍
2013/04/18 Javascript
JavaScript版TAB选项卡效果实例
2013/08/16 Javascript
Nodejs实战心得之eventproxy模块控制并发
2015/10/27 NodeJs
js和jq使用submit方法无法提交表单的快速解决方法
2016/05/17 Javascript
JavaScript中使用Async实现异步控制
2017/08/15 Javascript
详解js正则表达式验证时间格式xxxx-xx-xx形式
2018/02/09 Javascript
vue请求本地自己编写的json文件的方法
2019/04/25 Javascript
在Vue中使用icon 字体图标的方法
2019/06/14 Javascript
js通过循环多张图片实现动画效果
2019/12/19 Javascript
[02:28]PWL开团时刻DAY3——Ink Ice与DeMonsTer之间的勾心斗角
2020/11/03 DOTA
Python实现微信公众平台自定义菜单实例
2015/03/20 Python
Python使用urllib2模块抓取HTML页面资源的实例分享
2016/05/03 Python
Python利用matplotlib.pyplot绘图时如何设置坐标轴刻度
2018/04/09 Python
Python检测网络延迟的代码
2018/05/15 Python
Django 实现购物车功能的示例代码
2018/10/08 Python
详解重置Django migration的常见方式
2019/02/15 Python
pandas分区间,算频率的实例
2019/07/04 Python
python爬虫selenium和phantomJs使用方法解析
2019/08/08 Python
tensorflow如何继续训练之前保存的模型实例
2020/01/21 Python
货代行业个人求职简历的自我评价
2013/10/22 职场文书
给老婆的搞笑检讨书
2014/01/12 职场文书
婚礼秀策划方案
2014/05/19 职场文书
班组长安全工作职责
2014/07/15 职场文书
新疆民族团结演讲稿
2014/08/27 职场文书
建党伟业电影观后感
2015/06/01 职场文书
学习习近平主席讲话心得体会
2016/01/20 职场文书
使用Python的开发框架Brownie部署以太坊智能合约
2021/05/28 Python
GoFrame框架数据校验之校验结果Error接口对象
2022/06/21 Golang
MySQL 原理优化之Group By的优化技巧
2022/08/14 MySQL