有一个答题的小项目,表的字段如下:
id 用户id
times 答题次数
questions 回答的问题id,这是一个php serialize()的字符串
当用户答完题像后端提交结果时,构建的post包如下:
{ ‘id‘:1, ‘questionid‘:38 }
后台逻辑如下:
$user = User::findFirst("id = ?1",array(‘id‘=>$_POST[‘id‘]));//取得用户模型 $questions = unserialize($user->questions);//反序列化 $questionid = (int)$_POST[‘questionid‘];//题目id if ( ! in_array($questionid,$questions)) {//判断用户是否答过,没有答过则将题目id添加到questions字段里 $questions[] = $questionid; $user->times += 1;//统计答题次数 $user->questions = serialize($questions);//序列化questions $user->save();//update } else { exit(‘对不起,这个题目你已经答过‘); }
看上去流程没有问题啊,用户每次提交的时候会先读取自己的数据,检测是否答过这个题目,答过则直接退出。
但是出问题了:
当活动推出的第二天,后台数据发现有个用户的times答题次数和questions记录数不匹配,表现为 times=120 count(unserialize(questions))=17。
当时很奇怪,代码正常流程是没问题的,卡了半天,一直没想出来用户如何造成这种数据的,直到刚才,终于开窍,正如题目中所说,问题出在并发上,这个用户可能自己写脚本或者借助第三方工具,模拟post请求,并使用多线程等技术造成并发请求,我们来模拟一下并发过程:
用户同时发起两个请求,php也就分配了两个进程来处理,当着两个进程几乎同时执行了这行代码:
$user = User::findFirst("id = ?1",array(‘id‘=>$_POST[‘id‘]));//取得用户模型
此时在两个进程中的$user数据是一样的,我们假设一下,此时$user->questions 的值为 {1,2}。
假设两个请求构建的post数据包分别为:
{ ‘id‘:1, ‘questionid‘:3 }
和
{ ‘id‘:1, ‘questionid‘:4 }
那么当代码执行到:
in_array($questionid,$questions)
的时候实际上返回的都是 false
他们相继发起了 $user->save() ,分别希望将 3 和 4 加进questions,注意,这时候questions在数据库中的值还是{1,2},而sql语句是一条条执行的,也就是说数据库按顺序执行了两次update操作,先是将questions的值更改为 {1,2,3},随即第二个save()执行又将其改为了{1,2,4},两次times都增加1,所以最终的结果时:
times questions
4 {1,2,4}
这时候如果用户继续提交 ‘questionid‘:3 ,则又变更为:
times questions
5 {1,2,3,4}
这是两个并发的情况,会造成times比questions的数量多 1
如果模拟10个并发呢?不想算了。
问题既然已经出现了,还是得解决啊,首先想到用innodb的行级锁,很遗憾,表是myisam…
最终的解决方案是通过操作系统文件排他锁实现的,屏幕前的朋友如果有兴趣可以看:
http://my.oschina.net/cxz001/blog/281130