事故描述
在一次项目中,上线了一新功能之后,陆陆续续的有客服向我们反应,有用户的个别道具数量高达42亿,但是当时一直没有到证据表示这是,确实存在,并且直觉告诉我们,这是不可能的,就一直没有在意,直到后来真的发现了一个用户确实是42亿,当时我们整个公司都震惊了,如果有大量用户是这样的情况,公司要亏损几十万,我们的老大告诉我们,肯定是什么地方数据溢出的,最后我们一帮人,疯了似的查代码,发现……
如果按照正常的程序逻辑走下去,代码是完全没问题,但是我发现了一个地方,如果在高并发的情况下,是会走出错误的程序逻辑的,为什么我看到了,很简单,我在上一家公司,因为这个问题困扰了我一个多月,真是刻骨铭心呀!不多说了,说多了都是泪呀!上代码,重现场景(以下都是一些简单的代码,用来重现场景),道具名就定义为props。
<?php
mysql_connect("localhost", "mysql_user", "mysql_password");
mysql_select_db("user");
$consume_props_count = 10; // 要消耗的道具数量
$select = 'SELECT `count` from `user` where `userid`=123456';
$query = mysql_query($select);
$row = mysql_fetch_assoc($query);
// 对比当前道具数量
if ($row['count'] < $consume_props_count) {
return false;
}
// 扣除道具数量
$update = "update `user` set `count`=`count`-{$consume_props_count}";
$query = mysql_query($update);
return mysql_num_rows($query);
?>
大家可以看到,如果按照正常的扣除道具的流程来走,这个是没有问题的,但是在高并发场景下,两次扣道具的查询极有可能获取的是同一个结果,然后他们都能通过对比当前道具数量这一步逻辑,但是加入第一次的扣道具的操作把道具扣光了,或者扣的不够第二次继续扣了,想想会发生什么样的情况!
坑爹了!第二次会把道具数量扣为负数。
但是这依然不能解释这42亿的溢出那里来的!哎,祸不单行的古语再次应验了,我们的count字段的数据类型是Unsigned int,坑爹的MySQL,假如字段是Unsigned int,然后输入了一个负数,它就会让这个数字变为42亿这个巨大的数值。
看明白了吧!这42亿就是这么来的,坑啊!
解决方案的话,可以在update的sql改成这样
1 <?php
2 $update = "UPDATE `user` SET `count`=(CASE WHEN `count`<={$consume_props_count} THEN 0 ELSE `count`-{$consume_props_count} END) WHERE `userid`=123456"
3 ?>
这样,就不会在扣成负数了,另外以防万一,还要将Unsigned int的字段类型,改为int。