本博客讨论一下akka在秒杀场景下的应用,提出自己的见解,只做抛砖引玉,大神勿喷。秒杀活动涉及到前中后台各个阶段,为了说明问题,我们简化场景,只研究akka在后台如何处理秒杀业务。

  秒杀活动

商品数量不多客户量非常大或抢购流量非常大。客户量或抢购流量往往意味着并发量非常大,容易给服务器造成很大的瞬时压力。

  同样,为了简化问题,我们把秒杀活动中的概念也进行简化,分为库存和抢购请求。库存:待抢购商品的数量。抢购请求:客户为了抢购商品的点击动作,也就是一次请求。抢购请求分为成功和不成功两种,抢购成功会减少库存,否则不会。

akka原理架构 akka使用场景_kafka

  其实在活动中经常会有海量的、重复的、分布的抢购请求,因为同一个客户会点击多次或开多个页面进行抢购。如果处理这些请求的服务器只有一台,其他的不说,这个瞬时流量都不一定能够承受,因为宽带搞不定了啊。所以应该是多少个节点来处理。另外抢购请求是海量的、分布的,这就意味着并发量很大,如果处理不当还可能造成超卖的情况。传统技术解决这个问题,无非就是加锁、用事务、用队列。

其实吧,付款是可以规避掉的,提前让用户付款,然后再抢购不就好了?抢购成功意味着付款成功

  在akka中,重要的是如何通过actor实现我们的业务逻辑。其实细细分析可以发现,库存就是一个Actor集,每一个商品的实例就是一个actor,也就是一个SKU对应一个actor。抢购请求就是actor收到的消息。

  了解akka机制的同学,大概已经知道怎么解决秒杀问题了,不就是把每个商品抽象成一个actor,每个请求抽象成这个actor的消息么?这跟队列没啥区别啊,因为actor就是用队列来接收消息的。这跟传统的使用队列解决问题非常类似,但还是有些区别的。传统的队列只是用来解决并发问题的,毕竟解决并发的根本方法就是局部串行化。抽象成actor除了使用队列把并发编程串行之外,还分散了计算能力。传统的方案中,可能就是把请求塞到队列,然后使用多个消费者处理这些请求,很难做到分布式,如果做成了分布式,其实跟akka就差不多了。

  那么究竟该如何用akka解决这个问题呢?

首先每个商品(也就是SKU)抽象成一个actor,库存有多少,就有多少个actor。读者可能会问,会不会把内存撑爆,其实不会。抢购的商品,其库存一般都很低,一般都是千级别的,撑死了万级别的。如果你说你的业务场景是十万级别的,甚至百万级别的,那请你麻烦在下面留言讨论。^_^。每个actor自身大概会占用400字节,1G内存大概可以有250万个actor;如果加上你商品的其他属性或其他信息,姑且算每个actor占用4K的空间,那么1G内存可以生成26万的actor。26万件商品对于常规的秒杀活动,应该足够了。

  其次抢购请求抽象成actor的一个消息。那是不是就是简单的把消息发送给actor呢?其实消息路由的过程才是难点。比如用户X发送的抢购消息该发送给哪个商品的actor呢?随机发送?用户发送多次请求,不就会抢购到多个商品?用户ID计算hash之后发送给固定的actor?那如果这个商品被其他人抢到了呢?其实我们还需要一个处理抢购消息的分发器。

  还需要一个抢购请求的router。当然也可以不需要。在每个抢购消息发送之前都需要经过该actor,由该actor对消息进行分发。该router的作用,就是把同一个客户的抢购请求路由到固定的商品actor。那如何实现呢?其实也很简单,那就是用一致性HASH。每个商品和用户都会有一个HASH值,总是把用户ID的HASH值跟商品HASH进行比较,把抢购消息路由给与用户ID的HASH值最接近的商品actor上就可以了。但这个HASH算法需要做到尽可能的分散用户和商品,避免出现热点。其实这个actor的设计比较关键,也比较难,我这里只是提供了方案,并没有说明具体的实现细节,请大家见谅。

  另外商品actor的邮箱类型需要修改成有界队列。因为每个商品就是一个actor,而每个商品只能买个一个客户,所以严格上来说,这个actor只能接收一个抢购请求。所以商品actor的邮箱类型必须是有界队列,避免消息过多撑爆内存,当然这个队列的长度必须是大于1的。超过这个队列长度的消息怎么办呢?在这个商品被抢购成功之前,其实可以直接丢弃,因为已经有其他客户占用这个商品了。当然,占用不意味着一定能够抢成功,也可能失败,比如触发了反薅羊毛策略,或数据库更新失败。因为队列中还有其他的抢购请求,前面的客户抢购失败,还是可以把该商品分配给其他客户的。

  最后还需要计算当前商品的库存,这也需要一个actor。每个商品actor启动时,给stateActor发消息;商品被抢购成功后,给stateActor发消息,同时stop掉自身。这样stateActor就可以异步的获取当前的库存了。

  当然秒杀活动,还有其他很多的技术细节。比如商品actor如何更新数据库呢?毕竟不能每个actor都分配一个数据库链接,压力太大;还比如某个商品被抢购成功后,后面的抢购请求消息需不需要分发给其他商品呢;再比如,如果同时有多个秒杀活动又该怎么办呢;还比如使用分布式,由此带来的一致性、通信异常问题如何解决呢。当然了,这些都是可以用akka技术非常优美的解决的,我这里就不啰嗦了,欢迎大家留言讨论。

 

akka原理架构 akka使用场景_数据库_02


   补充。

  在上面的介绍中,我们还忽略了一个很重要的操作,那就是更新数据库,毕竟最终还是需要把订单等信息写到数据库的,而最终落库才是真正的抢购成功。那么该如何落库呢?

  我们知道每个商品都会处理抢购请求,也就是说,如果有n个商品,那么至少会有n个写数据库的请求,该由谁来完成呢?

  其实有两种方案,一是由stateAggregation 来完成,在收到对应商品actor的stop消息后,更新数据库;二是单独再用一个actor来完成该功能。其实两者都差不多,读者可自行决定。不过我更建议单独用一个actor来完成该功能,因为更新数据库的请求相对来说还是很大的。比如有1万个商品,那么同时就可能有1万个写数据库的请求,相对数据库来说,量还是很大的。此时我们可以用多个actor来完成数据库的写入,此时用一个router,分散更新请求。

  不过更新数据库也有两种方案,一种是单条写入,一种是批量写入。其实秒杀这个场景,更适合批量写入。因为抢购的行为是短时间内的,也就意味着更新数据库的请求也是短时间内的。此时我们可以每1000条更新一次数据库,而不必过分考虑批量时间间隔的问题。因为我们可以把更新数据库的请求看成,全部商品一次性发送过来 。批量写入可以大大节省写数据库的时间,这样也能尽可能的把插入数据库的结果返回给商品actor。只不过最后不满1000条的请求会慢一点,要等下一个超时时间后,批量写入。不过这个时间可以设置的很短,比如1秒。这也就意味着,所有的入库请求,最慢时间是1秒。

 


  2018年9月20日

  其实无论数据库怎么优化,最终都会成为秒杀活动的瓶颈的,因为是海量(对数据库而言)的并发写啊。有时候解决问题的最好方法,就是避免问题的出现。加一层缓存应该可以的喽?

akka原理架构 akka使用场景_akka原理架构_03

 

   上面是优化后的架构。外网用户抢购的请求通过多台Nginx进行转发,nginx将同一个用户的请求转发给后面的同一个region。region根据用户ID将其请求转发给相同的shard。shard里面有一组商品,shard会将相同用户的请求分配给相同的商品。但这会造成即使有商品,某特用户也抢不到,其实吧,这算是变相的公平,也就是说,商品会被随机的分发给不同的用户。不过shard的分配算法也可以修改,分给该shard的用户共同争抢该组内的商品。

  其实商品actor的信息最终是要落库的,考虑到高并发对数据库的压力是不可承受的,在数据库之前需要一层缓冲。可以使用kafka等其他可以迅速将消息落地的技术或框架,缓冲的数据落地到数据库,可以是一个缓慢的过程,因为抢购成功到最终发货还是需要一段时间的。但这里还有一个问题,就是用户去哪里查询自己已经抢到了呢?数据库,还是缓存?其实吧,要我说,只要告诉用户抢购已经结束(库存为0),不一定要立即告诉他有没有抢到。等缓存的消息全都落库,订单最终生成,再告诉他有没有抢到就好了


 

   2018年9月20日19:39:47

  

akka原理架构 akka使用场景_更新数据_04

  商品actor如何快速的落库就成了我们的“心头大患”,本来我想只是简单的把订单信息写入到kafka,kafka虽然也可以保证一定写成功,但又引入了第三方框架。为了偷懒,又设计了上面的架构,即用CacheActor替换kafka等消息队列。

  CacheActor对订单消息的持久化就是关键了,那该怎么做呢?为了偷懒,我选择了内存映射文件,也就是把订单信息写入本地文件。然后等抢购结束后在读取该文件,再进行落库。另外为了减少文件的大小,我准备使用一个位图保存这些信息。即,用户的序号为2345,则在文件的第2345个byte上面写入255,否则就是0。减少文件大小还可以增加写文件的速度,避免错误的发生。

  经测试,在1万个商品,10万个用户,每个用户重复点击100次抢购按钮的情况下,抢购完成的时间为1341毫秒。

  机器环境如下:

  

akka原理架构 akka使用场景_数据库_05

 

 

 

 

秒杀系统架构分析与实战

抢购(秒杀)业务的技术要点

电商技术解密——秒杀系统

秒杀活动一般怎么做