标签:keep recycle 问题 res redis数据库 mil 标记 pre main
Scenario:
一个类似ticket master的网站。 说某个时间段开放某明星演唱会订票,大概会同时有500K QPS 的访问量,一共只有50K张票。 订票的过程是用户打开订票网页(不用考虑认证等问题),填一个text box说要订几张票,然后click一个button就打开一个page,那个page会不停的spin直到系统能够预留那几张票,如果预留成功,用户会有几分钟时间填写用户信息已经完成支付,如果到期未支付,这些票就自动被系统收回了。每张票都是一样的,没有位置信息什么的。
求教怎么design这个系统,我一开始看到500K QPS就有点慌乱了。
订票网站一直都是世界难题。12306应该比这个还恐怖。
不要被 500k QPS吓到。500k也好,5k也好,500也好,分析的方法和思路都是一样的。
这个题的关键首先是给出一个可行解(无论任何系统设计题的关键都是如此),一个核心要实现的功能是,票的预留与回收。在设计可行解的时候,可以先将500k qps抛之脑后。假设现在只有10个用户来买票。优化的事情,放到Evolve的那一步。
按照我们的SNAKE分析法来:
响应时间
——是在用户点击的一瞬间就要完成预定么?不是,可以让用户等个几分钟。也就是说,500K的请求,可以在若干分钟内完成就好了。因此所谓的 500k QPS,并不是Average QPS,只是说峰值是 500k QPS。而你要做的事情并不是在1秒之内完成500k的预定,而是把确认你收到了购票申请就好了。id(primary_key)
created_at(timestamp)
concert_id(foreign key)
user_id(foreign key)
tickets_count(int)
status(int)
简单的说就是谁在什么时刻预定了哪个演唱会,预定了几张,当前预定状态是什么(等待,成功,失败)。
2. TicketService —— 系统从数据库中按照顺序选出预定,完成预定,预定成功的,生成对应的Ticket。表结构如下:
id (primary key)
created_at (timestamp)
user_id (fk)
concert_id (fk)
reservation_id (fk)
status (int) // 是否退票之类的
另外,我们当然还需要一个Concert的table,主要记录总共有多少票:
id (primary key)
title (string)
description (text)
start_at (timestamp)
tickets_amount (int)
remain_tickets_amount (int)
...
总结一下具体的一个Work Solution 的流程如下:
1. 用户提交一个预定,ReservationService 收到预定,存在数据库里,status=pending
2. 用户提交预定之后,跳转到一个等待订票结果的界面,该界面每隔5-10秒钟像服务器发送一个请求查询当前的预定状态
3. TicketService是一个单独执行的程序,你可以认为是一个死循环,不断检查数据库里是否有pending状态的票,取出一批票,比如1k张,然后顺利处理,创建对应的Tickets,修改对应的Reservation的status。
分析一下上述的每个操作在500k qps的情况下会发生什么,以及该如何解决。
1. 用户提交一个预定,ReservationService 收到预定,存在数据库里,status=pending
也就是说,在一秒钟之内,我们要同时处理500k的预定请求,首先web server一台肯定搞不定,需要增加到大概500台,每台web server一秒钟同时处理1k的请求还是可以的。数据库如果只有一台的话,也很难承受这样大的请求。并且SQL和NoSQL这种数据库处理这个问题也会非常吃力。可以选用Redis这种既是内存级访问速度,又可以做持久化的key-value数据库。并且Redis自带一个队列的功能,非常适合我们订票的这个模型。Redis的存取效率大概是每秒钟几十k,那么也就是我们要大概20台Redis应该就可以了。我们可以按照 user_id 作为 shard key,分配到各个redis上。
2. 用户提交预定之后,跳转到一个等待订票结果的界面,该界面每隔5-10秒钟像服务器发送一个请求查询当前的预定状态
使用了redis的队列之后,如何查询一个预定信息是否在队列里呢?方法是reservation的基本信息除了放到队列里,还需要同时继续存一份在redis里。队列里可以只放reservation_id。此时reservation_id可以用user_id+concert_id+timestamp来表示。
3. TicketService是一个单独执行的程序,你可以认为是一个死循环,不断检查数据库里是否有pending状态的票,取出一批票,比如1k张,然后顺利处理,创建对应的Tickets,修改对应的Reservation的status。
为每个Redis的数据库后面添加一个TicketService的程序(在某台机器上跑着),每个TicketService负责一个Redis数据库。该程序每次从Redis的队列中读出最多1k的数据,然后计算一下有需要多少张票,比如2k,然后访问Concert的数据库。问Concert要2k的票,如果还剩有那么多,那么就remain_tickets_amount - 2k,如果不够的话,就返回还有多少张票,并把remain_tickets_acount 清零。这个过程要对数据库进行加锁,可以用数据库自己带的锁,也可以用zookeeper之类的分布式锁。因为现在是1k为一组进行处理,所以这个过程不会很慢,存Concert的数据库也不需要很多,一台就够了。因为我们只有20个跑着TicketService的机器(对应20台Redis),也就是个20 qps的峰值,数据库处理起来绰绰有余额。
假如得到了2k张票的额度之后,就顺序处理这1k个reservation,然后对每个reservation生成对应的tickets,并在redis中标记reservation的状态,这里的话,tickets的table大概就会产生2k条的insert,所以tickets的数据库需要大概能够承受 20 x 1k = 20k 的并发写。这个的话,大概 20 台 SQL数据库也就搞定了。
增加一个RecycleService。这个RecycleService 不断访问 Tickets 的数据库,看看有没有超时的票,如果超时了,那么就回收,并且去Concert的数据库里把remain_tickets_acount 增加。
核心点就是,500k QPS 我只要做到收,不需要做到处理,那么500台web服务器+20台Redis就可以了。
处理的的时候,分成1k一组进行处理,让用户多等个几秒钟,问题不大。用户等10秒钟的话,我们需要的服务器数目就降低10-20倍,这是个tradeoff,需要好好权衡的。
问:500台Web服务器很多,而且除了订票的那几秒种,大部分的时候都是闲置浪费的,怎么办?
答:用AWS的弹性计算服务,为每场演唱会的火爆指数进行评估,然后预先开好机器,用完之后就可以销毁掉。
问:为什么不直接用Redis也来存储所有的数据信息?
答:因为是针对通同一个Concert的预定,大家需要访问同一条数据(remain_tickets_acount),shard是不管用的,Redis也承受不住500k QPS 对同一条数据进行读写,并且还要加锁之类的保证一致性。所以这个对 remain_tickets_acount 的值进行修改,创建对应的 tickets 的过程,是不能在用户请求的时候,实时完成的,需要延迟进行。
问:redis又用来做队列,又用来做Reservation 表的存储,是否有点乱?
答:是的,所以一个更好的办法是,只把redis当做队列来用 和 Reservation 信息的Cache来用。当一个Reservation 被处理的时候,再到SQL数据库里生成对应的持久化记录。这样的好处是,Redis 这种结构其实不是很擅长做持久化数据的存储,我们一般都还是拿来当队列和cache用得比较多。
我用了2个小时来写这个帖子回复你,是不是该给我一个小红花!
标签:keep recycle 问题 res redis数据库 mil 标记 pre main
原文地址:http://www.cnblogs.com/liangmou/p/7782996.html