• 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 }
  • 相关阅读:
    浮起来的验证消息
    关于jQuery UI 使用心得及技巧
    如何制作好一个提交按扭我是个爱折腾的人
    ABAP自测试题一 沧海
    商务出现问题 沧海
    [转帖]Report painter 沧海
    准备ABAP认证 沧海
    Characteristics and Key figures In Report Painter 沧海
    有朋自远方来 沧海
    What are the layers of data description in R/3? 沧海
  • 原文地址:https://www.cnblogs.com/buzzlight/p/2703439.html
Copyright © 2020-2023  润新知