上周做了一个周年庆的抽奖的需求, 抽奖的规则一般就是在概率的基础上, 将奖品按时间跨度均匀分配, 奖励有实物和虚拟奖励. 抽奖代码的核心在:按时间轴均匀分配奖励和严格控制奖励的数量, 不能出现数量的不一致.
昨天上线后, 今天早上发生了一件从来没有想到会发生的bug, 就是抽奖滚屏公告不见了, 就是前台拉取不到中奖滚屏信息了. 果然是万万没想到呀! 通过脚本直接从后台拉取也是同样的结果, 问题出现在哪里?
后台有一台存放游戏中所有滚屏公告的server, server中存放了不同msg_type的滚屏公告, 同一msg_type的滚屏信息通过key-value结构进行组织, key是一个uint32_t递增的unqiue seq, value是公告内容. 如下图:
前台在每次拉取某一类型滚屏公告时,seq都会传入0, 后台会通过lower_bound来根据传入的seq和该msg_type的map进行比较,找到>= seq的第一个滚屏信息,然后返回req中对于数量的公告数目. 问题就出现在这里,lower_bound查找无结果, 但根据debug log可以看到是有公告插入的, 最终经过仔细跟踪log, 发现在公告插入的时候自动生成的seq是一个超过2147483648 = 0x80000000的数, 这里没有任何问题, 因为msg_type对应的map的key是一个uint32_t类型, 但仔细查看map的compare函数, 发现了问题
1 | inline bool seq_lt(uint32_t a, uint32_t b) |
所以在seq传入0时, 进行lower_bound查找时,在msg map中的seq都大于0x80000000时, seq_lt的结果都是true, 这样所有的seq都小于0, lower_bound的结果始终是空, 但map中其实是有数据的, 那为什么当初写代码的人如此来设计compare函数呢, 询问事故当事人, 原因是: 整数回绕
什么是整数回绕呢? 简单的说:对于uint32_t的整数, 0xFFFFFFFF 和0x0是连续的.
经典的用法是在tcp协议中, tcp要求字节流是有序的传送的, 对于丢包和乱序等问题的判断都是依赖于序列号大小比较的, seq是uint32_t类型的, 所以当seq增加到(2^32 - 1)时,下一个seq就会回绕到0, 这就tcp的sequence number wraparound序号会绕, 这就需要保证0xFFFFFFFF的序号是小于0的, 这里tcp内核源码的做法就是上面的seq_lt代码, 对于a = 0xFFFFFFFF, b = 0, 1, 2…的时候a 都是< b的,这样就能保证字节流的有序传输.
那么这个绕回检测的范围是多少呢? 之所以会提出这个问题, 是因为在seq_lt的代码中, 0x80000000 < 0是成立的, 但0x7FFFFFFF 是> 0的.
其实不难看出其会绕跨度最大为:(2^31), 对于a 属于[0x80000000, 0xFFFFFFFF]之间, b 在[0, a - 2^31]之间, b都属于回绕数字.
- 在a = 0x80000000时, b = 0, (int32_t)(a - b) < 0, 此时b是a递增后回绕的数据;
- 在a = 0x80000000时, b = 1, (int32_t)(a - b) = 0x7FFFFFFF > 0 , 此时是检测不出绕回的;
所以数字回绕的检测是有特定场景的条件限制, 要保证用于判断回绕的数字跨度不能超过2^(n - 1),n是数字的位数.所以 不能将上述的seq_lt用于一般的数字比较当中.