20160824(公开课)并发冲突的处理

情景:

大家可能都有这样的经验,自个儿在家里很多功能很容易实现,一下就做完了,但是在做线上产品的时候,就变得无比复杂,需要花费很多的时间
自己写的程序在家跑,所有的业务都很正常,一旦发布到线上,就会出现很多bug,而且很多bug在测试的时候很难重现,这是在互联网开发的时候经常遇到的现象。
这些难以重现的bug,大部分是由于并发产生的,为了能让大家充分的了解并发的问题,并且建立并发环境下的程序设计思维,我们为大家准备了几个小案例

1.png

大家来看一下,这个网站典型的场景,遇到内容比较多的情况下,我们会使用分页

如果是在移动端,我们采用的是上拉刷新和下拉刷新,比如手机微博

2.png

分页功能由来已久,我们现在来看下常见的分页有什么问题呢?

大家看下我展示的这个项目是用thinkPHP来编写的。很多流行的框架和开源项目都对分页做了支持,那么大家来看我演示一个案例。

3.png

看到的现在的第一页是最新的新闻,如果我点击下一页,相对来说是老一点的新闻。

4.png

这个时候我们来做一件事情,我们新打开一个浏览器窗口,刚刚的分页页面不要关闭,我们在后台里面再发布5篇新闻,
假设我们在后台发布新闻的同时,用户还在浏览第二页,当发布完毕以后,用户又点了下一页,我们观察到了奇特的现象

我们看到第3页和刚刚看到的第二页完全一样,原本用户点下一页希望看到再早一些的新闻,结果看到了一样的数据,是我们的分页程序出问题了吗?非也。

当我们再点下一页,程序又正常工作了。刚刚出现的奇怪的那一幕,其实就是由于并发产生的冲突。

在什么地方我们做了并发操作了呢?其实就是在一个用户浏览页面的同时,还有人在往数据库里面写入数据。你会发现thinkPHP这样的框架,还是PHPCMS这样的开源系统,他们都存在这样的bug。

并发产生的问题,往往难以捕捉,更难以重现,而我们准备的这个案例,算是并发案例冲突中相对容易重新的典型案例

我们如果调整一下刚刚操作的顺序,我们会得到一些其他的结果。

比如说:我们分页条目数是每页5条,在用户浏览某一页的时候,后台管理员发布了新的新闻,新闻的数量小于5条的情况下,我们点下一页,会看到有几条重复的新闻,也有更早的新闻

这样影响用户体验,但毕竟我们点一下一的页时候内容还是能接的上的,用户浏览某一页的时候,后台发布的新闻数量大于分页条目数(5条)

那么再点击下一页,其中会有若干条新闻被跳过去了,无论用户点多少次下一页,都看不到那些条目,这种产生的并发冲突后果是很严重的,而且普遍存在。

他具有很好的隐蔽性,在过去很多年几乎不被察觉

在大门户网站时代,网站编辑并不会频繁的发布新闻,而用户也很少守着新闻列表去逐篇阅读,然而在微博诞生以后,这种问题就暴露出来了。

由于社区类应用信息的产生室友用户产生的,不在是靠编辑在后台发布的,就好像微博,每时每分秒,都有很多用户分享心得内容

这样一来你在查看微博列表的同时,内容就已经更新 了 很多,而且很多人打发无聊的时间,很多人也会去逐一查看,不停的上划,把所有遗漏的条目全部看一遍

这是如果项目设计不合理,产生并发事故,就会对用户体验造成极大的影响。

待会我会再讲并发冲突解决方案的时候告诉大家如何解决这种问题

现在我们来看一个更为常见的例子,大家可能还记得,我们在微信群搞的抽奖的互动,我们的上百份礼品,瞬间就被秒杀光。

在做这类抢购与秒杀抽奖等应用的时候,并发将导致更多的问题。通常比较容易出现的bug有实际商品的订单量大于库存量。

通俗点来说就是,明明已经售完,但还是有用户买到了商品,库存值变为负的。

又或者明明秒杀到商品的用户,订单失败。

还有企业的项目,在商品秒杀期间,明明用户数量不多,却导致服务器宕机。诸如此类的问题就不一一列举了。

我们接下来来看一下用常规思维来梳理业务流程程序是怎样编写的。

还是以商城秒杀业务为例。首先我们需要用产品库存这样的一个字段来记录库存信息,每当有用户购买商品的时候,先查看库存,判断库存大于0的时候,用户才能购买

当用户完成购买流程后,将库存数量减一,直到所有商品卖完,重复此过程,直到库存卖完秒杀活动结束。

如果按常规的思路来设计,这样的流程是没有问题的,商品毕竟是一件一件卖出的,但是,在互联网并发的情况下,就完全不是这样的。

要知道热销商品很有可能在同一时间,有多个用户都在进行购买流程操作

按照之前的业务设计,假如有ABCD 4个用户同时在秒杀某件商品时,库存仅剩2件,按照之前的业务流程设计,查询库存大于0,就可以继续后面的购买操作并付款

然而当任意用户购买成功后库存即减一,ABCD4个用户都认为自己查询时都有库存,因此他们都可以完成购买流程,导致的结果就是库存数为负数。

也就是说,商品实际销售量大于活动的商品数量,这样会导致公司的亏损。

有些公司为了解决这个问题,采用了一种思路,虽说4个人同时操作,但是交易成功的这次网络请求到达服务器的时间总会有个先后顺序

那么可以将订单支付成功之后的库存减一之后的值也随订单保存,如果这个值小于0,就证明有用户购买了产品,已经是卖完的,于是标记订单失败。

这样看上去避免公司造成额外的损失,但却会给用户带来极大的不满,是一种极差的用户体验。它并没有真正的解决我们的问题。

当然还有些公司解决方案也不高明,我们知道无论是数据库还是文件都可以给他加锁,在很早期的程序设计和软件开发里面,锁是解决并发问题的万能灵药。

无论是c++,或java,提到多进程或多线程的时候,往往也会提到锁这个字。那么作为最早期的通用解决方案,用到秒杀方案是否合适呢?

我们来看一下加锁后的工作流程:还是ABCD 4个用户同时秒杀,他们都去查询库存。当某一个用户,比如A的请求,优先到达时,我们就将数据表锁住,不让其他的数据库连接来动这张表,待用户A完成购买流程,将库存量减一后,把锁打开,其他的连接才可以再次操作这张表。

如此一来,可以保障一个用户查看库存以及库存减一这段时间内,不可能还有其他用户可以对表做出修改,这一并发冲突的问题就没有了。不过这样的做法真的合适吗?

要知道ABCD 4个用户都是在同一时间段去秒杀的,由于A用户在操作中锁表,导致其他用户只能等待,而且A完成整个业务需要消耗一段时间,只能等A完成以后其他用户才能操作

这样一来单位时间内的业务处理量会大幅降低,我们所看到的现象就是网站卡死,或者服务器宕机

关于并发性能如何设计,我们可能需要单独的一次或几次课来为大家讲解。不过锁这种很原始的并发冲突解决方案,我们可以看到他并不适合互联网项目。

之所以大家会有并发冲突的程序,是因为大部分程序员,思维模式都是线性的。

作为程序逻辑思维来讲,线性思维是没有错的,因为计算机执行指令的时候本身就是线性的。然而如果把业务也看做是线性的,就会产生问题了。

任何一个程序操作,他都会消耗一定的时间,即便你的CPU速度再快,也只是缩短了这个时间范围而已,

如果只有一个用户操作,比如我们在后台发布文章,看自己发布的新闻,我们是无法感知并发带来的冲突的。这就对我们的程序员提出了更高的要求。

理论上来讲,所有跨越时间段的操作过程中如果涉及到数据修改就会有可能产生并发冲突,因此我们在设计程序的时候,要保障应用程序的质量,就需要去做并发冲突处理,只是实现业务需求与实现业务的同时做好质量需求,就是好程序员与坏程序员的差别。

那么分析了产生并发冲突的原因以后,就比较容易思考解决方案了。大体的思路有两种:一种是将并发操作变为单线操作,另一种是让所有跨越时间的段的操作不去更改数据。

我们现在来看一下分页,或者上拉或者下拉刷新的解决方法。我们刚刚提出的2种的解决思路,哪一种比较合适呢?对于发布数据和浏览数据,比如微博,我们有可能把这种并发操作变为单线操作吗?好像不太容易。

那么我们能够走得路就剩下第二条,也就是跨时间段的过程中不要改变数据,我们刚刚产生的bug到底是什么数据改变导致了bug。回顾下我们的代码实现的本质,就容易找到其中的缘由了。

通常我们在实现分页的时候,首页看到的是最新的数据,那么从数据库中取数据的sel语句是select * from news order by desc limt 0,10,这样取到最新的数据,如果点击下一页,查询语句不变,只是分页条目不在是第0-9,而是第10-19条,如果在这个过程中有新的数据插入,我们会发现有一个东西变了,就是原有数据在数据库的排序序号变了,如果我新发布一条数据,原来的第一条最新的新闻就会变成第二条,原来的第10条会变成第11条。这就是一个时间段内的操作过程中有数据发生了改变。

既然我们无法把这样的并发操作变成单线操作,我们可以选择不让数据发生改变,这样并发bug就可以得到很好的解决了。

跨时间段的让数据不改变不好走,那我们可以选择第一种思路,让并发操作变为单线操作,之前提到的加锁是解决方案之一,但是对用户体验不好性能很差,基本上无法再互联网项目中使用。

如果不能加锁,那么常用的解决方案是什么?

我们可以用队列。如果我们将所有的用户请求进行排队,有一个服务来订阅这个队列,那么不管有多少用户访问,最终到服务器端,处理服务的就只有一个进程。这样就实现了一个由并发操作转换成单线操作。

推荐阅读更多精彩内容