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

View Code
  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 }
posted @ 2012-09-26 09:57  buzzlight  阅读(2763)  评论(7编辑  收藏  举报