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 相关文章推荐
第四节 构造函数和析构函数 [4]
Oct 09 PHP
PHPMailer安装方法及简单实例
Nov 25 PHP
php FPDF类库应用实现代码
Mar 20 PHP
解析php下载远程图片函数 可伪造来路
Jun 25 PHP
基于php常用函数总结(数组,字符串,时间,文件操作)
Jun 27 PHP
PHP中new static() 和 new self() 的区别介绍
Jan 09 PHP
php中实现可以返回多个值的函数实例
Mar 21 PHP
Windows下编译PHP5.4和xdebug全记录
Apr 03 PHP
php实现简单的MVC框架实例
Sep 23 PHP
PHP获取IP地址所在地信息的实例(使用纯真IP数据库qqwry.dat)
Nov 15 PHP
PHP二维数组去重算法
Dec 17 PHP
laravel框架select2多选插件初始化默认选中项操作示例
Feb 18 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
PHP学习之PHP变量
2006/10/09 PHP
PHP4实际应用经验篇(2)
2006/10/09 PHP
php对数组内元素进行随机调换的方法
2015/05/12 PHP
PHP使用flock实现文件加锁的方法
2015/07/01 PHP
使用PHP similar text计算两个字符串相似度
2015/11/06 PHP
PHP+iframe图片上传实现即时刷新效果
2016/11/18 PHP
JQuery 无废话系列教程(一) jquery入门 [推荐]
2009/06/23 Javascript
用JavaScript隐藏控件的方法
2009/09/21 Javascript
IE的fireEvent方法概述及应用
2013/02/22 Javascript
Javascript实现返回上一页面并刷新的小例子
2013/12/11 Javascript
浅谈JavaScript字符集
2014/05/22 Javascript
JavaScript DOM进阶方法
2015/04/13 Javascript
jQuery实现表格行上移下移和置顶的方法
2015/05/22 Javascript
简介JavaScript中的setTime()方法的使用
2015/06/11 Javascript
js改变css样式的三种方法推荐
2016/06/28 Javascript
Angular中$state.go页面跳转并传递参数的方法
2017/05/09 Javascript
在vue项目创建的后初始化首次使用stylus安装方法分享
2018/01/25 Javascript
详解微信小程序-canvas绘制文字实现自动换行
2019/04/26 Javascript
JS实现网站楼层导航效果代码实例
2020/06/16 Javascript
Python面向对象编程中的类和对象学习教程
2015/03/30 Python
Python同时向控制台和文件输出日志logging的方法
2015/05/26 Python
对python3中, print横向输出的方法详解
2019/01/28 Python
详解Tensorflow不同版本要求与CUDA及CUDNN版本对应关系
2020/08/04 Python
美国最受欢迎的度假目的地优惠套餐:BookVIP
2018/09/27 全球购物
医药大学生求职简历的自我评价
2013/10/17 职场文书
自我评价的范文
2014/02/02 职场文书
药品采购员岗位职责
2014/02/08 职场文书
和谐家庭演讲稿
2014/05/24 职场文书
珍惜资源的建议书
2014/08/26 职场文书
临时工聘用合同协议书
2014/10/29 职场文书
小学教育见习总结
2015/06/23 职场文书
Nginx进程管理和重载原理详解
2021/04/22 Servers
如何使用分区处理MySQL的亿级数据优化
2021/06/18 MySQL
Python面向对象之成员相关知识总结
2021/06/24 Python
JavaScript数组reduce()方法的语法与实例解析
2021/07/07 Javascript
Java中Dijkstra(迪杰斯特拉)算法
2022/05/20 Java/Android