"12306”的架构到底有多牛逼?( 三 )


我们结合下面架构图具体分析一下:

"12306”的架构到底有多牛逼?

文章插图
 
我们采用redis存储统一库存,因为Redis的性能非常高,号称单机QPS能抗10W的并发 。在本地减库存以后,如果本地有订单,我们再去请求redis远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖 。
当机器中有机器宕机时,因为每个机器上有预留的buffer余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖 。
buffer余票设置多少合适呢,理论上buffer设置的越多,系统容忍宕机的机器数量就越多,但是buffer设置的太大也会对redis造成一定的影响 。
虽然redis内存数据库抗并发能力非常高,请求依然会走一次网络IO,其实抢票过程中对redis的请求次数是本地库存和buffer库存的总量,因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑,这在一定程度上也避免了巨大的网络请求量把redis压跨,所以buffer值设置多少,需要架构师对系统的负载能力做认真的考量 。
4. 代码演示Go语言原生为并发设计,我采用go语言给大家演示一下单机抢票的具体流程 。
4.1 初始化工作go包中的init函数先于main函数执行,在这个阶段主要做一些准备性工作 。我们系统需要做的准备工作有:初始化本地库存、初始化远程redis存储统一库存的hash键值、初始化redis连接池;
另外还需要初始化一个大小为1的int类型chan,目的是实现分布式锁的功能,也可以直接使用读写锁或者使用redis等其他的方式避免资源竞争,但使用channel更加高效,这就是go语言的哲学:不要通过共享内存来通信,而要通过通信来共享内存 。redis库使用的是redigo,下面是代码实现:
...//localSpike包结构体定义package localSpiketype LocalSpike struct {LocalInStockint64LocalSalesVolume int64}...//remoteSpike对hash结构的定义和redis连接池package remoteSpike//远程订单存储健值type RemoteSpikeKeys struct {SpikeOrderHashKey string//redis中秒杀订单hash结构keyTotalInventoryKey string//hash结构中总订单库存keyQuantityOfOrderKey string//hash结构中已有订单数量key}//初始化redis连接池func NewPool() *redis.Pool {return &redis.Pool{MaxIdle:10000,MaxActive: 12000, // max number of connectionsDial: func() (redis.Conn, error) {c, err := redis.Dial("tcp", ":6379")if err != nil {panic(err.Error())}return c, err},}}...func init() {localSpike = localSpike2.LocalSpike{LocalInStock:150,LocalSalesVolume: 0,}remoteSpike = remoteSpike2.RemoteSpikeKeys{SpikeOrderHashKey:"ticket_hash_key",TotalInventoryKey:"ticket_total_nums",QuantityOfOrderKey: "ticket_sold_nums",}redisPool = remoteSpike2.NewPool()done = make(chan int, 1)done <- 1}4.2 本地扣库存和统一扣库存本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回bool值:
package localSpike//本地扣库存,返回bool值func (spike *LocalSpike) LocalDeductionStock() bool{spike.LocalSalesVolume = spike.LocalSalesVolume + 1return spike.LocalSalesVolume < spike.LocalInStock}注意这里对共享数据LocalSalesVolume的操作是要使用锁来实现的,但是因为本地扣库存和统一扣库存是一个原子性操作,所以在最上层使用channel来实现,这块后边会讲 。
统一扣库存操作redis,因为redis是单线程的,而我们要实现从中取数据,写数据并计算一些列步骤,我们要配合lua脚本打包命令,保证操作的原子性:
package remoteSpike......const LuaScript = `local ticket_key = KEYS[1]local ticket_total_key = ARGV[1]local ticket_sold_key = ARGV[2]local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))-- 查看是否还有余票,增加订单数量,返回结果值if(ticket_total_nums >= ticket_sold_nums) thenreturn redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)endreturn 0`//远端统一扣库存func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {lua := redis.NewScript(1, LuaScript)result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))if err != nil {return false}return result != 0}我们使用hash结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的bool值 。在启动服务之前,我们需要初始化redis的初始库存信息:


推荐阅读