cn12306的设计思路
cn12306的设计思路,不依赖数据库
=======
现在还有不少人在讨论12306的设计,在这里写一个简单的设计思路
1. 网站不是为了解决高峰期票少人多的问题,争论里总讨论这个话题没意义
2. 排队机制不能到处套用,拿网游的常规做法来处理web不是很合适,应该最大限度提升系统的响应速度
3. 最好的方式是开票后10分钟内热门车次票就被订光了,抢到票的高高兴兴去付钱,没抢到的骂骂咧咧想其他途径,早死早超生
4. 响应速度上去后,人们自然不用各种外挂、脚本来刷票了,对减轻网站压力也有好处
5. 交易量没有那么夸张,昨天上海热线微博发的消息:长假期间9月28日至10月1日4天网络预售票达到40万张,约占全部预售车票量的40%,建议旅客尽快换取纸质车票。据此可以估计总的交易规模
6. 12306一些纯配置数据的查询也很慢,比如车次、时刻表、票价等。说明设计确实做得比较渣
7. 静态内容处理好cdn、缓存是比较简单的,配置类数据可以缓存在各个服务节点,或者加一个配置服务,此类数据的查询web server自己就可以处理,都是基本的算法,放到nginx都行,再配合缓存,基本没大的压力
8. 发邮件,发短信之类的多弄几个队列发就是了
9. 查询时有余票,下订单时却没票属于合理情况,没必要回避
整个网站可以用varnish/nginx+redis+appservice
现在看看交易的主要部分:余票查询、订票和订单支付。只关注服务本身,不涉及http cache server、http proxy server等。
假设全国共5000车次(实际4000多,有一部分在12306还查不到),每个车次定员2000人(从网上资料看,很多车型定员不满1000),停靠站不超过64站(6245是62站,实际上超过30站的很少),预售12天车票。
如果这些票全通过网络预订,那么估计每天的交易量<1kw,实际上现在电话,窗口还是主要途径
可以用int来表示车次和日期,实际上int16就够了,用一个int64来表示每个座位的预订状态。每个车次/日期用组合用一个单独的队列来存储,预计占用的空间为定员数*size(int64)再加一些其他数据,可以算出这些数据全放到内存中也占不了多大的地。这样车票的查询和预订就可以全部在内存中计算。
车票预定后有45分钟的支付时间,可以用redis来存储这部分数据,因为存储的时间短,再优化一下存储结构,占不了多少空间。
支付后数据进入传统的RDMS,在查看订单,退票、取票时会用到。这些数据相对而言查询量应该不大,transaction的粒度小、好控制,分区策略比较容易制定。各大RDMS应付这个访问量都不成问题。
用golang写了一个数据结构的原型,没怎么用golang的特有功能比如chan,这样可以方便的移植到其他语言。
原先查询和预订放在一起的,后来为了设计一个简单的master/slave结构,把查询和预订分来了。数据持久化用redis,接口简单,吞吐量也有保证。
这样整个服务被划分成若干组service set,每一组service set由master\slave\redis组成,负责若干车次/日期的查询和预订
查询的流程如下:
1. 根据配置分析起点/终点,确定车次,根据车次/日期确定调用service的地址 (web)
2. 并发调用这些地址 (web)
3. 在对应的队列查询符合条件的票数,最多返回10 (slave)
4. 组合查询结果,输出到浏览器 (web)
查询逻辑基本上是没有锁的,而且全部内存中计算,响应速度很快(2000整数的位运算)。 web端可以缓存查询结果10-30秒, 对业务没什么影响。利用pipleline也可以提升浏览器的响应速度
预订的流程如下:
1. 根据配置分析起点/终点,确定车次,根据车次/日期确定调用service的地址(web)
2. 找到对应的队列,加锁 (master)
3. 找到可用的座位 (master)
4. 调用redis的HMSET修改状态 (master)
5. 修改内存中的数据,释放锁 (master)
6. 订单写入redis, 并返回web端 (master)
7. 同步座位状态 (slave)
还是有很大的优化空间的,比如悲观锁可以改成乐观锁,用cas保证数据同步; 优化座位扫描的起始点以减少扫描次数等。
只要网络部分处理的好,并发链接数和响应速度不是问题, 或者简单一点,直接用go.http提供rest接口性能也过得去
如果master挂了, slave可以在很短的时间内切换为master,同时再另起一个服务作为新的salve
订单支付的流程如下
1. 验证支付结果,写日志,写队列
2. 移除redis中的数据
3. 通过队列写RDMS、发邮件、发短信
预计10个靠谱的开发人员半年就可以做得很好,要是20个开发人员的话,估计火车上求艳遇的应用都可以做出来了
数据结构 https://files.cnblogs.com/buzzlight/main.zip
1 /* 2 cn 12306 数据结构 3 4 5 */ 6 package main 7 8 import ( 9 "fmt" 10 "strconv" 11 "sync" 12 "time" 13 ) 14 15 var ( 16 serverId int //服务编号 17 baseDate int = int(time.Date(2012, 1, 0, 0, 0, 0, 0, time.UTC).Unix() / (60 * 60 * 24)) 18 maxSearchCount int = 10 //查询剩余车票返回的最大值 19 maxLength int = 62 //车次最多经停站数 20 ) 21 22 var ( 23 trains map[int]*Train 24 lock sync.Mutex 25 ) 26 27 // 车次+日期 28 type Train struct { 29 snow *snow //Id生产器 30 lock sync.RWMutex //锁 31 train int //车次 32 date int //日期 33 data []uint64 //座位数据 34 config *TrainConfig //配置 35 dbAddress string //redis 的地址 36 dbKey string //redis hashset的key 37 dbConnect interface{} //redis connection 38 } 39 40 func newTrain(train, date int) *Train { 41 config := getTrainConfig(train) 42 t := &Train{ 43 snow: newSnow(train, serverId), 44 train: train, 45 date: date, 46 data: make([]uint64, config.length), 47 config: config, 48 dbKey: "hashset_" + strconv.Itoa((train<<16)|date), 49 dbAddress: "db address", 50 dbConnect: "db connection"} 51 return t 52 } 53 54 // 车次一些配置数据 55 type TrainConfig struct { 56 length int //座位数 57 _ string //其他 58 } 59 60 func getTrainConfig(train int) *TrainConfig { 61 return &TrainConfig{length: maxLength} 62 } 63 64 // 订单 65 type Order struct { 66 id uint64 //订单编号 67 user int64 //用户编号 68 index int //座位编号 69 train int //车次 70 date int //日期 71 stamp int64 //时间戳 72 } 73 74 func init() { 75 serverId = 123 76 trains = make(map[int]*Train, 1000) 77 78 fmt.Println("init", "server id", serverId, "baseDate", baseDate) 79 } 80 81 // 获取车次数据 82 func getTrain(train, date int) (*Train, bool) { 83 key := (train << 16) | date 84 t, ok := trains[key] 85 return t, ok 86 } 87 88 // 一次只查询一个车次,可以很容易的扩展到查询多个车次 89 func search(train, date int, start, end uint8) (count int) { 90 if t, ok := getTrain(train, date); ok { 91 return t.search(start, end) 92 } 93 return 0 94 } 95 96 // 一次只预订一个座位,可以很容易扩展为预订多个 97 func order(user int64, train, date int, start, end uint8) (order Order, ok bool) { 98 if t, ok := getTrain(train, date); ok { 99 return t.order(user, start, end) 100 } 101 return 102 } 103 104 // 定时增加车次数据 105 func addTrain(train, date int) { 106 key := (train << 16) | date 107 108 lock.Lock() 109 defer lock.Unlock() 110 111 t := newTrain(train, date) 112 trains[key] = t 113 } 114 115 // 查询是否有票 116 func (t *Train) search(start, end uint8) (count int) { 117 data := t.data 118 var mask uint64 = (1<<(end-start) - 1) << (start) 119 for _, d := range data { 120 if d&mask == 0 { 121 count++ 122 } 123 if count > maxSearchCount { 124 break 125 } 126 } 127 return count 128 } 129 130 // 预订 131 func (t *Train) order(user int64, start, end uint8) (order Order, ok bool) { 132 133 var mask uint64 = (1<<(end-start) - 1) << (start) 134 t.lock.Lock() 135 defer t.lock.Unlock() 136 137 data := t.data 138 length := t.config.length 139 for i := 0; i < length; i++ { 140 if data[i]&mask != 0 { 141 continue 142 } 143 144 //持久化,处理队列 145 order = Order{id: t.snow.nextInt(), user: user, index: i, train: t.train, date: t.date, stamp: time.Now().Unix()} 146 data[i] = data[i] | mask 147 return order, true 148 } 149 return 150 } 151 152 // 将日期转化为整数 153 func formatDate(t time.Time) int { 154 return int(t.Unix()/(60*60*24)) - baseDate 155 } 156 157 func main() { 158 159 date := formatDate(time.Now()) 160 for i := 0; i < 1024; i++ { 161 addTrain(i, date) 162 } 163 164 testSearch() 165 166 } 167 168 func testSearch() { 169 total := 1000 * 1000 170 date := formatDate(time.Now()) 171 172 start := time.Now() 173 var count int 174 for i := 0; i < total; i++ { 175 count = search(total/1000, date, 3, 17) 176 } 177 end := time.Now() 178 179 fmt.Println("start", start) 180 fmt.Println("end", end) 181 fmt.Println("duration", end.Sub(start).Nanoseconds()/1000000) 182 fmt.Println("search result", count) 183 } 184 185 // id生成器 186 type snow struct { 187 lock sync.Mutex 188 base int64 189 stamp int64 //上次生成编号的时间戳,24位,分钟为单位 190 train int //车次编号,13位 = 1024*8 191 server int //服务编号,9位 = 512 192 sequence int //上次生成编号,18位 = 1024*256 193 mask int 194 } 195 196 func newSnow(train, server int) *snow { 197 snow := &snow{ 198 train: train, 199 server: server, 200 base: time.Date(2012, 1, 0, 0, 0, 0, 0, time.UTC).Unix() / 60, 201 mask: -1 ^ (-1 << 18)} 202 203 return snow 204 } 205 206 func (snow *snow) nextInt() uint64 { 207 snow.lock.Lock() 208 defer snow.lock.Unlock() 209 210 ts := time.Now().Unix()/60 - snow.base 211 if ts == snow.stamp { 212 snow.sequence = (snow.sequence + 1) & snow.mask 213 if snow.sequence == 0 { 214 panic("error:overflow") 215 } 216 } else { 217 snow.sequence = 0 218 } 219 snow.stamp = ts 220 id := (uint64(snow.stamp) << (18 + 9 + 13)) | (uint64(snow.train) << (9 + 18)) | 221 (uint64(snow.server) << 18) | (uint64(snow.sequence)) 222 return id 223 }