基於MongoDB的全國電影票預定系統
前言
受到中文社群《電商參考架構第二部分:庫存最佳化方法》啟發,想到了去年做過類似的電影票預定系統,如果用MongoDB去做儲存支撐,那應該是怎樣架構的呢?本文的目的是為了更好的學習掌握MongoDB,所以某些設計上更偏向於功能的展示,在實際使用上要因地制宜的改變,合適才是最好的。
需求
電影票預定系統與電商系統非常類似,都可以抽象理解為商品的售賣。進一步的講電影票系統是電商系統的一個庫存特例場景:
- 每個場次,每個座位,都只有一個庫存
- 每個訂單所預定的座位有鎖定狀態,在支付前對應的作為不能被再次購買
- 訂單涉及到的座位要不全成功,要不全失敗
- “全國”級的,資料容量不是太大問題,但效能上要支援水平擴充套件
PS:實際上的理論TPS並不高,目前全國5000家影院,假設平均8個影廳,每個廳200個位置,每個影廳6個場次,早中晚各3個高峰,每個高峰1個小時。計算得出TPS大概是:5000 * 8 * 6 * 200/ 3 / 3600 = 4400 TPS;但是設計上我們還是要保證效能的可水平擴充套件,否則怎麼體現MongoDB的特色呢?^-^
描述資訊文件結構
影院描述資訊
儲存最基本的影院資訊,包括地理資訊,名稱,_id為MongoDB由MongoDB自動分配
CinemaManager.cinema_detail
{
_id: <ObjectID>,
name: "<cinema name>",
city: "<city name>" location: [<longitude>, <latitude>],
comments: "<detail message>" }
例如:
rs0:PRIMARY> db.cinema_detail.insert({ "name" : "大時代電影院", "city" : "杭州", "location" : [ 120.13, 30.16 ], "comments" : "IMAX 4K,有停車位" });
因為影院資訊的查詢一般都是按照城市和名稱,或者地理座標檢索,所以這裡建立兩個索引
Index1:城市+名稱的複合索引,因為查詢電影院時一般都會指定城市名
rs0:PRIMARY> db.cinema_detail.ensureIndex({city:1, name:1})
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 2, "numIndexesAfter" : 2, "ok" : 1 }
注意,這裡使用的是複合索引,所以針對 city + name的查詢,或者city的查詢是有效的,只查詢name欄位是無法透過索引最佳化的。
Index2:地理座標索引,用來應付"最近的電影院"類查詢
rs0:PRIMARY> db.cinema_detail.ensureIndex({location: "2d"})
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 3, "numIndexesAfter" : 4, "ok" : 1 }
例如,查詢在杭州最近的某個電影院
rs0:PRIMARY> db.cinema_detail.find({city:"杭州", location: { $near: [1.0, 2.0] }}).pretty()
{ "_id" : ObjectId("559a3ef8c6058dae1ac49ce8"), "name" : "大時代電影院", "city" : "杭州", "location" : [ 120.13, 30.16 ], "comments" : "IMAX 4K,有停車位" }
影廳描述資訊
theater_detail.cinema_id與cinema_detail._id集合形成references關係,透過cinema_detail._id可以快速找到所屬影廳的資訊。另一個關鍵欄位theater_detail.seat用來描述座位資訊,每排所有的座位是一個陣列,不同排可以有不同數量的座位。
CinemaManager.theater_detail
{
_id: <ObjectID>,
cinema_id: <ObjectID(cinema_detail._id)>,
name: <theater name>,
seat:
{
row1: [<seat valid>],
row2: [<seat valid>],
row3: [<seat valid>],
<seat row>: [<seat valid>] }
comments: "<detail message>" }
rs0:PRIMARY> db.theater_detail.insert({
cinema_id:ObjectId("559a3ef8c6058dae1ac49ce8"),
name:"IMAX廳",
seat:
{
row1: [1, 1, 1, 1],
row2: [1, 1, 1],
row3: [1, 1, 1, 1],
row4: [1, 1, 1, 1, 1],
},
comments: "可容納哦xxx人,弧形熒幕" })
rs0:PRIMARY> db.theater_detail.insert({
cinema_id:ObjectId("559a3ef8c6058dae1ac49ce8"),
name:"中國巨幕廳",
seat:
{
row1: [1, 1, 1, 1],
row2: [1, 1, 1],
row3: [1, 1, 1, 1]
},
comments: "可容納哦xxx人,弧形熒幕" })
建立索引
rs0:PRIMARY> db.theater_detail.ensureIndex({cinema_id:1})
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
影片描述資訊
影片說明
{
_id: <ObjectID>,
name: "<movie name>",
director: "director name" actor: [<actor name>] comments: "<detail message>" }
rs0:PRIMARY> db.movie_detail.insert({ name: "一路向西", director: "胡耀輝", actor:["張建聲", "王宗堯", "胡耀輝", "何佩瑜", "張暖雅", "郭穎兒"], comments: "該影片描寫的是當代香港社會中普通年輕人對“愛”與“性”的追求而逐漸改變的心路歷程的故事" })
索引
rs0:PRIMARY> db.movie_detail.ensureIndex({name:1})
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
影片放映文件結構
放映資訊包含放映時間段,放映影廳,票價。雖然Document結構可以做複雜的巢狀,但原則上期望Document儘量小,利用資料Shard,效能最佳化。所以在movie_schedule的設計上每個影片的每場放映獨立一個Document表達。
{
_id: <ObjectID>,
cinema_id: <ObjectID(cinema_detail._id)>
movie_id: <ObjectID(movie_detail._id)>,
theater_id: <ObjectID(theater_detail._id)>,
start_time: <ISODate>,
end_time: <ISODate>,
comments: "<detail message>" }
movie_schedule的References關係較多,需要與電影院,影廳,電影三者分別建立關係。
db.movie_schedule.insert({ cinema_id:ObjectId("559a3ef8c6058dae1ac49ce8"),
movie_id:ObjectId("559b68f372b34f216246cb1d"),
theater_id:ObjectId("559b625072b34f216246cb1b"),
start_time: ISODate("2015-07-07T10:00:00.00Z"),
end_time: ISODate("2015-07-07T12:00:00.000Z"),
comments: "首映" )} db.movie_schedule.insert({ cinema_id:ObjectId("559a3ef8c6058dae1ac49ce8"),
movie_id:ObjectId("559b68f372b34f216246cb1d"),
theater_id:ObjectId("559b625072b34f216246cb1b"),
start_time: ISODate("2015-07-07T12:30:00.00Z"),
end_time: ISODate("2015-07-07T14:30:00.000Z"),
comments: "" )}
還是建立一個複合索引,最佳化查詢某一電影院的某部影片(的某一影廳)上映資訊
rs0:PRIMARY> db.movie_schedule.ensureIndex({cinema_id:1, movie_id:1, theater_id:1})
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
PS:也可以建立相應的索引,用來最佳化某一時間段內的影片資訊查詢,讀者自行思考
交易系統
至此,基本的資訊文件集合均已建立完成,一般的查詢需求都可以滿足了。接下來是重點:庫存售賣系統。抽象的來看,售賣系統就是對上訴所有集合的一個整合,外加一套庫存欄位。我們認為一場放映就是一個主商品,每個座位可以認為是這個商品的SKU,每個SKU都是1份。
透過Reference關係結合movie_schedule與theater_detail,注意這裡引用了
{
_id: <ObjectID>,
movie_schedule_id: <ObjectID(movie_schedule._id)>
theater_id: <ObjectID(theater_detail._id)>,
seat:
{
row1: [2, 2, 2, 2],
row2: [2, 2, 2],
row3: [2, 2, 2, 2],
row4: [2, 2, 2, 2, 2],
}
}
注意,這裡不僅僅是Reference的引用關係,還複製了theater_detail.seat欄位,每個seat都有一個庫存數字,因為在MongoDB中一個Document的操作是可以保證原子的,不需要對Collection加任何鎖。數字2並不是表示可以賣2次:
- 數字2表示,可銷售
- 數字1表示,已鎖定
- 數字0表示,已售完
交易邏輯上可透過FindAndModify + $inc,原子性的修改庫存資訊。其他的描述資訊是否需要再次冗餘取決於具體的業務狀況了,具體問題具體分析。我本人更傾向於目前的資料結構方案,不做過多的冗餘,原因:
- 資料訂正複雜,多一個冗餘,多一份複雜
- 其他資訊基本都是靜態資料,資料量又小,完全可以透過Cache技術解決讀取問題
先插入一個我們的商品
db.movie_item.insert({
movie_schedule_id : ObjectId("559b6ee472b34f216246cb1e"),
theater_id : ObjectId("559b625072b34f216246cb1b"),
seat :
{
row1: [2, 2, 2, 2],
row2: [2, 2, 2],
row3: [2, 2, 2, 2],
row4: [2, 2, 2, 2, 2],
}
})
索引
rs0:PRIMARY> db.movie_item.ensureIndex({movie_schedule_id:1})
{ "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
鎖定座位的動作,鎖定第4排的3號位置(從1開始計數)和鎖定第4排的2號位置:
db.movie_item.findAndModify({
query: { "_id":ObjectId("559b790f72b34f216246cb22"), "seat.row4.2":2 },
update: { $inc: {"seat.row4.2":-1}},
upsert: false
})
db.movie_item.findAndModify({
query: { "_id":ObjectId("559b790f72b34f216246cb22"), "seat.row4.1":2 },
update: { $inc: {"seat.row4.1":-1}},
upsert: false
})
分別鎖定了第4排3號(row4[2]),第4排2號(row4[1]),
注意,這裡是分兩次鎖定的,鎖定操作並不需要原子完成,否則會造成使用者鎖定失敗機率的上升。
rs0:PRIMARY> db.movie_item.find({_id:ObjectId("559b790f72b34f216246cb22")}).pretty()
{ "_id" : ObjectId("559b790f72b34f216246cb22"), "movie_schedule_id" : ObjectId("559b6ee472b34f216246cb1e"), "theater_id" : ObjectId("559b625072b34f216246cb1b"), "seat" : { "row1" : [ 2, 2, 2, 2 ], "row2" : [ 2, 2, 2 ], "row3" : [ 2, 2, 2, 2 ], "row4" : [ 2, 1, 1, 2, 2 ]
}
}
OK,交易成功以此類推,同時修改兩個庫存到0,這裡利用了findAndModify的原子特性
db.movie_item.findAndModify({
query: {
_id:ObjectId("559b790f72b34f216246cb22"), $and:[ {"seat.row4.2":1}, {"seat.row4.1":1}]
},
update: { $inc: {"seat.row4.2":-1, "seat.row4.1":-1}
},
upsert: false
})
再查下集合看看:
rs0:PRIMARY> db.movie_item.find({_id:ObjectId("559b790f72b34f216246cb22")}).pretty()
{ "_id" : ObjectId("559b790f72b34f216246cb22"), "movie_schedule_id" : ObjectId("559b6ee472b34f216246cb1e"), "theater_id" : ObjectId("559b625072b34f216246cb1b"), "seat" : { "row1" : [ 2, 2, 2, 2 ], "row2" : [ 2, 2, 2 ], "row3" : [ 2, 2, 2, 2 ], "row4" : [ 2, 0, 0, 2, 2 ]
}
}
總結
一套全國級的電影票系統會比這複雜的多,本文的目的還是以教程為主,主要是說明MongoDB如何構建一個電影票系統,但距離生長系統還是有一定的距離,仍有很多其他的技術點需要討論,可以延伸開的還有,下單失敗,過期未付款,資料唯一性等問題。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/15498/viewspace-1982585/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 基於RubyOnRails2.0的全國影訊查詢系統AI
- 電影票務管理系統改進
- 一個基於Vue.js+Mongodb+Node.js的部落格內容管理系統Vue.jsMongoDBNode.js
- QuillCMS – 基於Nodejs、Nuxtjs、MongoDB構建內容管理系統UINodeJSUXMongoDB
- 基於前端技術實現的全面預算編制系統前端
- 基於JavaScript的機器學習系統JavaScript機器學習
- 基於 Golang 開發的分散式定時任務管理系統Golang分散式
- 基於Web的系統測試Web
- 電資辦:2024年10月全國電影票房
- 基於 MongoDB 的 python 日誌功能MongoDBPython
- 基於MongoDB.Driver的擴充套件MongoDB套件
- 基於 Docker 的 MongoDB 主從叢集DockerMongoDB
- 基於MongoDB的python日誌功能MongoDBPython
- 基於雲的MES系統軟體
- 基於thincmf的內容管理系統
- 基於SpringBoot 的CMS系統Spring Boot
- 基於SSM的職員考勤系統SSM
- LKCOS:基於程式移植的COS系統
- 基於WIFI無線組網的水雨情遠端監測預警系統WiFi
- MongoDB 為系統新增MongoDB服務MongoDB
- mongodb 的許可權系統MongoDB
- 基於統計的預警:同環比預警實現深度剖析
- mongodb基礎運維能力定義MongoDB運維
- 【2】基於zookeeper,quartz,rocketMQ實現叢集化定時系統quartzMQ
- 基於感知機的人名-性別預測系統 —— Python實現Python
- Python 設定系統預設編碼Python
- 資料到底如何搞定電影票房預測?
- 機器學習 | 基於機器學習的推薦系統客戶購買可能性預測分析機器學習
- 基於Linux系統的影片點播系統的實現(轉)Linux
- Oracle預定義的21個系統異常型別Oracle型別
- 基於GitHub Issues的評論系統--gitmentGithub
- 基於TP3.2.3的問答系統!
- 基於thinkphp的會員推廣系統PHP
- Redox OS:基於Rust的作業系統Rust作業系統
- 基於websocket的簡單廣播系統Web
- 基於 Prometheus 的監控系統實踐Prometheus
- 基於RBAC的許可權管理系統
- 基於Linux系統的PXE搭建方法Linux