文章目录
- 何为在线售票系统?
- 系统目标和要求
- 1、功能要求
- 2、非功能性需求
- 3、设计注意事项
- 4、容量估算
- 5、系统API
- 1.SearchMovies
- 2.ReserveSeats
- 6、数据库设计
- 7、高级设计
- 8、细节模块设计
- 9、流程
- 服务器如何跟踪所有尚未预订的active预订?服务器如何跟踪所有等待的客户?
- ActiveReservationService
- WaitingUsersService
- 10、并发性
- 11、容错性
- 12、数据分区
何为在线售票系统?
电影票预订系统为其客户提供了在线购买影院座位的能力。Eticketing系统允许客户浏览当前正在播放的电影并预订座位,随时随地。
系统目标和要求
1、功能要求
功能要求:
- 1、我们的售票服务应该能够列出其附属影院所在的不同城市。
- 2、一旦用户选择城市,服务应显示在该特定城市发布的电影。
- 3、一旦用户选择了一部电影,服务应显示运行该电影的电影院及其可用放映时间。
- 4、用户应该能够在特定电影院选择一场演出并预订门票。
- 5、服务应能向用户展示影院大厅的座位安排。用户应该能够根据自己的喜好选择多个座位。
- 6、用户应能区分可用座位和预定座位。
- 7、用户应能在付款完成预订之前,将座位保留五分钟。
- 8、如果座位有可能可用,例如,当其他用户持有的座位过期时,用户应该能够等待。
- 9、等待的客户应以公平、先到先服务的方式进行服务。
2、非功能性需求
1、系统需要高度并发。
在任何特定时间点,同一座位都会有多个预订请求。服务应该优雅而公平地处理这一问题。
2、这项服务的核心是订票,即金融交易。
这意味着系统应该是安全的,并且数据库符合ACID。
3、设计注意事项
1。为简单起见,假设我们的服务不需要任何用户身份验证。
2、系统不处理部分客票订单。要么用户得到了他们想要的所有门票,要么什么也得不到。
3、公平是制度的强制性要求。
4、为了防止系统滥用,我们可以限制用户一次预订10个以上的座位。
5、我们可以假设,在人们期待已久的热门电影上映时,流量会急剧上升,座位很快就会挤满。该系统应具有可扩展性和高可用性,以跟上流量激增的步伐。
4、容量估算
1、流量估计
假设我们的服务每月有30亿次页面浏览量,销量为10%一个月一百万张票。
2、存储量估计
假设我们有500个城市,平均每个城市有10家电影院。如果每家电影院有2000个座位,平均每天有两场演出。
假设每个座位预订需要50字节(ID、NumberOfSeats、ShowID、MovieID、SeatNumber、SeatStatus、Timestamp等)存储在数据库中。我们还需要存储关于电影和电影院的信息;假设需要50字节。所以,要存储所有关于所有城市的所有电影院一天的放映:
500 cities * 10 cinemas * 2000 seats * 2 shows * (50+50) bytes = 2GB / day
要存储五年的数据,我们需要大约3.6TB。
5、系统API
我们可以使用SOAP或RESTAPI来公开服务的功能。以下可能是用于搜索电影节目和预订座位的API的定义。
1.SearchMovies
api参数讲解:
api_dev_key (string): 注册账户的API开发人员密钥。这将用于根据分配的配额限制用户。keyword (string): 要搜索的关键字city (string): 筛选电影的城市lat_long (string): 用来过滤的经纬度信息radius (number): 要搜索事件的区域半径 start_datetime (string): 用来筛选的电影的开始日期end_datetime (string): 用来筛选的电影的结束日期postal_code (string): 用来筛选的邮政编码includeSpellcheck (Enum: “yes” or “no”): 拼写检查建议results_per_page (number): 每页返回的结果数。 Maximum is 30. sorting_order (string): 对搜索结果进行排序.
Some allowable values : ‘name,asc’, ‘name,desc’, ‘date,asc’, ‘date,desc’, ‘distance,asc’, ‘name,date,asc’,
‘name,date,desc’, ‘date,name,asc’, ‘date,name,desc’.
下面是电影及其节目的示例列表,后台的Returns: (JSON)
[{"MovieID": 1,"ShowID": 1,"Title": "Cars 2","Description": "About cars","Duration": 120,"Genre": "Animation","Language": "English","ReleaseDate": "8th Oct. 2014","Country": USA,"StartTime": "14:00","EndTime": "16:00","Seats":[{"Type": "Regular""Price": 14.99"Status: "Almost Full"},{"Type": "Premium""Price": 24.99"Status: "Available"}]},{"MovieID": 1,"ShowID": 2,"Title": "Cars 2","Description": "About cars","Duration": 120,"Genre": "Animation","Language": "English","ReleaseDate": "8th Oct. 2014","Country": USA,"StartTime": "16:30","EndTime": "18:30","Seats":[{"Type": "Regular""Price": 14.99"Status: "Full"},{ "Type": "Premium""Price": 24.99"Status: "Almost Full"}]},]
2.ReserveSeats
api参数讲解:
api_dev_key (string): same as above session_id (string): 用于跟踪此预订的用户会话ID。一旦保留时间到期,将使用此删除用户在服务器上的保留
身份证件movie_id (string): 要预定的电影show_id (string): 要预定的场次eats_to_reserve (number): 包含要保留的座位ID的数组
下面是api返回值:Returns: (JSON)
Returns the status of the reservation, which would be one of the following:
1) “Reservation Successful”
2) “Reservation Failed - Show Full,”
3) “Reservation Failed - Retry, as other users are holding reserved seats”.
6、数据库设计
我们要存储的数据:
1、每个城市可以有多家电影院。
2、每家电影院将有多个大厅。
3、每部电影将有多场演出,每场演出将有多个预订。
4、一个用户可以进行多次预订。
下面是UML表单图设计
7、高级设计
在上层,我们的web服务器将管理用户会话,应用服务器将处理所有票证管理,将数据存储在数据库中,并与缓存服务器一起处理预订。
8、细节模块设计
首先,让我们尝试构建我们的服务,假设它是从单个服务器提供的。
以下是典型的售票流程:
1.用户搜索电影。
2.用户选择一部电影。
3.向用户显示电影的可用放映。
4.用户选择一个场次。
5.用户选择要预订的座位数目
6.如果有所需数量的座位,则会向用户显示要选择的剧院地图座位。如果没有,用户将进入下面的“步骤8”。
7.一旦用户选择了座位,系统将尝试预订这些选定的座位。
8.如果无法预订座位,我们有以下选择:
- 本场次已满座,向用户显示错误消息
- 用户想要预订的座位不再可用,但还有其他座位可用,因此用户将返回影院地图以选择不同的座位。
- 没有可用的座位可预订,但是进入选定状态的座位有些还没有支付,也就是预订池中有一些其他用户持有的座位尚未预订。用户将进入等待页面,在那里他们可以等待,直到所需的座位从预订池中释放出来。这种等待可能会导致以下选项:
- 如果所需的座位数量可用,用户将被带到剧院地图页面,在那里他们可以选择座位。
- 等待期间,如果所有座位都已预订成功,或者预订池中的座位数少于用户预定的座位数,则会向用户显示错误消息。
- 用户取消等待并返回到电影搜索页面。
- 在用户会话过期并返回到电影搜索页面后,用户最多可以等待一个小时。
9.如果成功预订座位,用户有五分钟的时间支付预订费用。付款后,预订被标记为完成。如果用户无法在五分钟内付款,则其所有预留座位将被释放,以供其他用户使用。
整个流程如下:
9、流程
服务器如何跟踪所有尚未预订的active预订?服务器如何跟踪所有等待的客户?
我们需要两个守护程序服务,一个用于跟踪所有active预订并从系统中删除任何过期的预定;我们称之为ActiveReservationService。
另一项服务将跟踪所有等待的用户请求,一旦所需的座位数可用,它将通知(等待时间最长的)用户选择座位;我们叫它WaitingUserService吧。
ActiveReservationService
除了将所有数据保存在数据库中之外,我们还可以将本场次的所有预定保留在内存中,保存在类似于链接HashMap或TreeMap的数据结构中。
我们需要一个链接的HashMap类型的数据结构,允许我们跳转到任何预订,以便在预订完成后将其删除。
此外,由于我们将有与每个保留相关联的过期时间,哈希映射的头部将始终指向最旧的保留记录,以便在达到超时时可以过期保留。
为了存储每个场次的每个预订,我们可以有一个哈希表,其中“key”是“ShowID”,而“value”是包含“BookingID”和创建“Timestamp”的链接哈希表。
在数据库中,我们将预订存储在“Booking”表中,过期时间将在时间戳列中。
“Status”(状态)字段的值为“Reserved(1)”,一旦预订完成,系统会将“Status”(状态)更新为“Booked(2)”,并从相关节目的链接哈希表中删除预订记录。当预订过期时,除了从内存中删除外,我们还可以将其从预订表中删除或将其标记为“过期(3)”。
ActiveReservationsService还将与外部金融服务一起处理用户付款。无论何时预订完成,或预订过期,WaitingUsersService都会收到信号,以便为任何等待的客户提供服务。
WaitingUsersService
就像ActiveReservationsService一样,我们可以将一个场次的所有等待用户保存在链接哈希映射或树映射的内存中。我们需要一个类似于链接HashMap的数据结构,这样当用户取消请求时,我们就可以跳转到任何用户,将其从HashMap中删除。此外,由于我们以先到先得的方式提供服务,链接HashMap的头部将始终指向等待时间最长的用户,因此每当有座位可用时,我们都可以以公平的方式为用户提供服务。
我们将有一个哈希表来存储每个节目的所有等待用户。“key”将是“ShowID”,“value”将是一个包含“userid”及其等待开始时间的链接HashMap。客户端可以使用长轮询来更新自己的预订状态。每当座位可用时,服务器都可以使用此请求通知用户。
在服务器上,ActiveReservationsService跟踪活动预订的到期时间(基于预订时间)。由于客户端将显示一个计时器(用于过期时间),该计时器可能与服务器有点不同步,因此我们可以在服务器上添加一个5秒的缓冲区,以防止出现中断的体验,这样客户端就不会在服务器之后超时,从而阻止成功购买。
10、并发性
如何处理并发性,以便没有两个用户能够预订同一个座位。我们可以在SQL数据库中使用事务来避免任何冲突。例如,如果我们使用的是SQL server,我们可以利用事务隔离级别锁定行,然后再更新它们。以下是示例代码:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;-- Suppose we intend to reserve three seats (IDs: 54, 55, 56) for ShowID=99
Select * From Show_Seat where ShowID=99 and ShowSeatID in (54, 55, 56) and
Status = 0 -- free-- if the number of rows returned by the above statement is three, we can-- update to return success otherwise return failure to the user.update Show_Seat ...update Booking ...COMMIT TRANSACTION;
“Serializable”是最高的隔离级别,可确保不受脏、不可重复和幻影读取的影响。这里要注意一件事;在一个事务中,如果我们读取行,我们会得到一个写锁,这样其他人就无法更新它们。
一旦上述数据库事务成功,我们就可以在ActiveReservationService中开始跟踪保留。
11、容错性
当ActiveReservationsService或WaitingUserService崩溃时会发生什么情况?
每当ActiveReservationsService崩溃时,我们都可以从“Booking”表中读取重新所有active预定。请记住,在预订之前,我们将“状态”列保留为“预定(1)”。
另一种选择是进行主从配置,以便在主服务器崩溃时,从服务器可以接管。
我们没有将等待的用户存储在数据库中,因此,当WaitingUsersService崩溃时,我们没有任何方法来恢复该数据,除非我们有主从设置。类似地,我们将对数据库进行主从设置,使其具有容错性。
12、数据分区
数据库分区:如果我们按“MovieID”进行分区,那么一部电影的所有放映都将在一台服务器上进行。对于非常热门的电影,这可能会在该服务器上造成大量负载。更好的方法是基于ShowID进行分区;这样,负载就可以分布在不同的服务器上。
ActiveReservationService和WaitingUserService分区:
我们的web服务器将管理所有active用户的会话,并处理与用户的所有通信。我们可以使用一致性哈希根据“ShowID”为ActiveReservationService和WaitingUserService分配应用程序服务器。
这样,特定场次的所有预订和等待用户都将由一组特定的服务器处理。
让我们假设为了负载平衡,我们的一致哈希为任何场次分配三台服务器,因此每当预订过期时,持有该预订的服务器将执行以下操作:
1、更新数据库以删除预订(或将其标记为过期),并更新“Show seats”表中的座位状态。
2、从链接的HashMap中删除保留。
3、通知用户其预约已过期。
4、向所有等待该节目的等待用户的WaitingUserService服务器广播一条消息,以找出等待时间最长的用户。一致性哈希将告诉哪些服务器容纳这些用户。如果所需座位可用,则向等待时间最长的用户所在的WaitingUserService服务器发送消息,以处理其请求。
无论何时预订成功,都会发生以下情况:
1。持有该预订的服务器将向持有该节目等待用户的所有服务器发送一条消息,以便这些服务器可以终止需要比可用座位更多座位的所有等待用户。
2、在收到上述消息后,所有等待用户的服务器都会查询数据库,以确定现在有多少免费座位。在这里,数据库缓存将非常有助于只运行一次此查询。
3、让所有想预订比可用座位更多座位的等待用户过期。为此,WaitingUserService必须遍历所有等待用户的链接HashMap。