前言

受到中文社区《电商参考架构第二部分:库存优化方法》启发,想到了去年做过类似的电影票预定系统,如果用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,原子性的修改库存信息。其他的描述信息是否需要再次冗余取决于具体的业务状况了,具体问题具体分析。我本人更倾向于目前的数据结构方案,不做过多的冗余,原因:

  1. 数据订正复杂,多一个冗余,多一份复杂
  2. 其他信息基本都是静态数据,数据量又小,完全可以通过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如何构建一个电影票系统,但距离生长系统还是有一定的距离,仍有很多其他的技术点需要讨论,可以延伸开的还有,下单失败,过期未付款,数据唯一性等问题。

基于空间数据库MongoDB实现全国电影票预定系统相关推荐

  1. 基于JAVA+Spring+MYSQL的电影票预定系统

    前台模块 系统首页 我的订票车 我的订单 我的信息 留言板 后台模块 系统属性 修改密码 用户管理 用户类别 电影票管理 订单管理 留言管理 页面效果

  2. 基于php003飞机票航空售票查询预定系统

    本论文主要阐述一个功能比较强大的航空客运订票系统的前台后台操作过程及一些关键技术.该网站据采取互联网web语言最流行的技术PHP,数据库库采用php的黄金搭档mysql,web服务器采用世界上最优良, ...

  3. 基于php003飞机票航空售票查询预定系统-计算机毕业设计

    项目介绍 本论文主要阐述一个功能比较强大的航空客运订票系统的前台后台操作过程及一些关键技术.该网站据采取互联网web语言最流行的技术PHP,数据库库采用php的黄金搭档mysql,web服务器采用世界 ...

  4. 基于java的springboot餐厅座位预定系统毕业设计springboot开题报告

    (1)注册登录:游客进行账号注册.登录平台 (2)查看网站介绍:网站介绍.关于我们.联系我们.加入我们.法律声明 (3)菜品介绍:查看餐厅的所有菜品,可以根据分类查询 (4)菜品详情:查看菜品的详细介 ...

  5. 毕业设计-基于微信小程序的电影票购票系统

    目录 前言 课题背景与简介 实现设计思路 一.微信小程序开发技术 二.总体功能 三.电影票购票微信小程序实现 四.电影票购票微信小程序的测试 实现效果样例 更多帮助 前言

  6. 毕业设计-基于微信小程序的电影票网购系统

    目录 前言 课题背景与简介 实现设计思路 一.系统设计 二.功能实现 三.部署与上线 四.总结 实现效果样例 更多帮助 前言

  7. 开题报告:基于java餐厅网站和座位预定系统 毕业设计论文开题报告模板

    开发操作系统:windows10 + 4G内存 + 500G 开发环境:JDK1.8 + Tomcat8 开发语言:Java 开发框架:springboot 模板引擎:Thymeleaf 开发工具:I ...

  8. 基于JAVA社区老人健康服务跟踪系统计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA社区老人健康服务跟踪系统计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA社区老人健康服务跟踪系统计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B ...

  9. 基于Java毕业设计养老院信息管理源码+系统+mysql+lw文档+部署软件

    基于Java毕业设计养老院信息管理源码+系统+mysql+lw文档+部署软件 基于Java毕业设计养老院信息管理源码+系统+mysql+lw文档+部署软件 本源码技术栈: 项目架构:B/S架构 开发语 ...

最新文章

  1. android中界面滑动延伸,android中ViewPager详解--视图滑动、界面卡等效果 (三)
  2. SpringBoot入门_搭建及配置环境(eclipse || Spring ToolS)
  3. idea中lombok的使用
  4. js 正则表达式实现文本验证
  5. mac php errorlog,Mac下使用php的error_log()函数发送邮件
  6. mysql binlog生成异常_mysql binlog故障演练
  7. 前端诸神大战,Vue、React 依旧笑傲江湖
  8. 多分辨率下的彩色图像分割方法
  9. Biztalk 调用带Soap Head WebService的方法
  10. ES6学习笔记---二进制数组(应用)
  11. 一位Android大牛的BAT面试心得与经验总结
  12. 音轨分离软件 Spleeter 使用教程及踩过的坑
  13. 图像复原matlab论文,基于matlab图像复原论文
  14. Elasticsearch blocked by: [SERVICE_UNAVAILABLE/1/state not recovered / initialized];
  15. jQuery,实现想弹出什么,弹出什么(弹出提示、弹出一个新的页面等等)
  16. (十四)覆盖率类型、覆盖率组
  17. ISBN码识别-DA数据结构二级项目
  18. 南明区建成呼叫座席11710席
  19. 推荐google浏览器插件(为专注工作使用)
  20. 英语面试问题及答案(转)

热门文章

  1. 安装HCL遇到的问题
  2. 彻底弄懂浏览器端的Event-Loop
  3. Linux中如何添加/删除FTP用户并设置权限
  4. Android 系统启动(一)---fork机制
  5. Docker - 安装
  6. Linux系统文件类型
  7. ajax——实现三级联动下拉列表
  8. Flash Builder 4.7 类模板编辑设置
  9. Unicode和UTF-8的关系
  10. VMware Workstation 9下基于Ubuntu 12.10服务器版本的Hadoop集群的配置