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

  秒杀活动

  所谓的秒杀活动,简单点来说,就是把某个稀缺商品或促销商品,挂到页面,供大量客户抢购。这里有两个关键点,商品数量不多客户量非常大或抢购流量非常大。客户量或抢购流量往往意味着并发量非常大,容易给服务器造成很大的瞬时压力。

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

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

  我们把抢购请求只划分为成功和失败两种,请大家一定要注意,这里其实我们忽略了付款的问题,因为这会增加问题的复杂度,不利于分析问题。其实吧,付款是可以规避掉的,提前让用户付款,然后再抢购不就好了?抢购成功意味着付款成功

  在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技术非常优美的解决的,我这里就不啰嗦了,欢迎大家留言讨论。


  补充。

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

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

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

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


  2018年9月20日

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

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

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


  2018年9月20日19:39:47

  

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

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

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

  机器环境如下:

  

秒杀系统架构分析与实战

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

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

秒杀活动一般怎么做

akka设计模式系列-akka在秒杀场景的应用的更多相关文章

  1. akka设计模式系列-慎用ask

    慎用ask应该是Akka设计的一个准则,很多时候我们应该禁用ask.之所以单独把ask拎出来作为一篇博文,主要是akka的初学者往往对ask的使用比较疑惑. "Using ask will ...

  2. akka设计模式系列-消息模型(续)

    在之前的akka设计模式系列-消息模型中,我们介绍了akka的消息设计方案,但随着实践的深入,发现了一些问题,这里重新梳理一下设计方法,避免之前的错误.不当的观点给大家带来误解. 命令和事件 我们仍然 ...

  3. akka设计模式系列-actor锚定

    actor锚定模式是指使用actorSelection对acor进行锚定的设计模式,也可以说是一个对actor的引用技巧.在某些情况下,我们可能需要能够根据Actor的path锚定对应的实例.简单来说 ...

  4. akka设计模式系列

    由于本人爱好Scala,顺便也就爱好Akka,但目前网上对Akka的介绍大多都是概念上或技术方向上的介绍,基本没有Akka设计模式或者Actor模型设计模式的资料.这对于Akka的普及非常不利,因为即 ...

  5. akka设计模式系列(Actor模型)

    谈到Akka就必须介绍Actor并发模型,而谈到Actor就必须看一篇叫做<A Universal Modular Actor Formalism for Artificial Intellig ...

  6. akka设计模式系列-While模式

    While模式严格来说是while循环在Akka中的合理实现.while是开发过程中经常用到的语句之一,也是绝大部分编程语言都支持的语法.但while语句是一个循环,如果循环条件没有达到会一直执行wh ...

  7. akka设计模式系列-Chain模式

    链式调用在很多框架和系统中经常存在,算不得上是我自己总结的设计模式,此处只是简单介绍在Akka中的两种实现方式.我在这边博客中简化了链式调用的场景,简化后也更符合Akka的设计哲学. trait Ch ...

  8. akka设计模式系列-Backend模式

    上一节我们介绍了Akka使用的基本模式,简单点来说就是,发消息给actor,处理结束后返回消息.但这种模式有个缺陷,就是一旦某个消息处理的比较慢,就会阻塞后面所有消息的处理.那么有没有方法规避这种阻塞 ...

  9. akka设计模式系列-消息模型

    通过前面的文章我们总结了几个常见的actor设计模式,但此处不得不提前介绍一下在Akka中消息的设计模式.随着对Akka的使用,我们会发现,使用Akka设计系统其实就是面向消息编程.actor之间消息 ...

随机推荐

  1. C. Nearest vectors--cf598C(极角排序)

    http://codeforces.com/problemset/problem/598/C 题目大意: 给你你个向量  向量的起点都是从(0,0)开始的   求哪个角最小  输出这两个向量 这是第一 ...

  2. BZOJ 1798:

    6:       LAZY 线段树有乘法的更新    #include <cstdio> #include <cstring> #include <algorithm&g ...

  3. 学习日常笔记<day16>mysql加强

    1.数据约束 1.1什么是数据约束 对用户操作表的数据进行约束 1.2 默认值 作用:当永辉对使用默认值的字段不插入值的时候,就使用默认值 注意: 1)对默认值字段插入null是可以的 2)对默认值字 ...

  4. Javascript标准事件模型

    本文为原创,转载请注明出处: cnzt       文章:cnzt-p http://www.cnblogs.com/zt-blog/p/6676913.html 1. 分类 IE -- 冒泡型 现代 ...

  5. how to read openstack code: service plugin

    We have learned core plugin, service plugin and extension in last post. Now let`s review: Core Plugi ...

  6. django 简易博客开发 3 静态文件、from 应用与自定义

    首先还是贴一下源代码地址  https://github.com/goodspeedcheng/sblog 上一篇博客我们介绍了 django 如何在views中使用templates以及一些常用的数 ...

  7. JAVA数组去除重复数据

    一.用List集合实现   , , , , , , ,}; List<Integer> list = new ArrayList<Integer>(); ; i<str. ...

  8. org.json.JSONException: A JSONObject text must begin with &#39;{&#39; at character 1 of {解决方法

    在使用java读取一个本地的json配置文件的时候,产生了这个异常:org.json.JSONException: A JSONObject text must begin with '{' at c ...

  9. 【Java 虚拟机探索之路系列】:JIT编译器

    作者:郭嘉 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells github:https://github.com/AllenWell 为 ...

  10. crm使用soap删除下拉框

    //C# 代码: //DeleteOptionSetRequest request = new DeleteOptionSetRequest(); //request.Name = "new ...