设计BookMyShow
让我们设计一个像BookMyShow这样销售电影票的在线售票系统。类似服务:bookmyshow.com, ticketmaster.com
Level: Hard
1. 什么是网上电影票预订系统?
电影票预订系统为其客户提供了在线购买电影票的能力。电子售票系统允许顾客随时随地浏览正在播放的电影,并预订座位。
2. 系统的需求和目标
我们的订票服务应符合以下要求:
功能需求:
1.我们的订票服务应该能够列出不同的城市,其附属影院位于。
2.一旦用户选择了某个城市,该服务就应该显示该城市上映的电影。
3.一旦用户选择了电影,服务应该显示运行该电影的电影院及其可用的节目。
4.用户应该能够选择特定电影院的演出并预订他们的票。
5.该服务应能向用户显示电影院的座位安排。用户应该能够根据自己的喜好选择多个座位。
6.用户应该能够区分可用的座位和已预订的座位。
7.用户在付款完成预订之前,应该能够在座位上停留5分钟。
8.如果有可能有座位可用,用户应该能够等待,例如当其他用户持有的座位过期时。
9.等候的顾客应以先到先得的方式得到公平的服务。
非功能性需求:
1.系统需要高度并发。在任何特定的时间点,同一座位将会有多个预订请求。服务应该优雅而公平地处理这个问题。
2.该服务的核心是订票,这意味着金融交易。这意味着系统应该是安全的,并且数据库应该与ACID兼容。
3. 一些设计方面的考虑
1.为简单起见,让我们假设我们的服务不需要用户身份验证。
2.该系统将不处理部分机票订单。要么用户得到他们想要的所有罚单,要么什么都得不到。
3.公平是系统的必修课。
4.为了防止系统被滥用,我们可以限制用户不超过10个座位。
5.我们可以假设,当人们期待已久的热门电影上映时,车流量会激增,座位很快就会坐满。该系统应该是可伸缩的,高可用性的,以应对流量的激增。
4. 能力评估
流量估计:假设我们的服务每月有30亿的页面浏览量,每月卖出1000万张票。
存储空间估算:假设我们有500个城市,平均每个城市有10个电影院。如果每家影院有2000个座位,平均每天有两场演出。
让我们假设每个座位预订需要50个字节(id、NumberOfSeats、ShowID、MovieID、SeatNumbers、SeatStatus、Timestamp等)存储在数据库中。我们还需要存储关于电影和电影院的信息,假设它需要50字节。所以,存储一天所有城市所有影院的所有节目数据:
500 cities * 10 cinemas * 2000 seats * 2 shows * (50+50) bytes = 2GB / day bytes = 1GB
要存储五年的数据,我们需要大约3.6PB。
5. System APIs
我们可以使用SOAP或REST api来公开服务的功能。以下是用于搜索电影放映和预订座位的api的定义。
SearchMovies(api_dev_key, keyword, city, lat_long, radius, start_datetime, end_datetime, postal_code,
includeSpellcheck, results_per_page, sorting_order)
Parameters:
api_dev_key (string):注册帐户的API开发人员密钥。这将用于根据用户分配的配额限制用户。
keyword (string):要搜索的关键字。
city(字符串):过滤电影的城市。
lat_long (string):要过滤的纬度和经度。radius (number):我们要搜索事件的区域的半径。
start_datetime (string):过滤开始日期时间在这个日期时间之后的电影。
end_datetime (string):过滤开始日期在此日期之前的电影。
postal_code (string):按邮政编码过滤电影。
inclespellcheck (Enum: " yes "或" no"): yes,在响应中包含拼写检查建议。
results_per_page (number):每页返回多少个结果。最大的是30。
sorting_order (string):搜索结果的排序顺序。一些允许的值:' name,asc ', ' name,desc ', ' date,asc ', ' date,desc ', ' distance,asc ', ' name,date,asc ', ' name,date,desc ', ' date,name,asc ', ' date,name,desc ', ' date,name,asc ', ' date,name,desc ', ' date,name,asc ', ' date,name,desc '。
Returns: (JSON)
以下是一些电影和它们的节目:
Parameters:
Api_dev_key (string):同上
session_id(字符串):用于跟踪此预订的用户会话ID。一旦预约时间过期,用户在服务器上的预约将使用此ID删除。
movie_id (string):要保留的电影。
show_id (string): Show to reserve.
seats_to_reserve (number):包含要预留的座位id的数组。
Returns: (JSON)
返回预订的状态,可能是以下情况之一:1)“预订成功”2)“预订失败-显示全部”,3)“预订失败-重试,因为其他用户持有预订座位”。
6. 数据库设计
以下是关于我们将要存储的数据的一些观察结果:
1.每个城市可以有多个电影院。
2.每个电影院将有多个大厅。
3.每部电影将有多个节目,每个节目将有多个预订。
4.一个用户可以有多个预订。
7. High Level Design
在高层次上,我们的web服务器将管理用户的会话,而应用服务器将处理所有的票据管理,将数据存储在数据库中,并与缓存服务器一起处理预订。
8. 详细的组件设计
首先,假设服务是从单个服务器提供的,让我们尝试构建我们的服务。
订票工作流程:以下是典型的订票工作流程:
1.用户搜索电影。
2.用户选择电影。
3.用户被显示电影的可用显示。
4.用户选择一个节目。
5.用户选择要保留的座位数量。
6.如果需要的座位数量是可用的,用户会看到剧院的地图来选择座位。如果没有,则将用户带到下面的“步骤8”
7.一旦用户选择了座位,系统将尝试保留这些被选择的座位。
8.如无法预订座位,我们有以下选择:
●显示充满;用户将看到错误消息。
●用户想要预订的座位不再可用,但是还有其他的座位可用,因此用户被带回到剧院地图以选择不同的座位。
●没有座位可供预订,但由于在预订池中有其他用户持有的一些座位尚未预订,所以所有的座位尚未被预订。用户将被带到一个等待页面,在那里他们可以等待,直到预定池中释放出所需的座位。这种等待可能导致以下选择:
○如果所需的座位数量可用,用户将被带到剧院地图页面,在那里他们可以选择座位。
○在等待时,如果所有座位都预订了,或者预订池中的座位比用户预定的要少,则会显示错误消息。
○用户取消等待,返回电影搜索页面。
○在用户的会话过期并返回到电影搜索页面后,用户最多可以等待一个小时。
1.如果成功预订座位,用户有5分钟的时间为预订付费。付款后,预订被标记为完成。如果用户不能在5分钟内付款,那么他们所有的预留座位就会被释放出来供其他用户使用。
服务器如何跟踪所有尚未预订的活动预订?服务器如何跟踪所有等待的顾客?
我们需要两个守护进程服务,一个用于跟踪所有活动预订,并从系统中删除任何过期的预订,我们将其称为ActiveReservationService。另一个服务将跟踪所有等待的用户请求,一旦所需的座位数量可用,它将通知(等待时间最长的)用户选择座位,我们称其为WaitingUserService。
a. ActiveReservationsService
除了将所有数据保存在数据库中,我们还可以在Linked HashMap中保留所有“show”的内存。我们需要一个Linked HashMap,以便在预订完成时跳转到任何预订并删除它。另外,由于每个保留都有过期时间,所以链接的HashTable的头将始终指向最老的保留记录,因此当超时到达时,保留可以过期。
为了存储每个节目的每个预订,我们可以有一个HashTable,其中' key '将是' ShowID ', ' value '将是包含' BookingID '和创建' Timestamp '的链接HashMap。
在数据库中,我们将预订存储在' Booking '表中,过期时间将存储在Timestamp列中。“Status”字段的值为“Reserved(1)”,一旦预订完成,系统将更新“Status”为“booking(2)”,并从相关显示的Linked HashMap中删除预订记录。当预订过期时,除了将其从内存中删除外,我们还可以将其从Booking表中删除,或者将其标记为“expired(3)”。
ActiveReservationsService还将与外部金融服务合作处理用户支付。无论何时预订完成或预订过期,WaitingUsersService都会收到一个信号,以便为任何等待的客户提供服务。
b. WaitingUsersService
就像ActiveReservationsService一样,我们可以将一个show的所有等待用户保存在一个Linked HashMap中。我们需要一个Linked HashMap,这样当用户取消请求时,我们可以跳转到任何用户,从HashMap中删除他们。此外,由于我们以先到先得的方式提供服务,Linked HashMap的头将始终指向等待时间最长的用户,因此只要有座位,我们就可以以公平的方式为用户提供服务。
我们将有一个HashTable来存储每个Show的所有等待用户。“key”将是“ShowID”,“value”将是一个包含“userid”及其wait-start-time的链接HashMap。
客户端可以使用长轮询来更新自己的预订状态。只要有空位,服务器就可以使用这个请求来通知用户。
预订到期
在服务器上,ActiveReservationsService跟踪活动预订的过期时间(基于预订时间)。客户端将显示一个计时器(过期时间)可以与服务器不同步,我们可以添加一个5秒的缓冲服务器维护从破碎的经验——这样——客户永远不会超时服务器后,防止一个成功的收购。
9. 并发性
如何处理并发;没有两个用户可以预订相同的座位?我们可以在SQL数据库中使用事务来避免任何冲突。例如,如果我们使用SQL server,我们可以利用事务隔离级别来锁定行,然后才能更新它们。下面是示例代码:
“Serializable”是最高级的隔离级别,它保证了Dirty、Nonrepeatable和phantom读取的安全性。这里需要注意的一件事是,在事务中,如果我们读取行,就会获得写锁,因此其他人无法更新这些行。
一旦上面的数据库事务成功,我们就可以开始在ActiveReservationService中跟踪预订。
10. 容错
当ActiveReservationsService或WaitingUsersService崩溃时会发生什么?
每当ActiveReservationsService崩溃时,我们可以从“Booking”表中读取所有活动预订。请记住,在预订完成之前,我们将“Status”列保持为“Reserved(1)”。另一个选择是有主从配置,这样当主崩溃时,从可以接管。因为我们没有在数据库中存储等待的用户,所以当WaitingUsersService崩溃时,我们没有任何方法恢复数据,除非我们有一个主从设置。
类似地,我们将对数据库进行主从设置,使其具有容错能力。
11. 数据分区
数据库分区:如果我们按照“MovieID”进行分区,那么一个电影的所有显示都将在一个服务器上。对于一部非常热门的电影,这可能会在服务器上造成很大的负载。更好的方法是基于ShowID进行分区,这样负载就可以在不同的服务器之间分配。
ActiveReservationService和WaitingUserService分区:我们的web服务器将管理所有活动用户的会话,并处理与用户的所有通信。我们可以使用一致的哈希来根据' ShowID '为ActiveReservationService和WaitingUserService分配应用服务器。这样,一个特定节目的所有预订和等待用户都将由一组特定的服务器处理。让我们假设为了负载平衡,我们的Consistent hashing为任何Show分配了三个服务器,所以每当一个预留过期时,持有该预留的服务器将做以下事情:
1.更新数据库以删除Booking(或标记其过期),并更新' Show_Seats '表中的席位' Status '。
2.从Linked HashMap中移除保留。
3.通知用户他们的预订已经过期。
4.向所有等待该Show的用户的WaitingUserService服务器广播一条消息,以找出等待时间最长的用户。一致的哈希方案将告诉哪些服务器持有这些用户。
5.如果所需的座位可用,则向等待时间最长的用户所在的WaitingUserService服务器发送一条消息来处理他们的请求。
当预约成功时,会发生以下事情:
1.持有该预订的服务器向持有该Show的等待用户的所有服务器发送一条消息,以便它们可以使所有需要比可用座位更多的等待用户过期。
2.所有等待用户的服务器在收到上述消息后,将查询数据库,查看目前有多少空闲座位可用。在这里,数据库缓存将极大地帮助只运行一次查询。
3.在所有等待的用户中,如果用户希望保留比现有座位更多的座位,则将其过期。为此,WaitingUserService必须遍历所有等待用户的Linked HashMap。
Last updated