100行代码搞定12306库存问题:谈12306系统设计思路 您所在的位置:网站首页 12306订票系统中数据库的作用 100行代码搞定12306库存问题:谈12306系统设计思路

100行代码搞定12306库存问题:谈12306系统设计思路

2024-07-15 22:16| 来源: 网络整理| 查看: 265

2016年春运刚过,12306购票系统又一次被推到风头浪尖,这次春运12306系统表现还是不错的,除了神一样的验证码以外,基本上没有出现过大面积长时间的故障。12306系统复杂程度、系统如何设计,作为技术人员值得深入思考一下。

##12306数据分析

预售期:60天车站:3000+车次:7000/天日期车次:7000 X 60=42万站站:21万/天日期站站:21万x 60=1260万商品(站站、车次、日期、座位类型):1.5亿左右

商品间存在关联,当售出一张票后,两站间的库存都要减少.

2015年春节访问量峰值当天:共有297亿pv次访问,536.9万张票访问量平均值;51万 qps访问量峰值(平均值2倍计算);51万 x 2 = 102万 qps

虽然访问量很高,但主要集中在搜索查询上,搜索的压力体现在库存管理上.

##架构设计思路

这里写图片描述

如上图:系统架构可分为几大块,数据分析和维护一块,数据服务一块,交易系统、用户中心及其它平台一块;交易系统和用户中心这里不赘述;原始数据来源这块,应该包括数据分析和数据维护部分,数据分析分为离线数据分析和实时数据分析;数据分析在火车票这个方向上,是非常值得深究的,包括火车调度、火车调图、增减车次、运力评估及应急预案分析等等;有兴趣的同学可以深入研究一下;数据服务这块是本文重点(红圈部分),由于火车票售票系统和传统的电商平台在数据结构和业务上的差别,在设计上要下一些功夫;在数据上我们可以把数据分为静态数据和动态数据,二者区别是更新频率,而我们最关注库存问题,就是要讨论的动态数据;库存管理外侧我们需要软负载,按照日期车次分片,同一日期车次使用同一台服务器,可免去分布式锁问题;如果是联程或往返仍需分布式锁(目前联程和往返12306还没有真正支持,方案上大家可以思考一下);存储方面需要有分布式cache存储和分布式持久化存储两层,确保数据高效且不丢失。

##基础数据设计思路

这里写图片描述

如上图:原始数据可以包括车次日历、车次信息日历、车次信息及日期车次库存等,核心数据保存在车次信息部分,这有个技巧就是同一车次不同日期车次信息不同的存储问题,即0–20160315–20160418;车次车厢数据这块,考虑到存储空间问题,可以简化为根据车厢编号和车厢类型计算出座位编号,比如1车1排D座顺序号是4;基础数据维护系统补充原始数据,并由原始数据生成查询服务数据,静态数据提前生成,动态数据实时计算,站站车次库存与日期车次库存之间可以有一定缓存时间以减少计算量;日期车次库存是动态数据是经常变化的,要充分考虑空间和时间复杂度,后面有具体设计思路;其它数据可参考上面设计,如果想节约空间,设计上可以多用数字或Byte数组替代字符串等方式。

##库存管理设计思路

购票占座其实就是找到符合条件的座位,假设有一列经过A到F ,6个车站的火车,车上共有10个座位,大体需要有以下几种场景需要满足:

场景1:当卖掉了一张A到F的车票,则AF之间所有站站库存要减1,若果经过100个站,则需要C(100,2)=4950次操作;场景2:列车中途需要增加减少车厢,或者有一节车厢不对外出售;场景3:为了降低空座率,需要特定站站最多卖票数量,特定出发站的最多卖票数量;场景4:由于座位的唯一性,需要标示特定座位某站站已被卖出,并且找到满足特定条件的座位,如果1列火车有2000个座位,这个运算就是一个挑战;场景5:某特定车厢或座位只能卖给特定始发站的人群,即某特定车厢是给某个车站预留的;

###数据结构和算法设计 由于车站A,B,C,D,E,F顺序固定,可以用用数字表示为(1,2,3,4,5,6),相邻车站站 (AB,BC,CD,DE,EF)顺序号可表示为(0,1,2,3,4);

这里写图片描述

购票流程:

1、输入车次、站站(AE)、坐席类型信息;

2、根据站站找到A和E的位置信息,即1和5;

3、根据车次站站库存判断索引,判断该车次站站是否有余票,若果有继续;

4、根据购买限制索引,判断站站余票数量限制,如果可以购买继续;

5、遍历座位站站库存判断索引数组,找到该站站有余票的座位索引编号;

6、更新座位站站库存判断索引值,相关位置置1,说明该站站座位票已卖出;

7、相邻站库存数组,AE间相邻站库存减1;

8、更新车次站站库存判断索引,如果AE间相邻站库存为0,则对应标示位置1,表示该邻近站站已无库存;

9、更新购买限制索引,如果存在限制索引则减1;

10、返回座位索引编号;

取消票流程:

1、输入车次、站站(AE)、座位编号;

2、根据座位编号找到座位站站库存判断索引,相关索引位置置0,说明该站站座位票未卖出;

3、更新相邻站库存,AE间相邻站库存加1;

4、更新车次站站库存判断索引,AE间相邻站站位置置0,说明AE间所有站站还有库存;

5、更新购买限制索引,如果存在限制索引则加1;

余票查询流程

1、输入车次、站站(AE)、坐席类型信息;

2、根据车次站站库存判断索引,判断该车次站站是否有余票,若有记录限制值并,若无返回0;

3、遍历AE间相邻站(AB,BC,CD,DE)间余票,记录相邻站站余票最小值min(AB,BC,CD,DE);

4、返回min(限制值,相邻站站余票最小值);

以100车站2000座位车次为例,存储空间应不超过3K,其实大多数车次不超过10个车站1000个座位,基本上1.5K能搞定,理想情况下42W的日期车次库存数据不超过1G的存储空间。当然这个不包括存储数据本身需要的数据结构维护的空间,这也与实现方式和存储方案有关;

整个实时计算过程中采用位运算和数组操作,模拟一个100车站2000坐席的车次,在出票情况最多的场景下,即每次都购买相邻站并且每次都需要遍历,不存在被可购买索引挡出的情况下,订票19.8万张票用时17秒,即每个日期车次每秒钟可订票11000张。理想情况下最多每秒钟可出47万(日期车次数量)x1.1万张票,即47亿张,如果每个日期车次最多只售卖1万张票,则所有票可在1秒内出完。

###算法代码

import java.util.BitSet; import java.util.HashMap; import java.util.Map; /** * Created by jerrysun on 16/2/23. */ public class TrainStock { public Map stationMap = new HashMap(); public BitSet canBookIndex; public Map s2sMaxCountMap = new HashMap(); public Integer[] s2sTickets; public BitSet[] seatCanBookIndex; @Override public String toString() { String temp = "stationMap:" + stationMap.toString() + "\r\n"; temp = temp + "s2sTickets:"; for (int i = 0; i temp = temp + "," + seatCanBookIndex[i].toString(); } temp = temp + "\r\n"; return temp; } public Integer bookTicket(Integer fromStation, Integer toStation) { Integer fIdx = stationMap.get(fromStation); Integer tIdx = stationMap.get(toStation); BitSet bookIndex = this.canBookIndex.get(fIdx, tIdx); if (bookIndex.isEmpty()) { Integer maxCount = s2sMaxCountMap.get(fromStation * 10000 + toStation); if (maxCount != null && maxCount > 0 || maxCount == null) { for (int i = 0; i for (int j = fIdx; j return -100; } } public Integer cancleTicket(Integer fromStation, Integer toStation, int seatIndex) { Integer fIdx = stationMap.get(fromStation); Integer tIdx = stationMap.get(toStation); for (int j = fIdx; j Integer fIdx = this.stationMap.get(fromStation); Integer tIdx = this.stationMap.get(toStation); int temp1 = 10000; for (int j = fIdx; j @Test public void testData() throws Exception { Integer stationCount = 6; Integer seatCount = 10; TrainStock trainStock = buildTrainStock(stationCount, seatCount); System.out.println(trainStock.toString()); //已出票索引,这么设计存储有性能问题,其实可以优化为List,数据按位存储,实际应用中应该是和订单相关信息 Map bookedSeatMap = new HashMap(); //订票测试,并计入已出票 long start = System.currentTimeMillis(); for (int i = 1; i TrainSeatMsg trainSeatMsg = new TrainSeatMsg(); trainSeatMsg.setFromStation(i); trainSeatMsg.setToStation(i + 1); trainSeatMsg.setSeatIndex(num); bookedSeatMap.put(i + "_" + (i + 1) + "_" + num, trainSeatMsg); } System.out.println((System.currentTimeMillis() - start) + "ms"); System.out.println("Book s2s=" + i + "_" + (i + 1) + ":" + num); System.out.println(trainStock.toString()); } //取消订票测试 long start1 = System.currentTimeMillis(); for (TrainSeatMsg value : bookedSeatMap.values()) { int num = trainStock.cancleTicket(value.fromStation, value.toStation, value.seatIndex); System.out.println((System.currentTimeMillis() - start1) + "ms"); System.out.println("Cancle s2s=" + value.fromStation + "_" + value.toStation + "_" + value.seatIndex); System.out.println(trainStock.toString()); } //余票查询 long start2 = System.currentTimeMillis(); for (int i = 1; i Integer stationCount = 100; Integer seatCount = 2000; TrainStock trainStock = buildTrainStock(stationCount, seatCount); System.out.println(trainStock.toString()); //订票测试,模拟stationCount*seatCount次,模拟出票情况最多的情况,即每次都购买相邻站并且每次都需要遍历,不存在被可购买索引挡出的情况 Integer bookcount=0; System.out.println("Book Ticket"); long start = System.currentTimeMillis(); for (int i = 1; i int num = trainStock.bookTicket(i, i + 1); bookcount++; } } long bookTotalTime= System.currentTimeMillis() - start; System.out.println(bookTotalTime + "ms"); System.out.println(bookcount + "times"); System.out.println((float)bookTotalTime/(float)bookcount + "ms"); System.out.println(bookcount/bookTotalTime*1000+"/s"); //System.out.println(trainStock.toString()); System.out.println("=========================="); //余票查询,模拟stationCount*seatCount次,模拟最耗时的始发站到终点站的查询 Integer getcount=0; System.out.println("Get " + "Stock"); long start1 = System.currentTimeMillis(); for (int i = 1; i int num = trainStock.getStock(1,stationCount); getcount++; } } long getTotalTime= System.currentTimeMillis() - start; System.out.println(getTotalTime + "ms"); System.out.println(getcount + "times"); System.out.println((float)getTotalTime/(float)getcount + "ms"); System.out.println(getcount/getTotalTime*1000+"/s"); //System.out.println(trainStock.toString()); } /** * 构建车次库存数据实体 * * @param stationCount 车站数量 * @param seatCount 座位数量 * @return */ private TrainStock buildTrainStock(Integer stationCount, Integer seatCount) { TrainStock trainStock = new TrainStock(); //构建车站map,车站索引从1开始构建 Integer[] stations = new Integer[stationCount]; for (int i = 0; i stationMap.put(stations[i], i); } //构建可预定判断索引 BitSet canBookIndex = new BitSet(stationCount - 1); //构建车次购买限制Map,构建相邻车站最大可购买数量 Map s2sMaxCountMap = new HashMap(); for (int i = 0; i s2stickets[i] = seatCount; } //构建座位可预定判断索引Map BitSet[] seatCanBookIndexs = new BitSet[seatCount]; for (int i = 0; i public Map stationMap = new HashMap(); public BitSet canBookIndex; public Map s2sMaxCountMap = new HashMap(); public Integer[] s2sTickets; public BitSet[] seatCanBookIndex; @Override public String toString() { String temp = "stationMap:" + stationMap.toString() + "\r\n"; temp = temp + "s2sTickets:"; for (int i = 0; i temp = temp + "," + seatCanBookIndex[i].toString(); } temp = temp + "\r\n"; return temp; } public Integer bookTicket(Integer fromStation, Integer toStation) { Integer fIdx = stationMap.get(fromStation); Integer tIdx = stationMap.get(toStation); BitSet bookIndex = this.canBookIndex.get(fIdx, tIdx); if (bookIndex.isEmpty()) { Integer maxCount = s2sMaxCountMap.get(fromStation * 10000 + toStation); if (maxCount != null && maxCount > 0 || maxCount == null) { for (int i = 0; i for (int j = fIdx; j return -100; } } public Integer cancleTicket(Integer fromStation, Integer toStation, int seatIndex) { Integer fIdx = stationMap.get(fromStation); Integer tIdx = stationMap.get(toStation); for (int j = fIdx; j Integer fIdx = this.stationMap.get(fromStation); Integer tIdx = this.stationMap.get(toStation); int temp1 = 10000; for (int j = fIdx; j Integer fromStation; Integer toStation; Integer seatIndex; public Integer getSeatIndex() { return seatIndex; } public void setSeatIndex(Integer seatIndex) { this.seatIndex = seatIndex; } public Integer getFromStation() { return fromStation; } public void setFromStation(Integer fromStation) { this.fromStation = fromStation; } public Integer getToStation() { return toStation; } public void setToStation(Integer toStation) { this.toStation = toStation; } } }


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有