最近和zhiyong聊天,有两个关于并发的场景比较有意思,记录下思考的过程。
场景一
A和B在微信里发消息,如果A发的消息顺序是A1,A2,A3,B发的消息顺序是B1,B2,B3。
两个问题:
- 如果A2由于网络原因,比A3后到服务器,B收到的消息是什么样的?
- 如果都不丢消息,A1和B1同时发出,几乎同时到服务器,再分别传给A和B,那么在聊天窗口的表现是什么?
经过zhiyong的黑盒逆向测试,问题1的答案是B看到A1和A3,在A看到A2发送失败,有个红色叹号,如果点击重发,相当于发出了一个相同内容的A4。
问题2的答案是可能A看到的是A1,B1,B看到的是B1,A1,即在不同人之间的消息,没有严格的顺序,以客户端本地上屏的顺序为主。(在A来说,A1发完就上屏,B1是后来接收到的,就A1在上面,同理B也是)。如果这时打开一个新终端来同步,那么以服务器那份数据为主,按照时间线的顺序拉取。
分析
问题1中,在微信客户端上传消息时,应该给每条消息都加了序列号,按照发送的顺序升序,如果服务器发现序列号回跳缩小了,就丢弃掉,并返回客户端失败。这样做的好处是处理简单,不会造成用户收到一条消息,突然又蹦出一跳消息插在了前面,导致漏看消息,都是追加写。
当然也有另外的实现方式,就是没序号,你发什么服务器收什么,哪条先到就哪条在先,但是会有个新问题,用户本想发的是A1,A2,因为人的语言是有顺序的,但是看到的是A2,A1,有些语境乱掉的会引起歧义。
在丢消息和乱消息两者,选择了可以丢。
问题2也是,可以做到以服务器为准,纠正客户端的表现,做到强一致,但是微信认为这不是个问题,两个人同时发的哪个先哪个后不影响理解内容,以每个手机上屏时间为准。但是在同步新消息时,以服务器为准,因为此时根本没有冲突。
如果单纯从技术角度解决这两个问题,也有很直接的方法。对于问题一,发现漏了序号,服务器再下发一小消息,让客户端重传,然后等待重传成功再发给接收方,但这种方法复杂,出问题也不易调试,在产品需求上得不偿失。对于问题二,建立个全局id,每条消息都分个先后,对于全局id的一致和出错恢复的影响,要投入巨大精力来实现,能解决的还是个很少发生的场景问题,甚至真发生了,也不算个问题。
场景二
用户从其他渠道拿到兑换码,然后在小米官网输入兑换码,兑换奖品,每个码只能用一次,每个用户一天只能兑换2次奖品。此时会有问题。兑换码标记失效,和给用户发奖,是两个动作,会有并发问题。两个动作的先后顺序,哪个先哪个后。是否要用事务。
也是两个问题:
- 两个用户A和B用同一个兑换码同时兑换,怎么处理?
- 一个用户同时用两个兑换码兑换,发现当天只能有一次兑换机会时,两个码都失效吗,会多扣一个码吗?
分析
方法一最简单方法,用事务,每次兑换前开启事务,兑换后结束事务。优点:代码简单。缺点:事务太重,在互联网高并发的场景中,性能不高。
方法二先扣兑奖次数,再失效兑换码。这种情况在两个用户用同一个码并发时,会判断都有兑换次数,都扣,但失效兑换码时,只有一个人成功,失败的那个人被多扣了一次抽奖机会。
方法三先失效兑换码,再扣抽奖次数。两个用户用同一个码时,后用的提示码已用过,没问题。一个用户用两个码并发时,先把两个码都失效,再根据兑奖次数,只有一个兑换码成功兑换了奖品,多扣了一个兑换码。
最后选择的方法三,因为两个用户用同一个码是存在的,但是一个用户同时用两个码的概率比较小,用户要输入码再点确定才行。还有如果给用户多扣了一个兑换码的情况,只有用户当天没有兑换次数的时候才出现,不会对用户造成更大的损失。后台根据告警,用修复脚本把多扣的券找出来恢复,用户第二天还能用。
总结
并没有什么银弹,设计的那种方案的好坏,要结合具体需求,详细分析,深入思考需求的特点,在实现复杂度和满足需求找到平衡。