12306 的架构也太 "牛X" 了吧!
GitGub代码地址:https://github.com/GuoZhaoran/spikeSystem
1
大型高并发系统架构
1.1 负载均衡简介
1.2 Nginx加权轮询的演示
#配置负载均衡
upstream load_rule {
server 127.0.0.1:3001 weight=1;
server 127.0.0.1:3002 weight=2;
server 127.0.0.1:3003 weight=3;
server 127.0.0.1:3004 weight=4;
}
...
server {
listen 80;
server_name load_balance.com www.load_balance.com;
location / {
proxy_pass http://load_rule;
}
}
package main
import (
"net/http"
"os"
"strings"
)
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3001", nil)
}
//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
failedMsg := "handle in port:"
writeLog(failedMsg, "./stat.log")
}
//写入日志
func writeLog(msg string, logPath string) {
fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
content := strings.Join([]string{msg, "\r\n"}, "3001")
buf := []byte(content)
fd.Write(buf)
}
ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket
统计日志中的结果,3001-3004端口分别得到了100、200、300、400的请求量,这和我在nginx中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。
具体的实现大家可以参考nginx的upsteam模块实现源码,这里推荐一篇文章:
https://www.kancloud.cn/digest/understandingnginx/202607
2
秒杀抢购系统选型
回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢?
从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的QPS也是非常高的。如何将单机性能优化到极致呢?要解决这个问题,我们就要想明白一件事:
通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段,我们系统要做的事情是要保证火车票订单不超卖、不少卖,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。
这三个阶段的先后顺序改怎么分配才更加合理呢?我们来分析一下:
2.1 下单减库存
当用户并发请求到达服务端时,首先创建订单,然后扣除库存,等待用户支付。这种顺序是我们一般人首先会想到的解决方案,这种情况下也能保证订单不会超卖,因为创建订单之后就会减库存,这是一个原子操作。
但是这样也会产生一些问题。
第一就是在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的;
第二是如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制IP和用户的购买订单数量,这也不算是一个好方法。
2.2 支付减库存
如果等待用户支付了订单在减库存,第一感觉就是不会少卖。但是这是并发架构的大忌,因为在极限并发情况下,用户可能会创建很多订单,当库存减为零的时候很多用户发现抢到的订单支付不了了,这也就是所谓的“超卖”。也不能避免并发操作数据库磁盘IO
2.3 预扣库存
3
扣库存的艺术
从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求?
在单机低并发情况下,我们实现扣库存通常是这样的:
为了保证扣库存和生成订单的原子性,需要采用事务处理,然后取库存判断、减库存,最后提交事务,整个流程有很多IO,对数据库的操作又是阻塞的。这种方式根本不适合高并发的秒杀系统。
接下来我们对单机扣库存的方案做优化:本地扣库存。我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。改进过之后的单机系统是这样的:
4
代码演示
Go语言原生为并发设计,我采用go语言给大家演示一下单机抢票的具体流程。
4.1 初始化工作
...
//localSpike包结构体定义
package localSpike
type LocalSpike struct {
LocalInStock int64
LocalSalesVolume int64
}
...
//remoteSpike对hash结构的定义和redis连接池
package remoteSpike
//远程订单存储健值
type RemoteSpikeKeys struct {
SpikeOrderHashKey string //redis中秒杀订单hash结构key
TotalInventoryKey string //hash结构中总订单库存key
QuantityOfOrderKey string //hash结构中已有订单数量key
}
//初始化redis连接池
func NewPool() *redis.Pool {
return &redis.Pool{
MaxIdle: 10000,
MaxActive: 12000, // max number of connections
Dial: 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 本地扣库存和统一扣库存
package localSpike
//本地扣库存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
spike.LocalSalesVolume = spike.LocalSalesVolume + 1
return spike.LocalSalesVolume < spike.LocalInStock
}
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) then
return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
end
return 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
}
hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0
4.3 响应用户信息
package main
...
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3005", nil)
}
package main
//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
redisConn := redisPool.Get()
LogMsg := ""
<-done
//全局读写锁
if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
util.RespJson(w, 1, "抢票成功", nil)
LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
} else {
util.RespJson(w, -1, "已售罄", nil)
LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
}
done <- 1
//将抢票状态写入到log中
writeLog(LogMsg, "./stat.log")
}
func writeLog(msg string, logPath string) {
fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
content := strings.Join([]string{msg, "\r\n"}, "")
buf := []byte(content)
fd.Write(buf)
}
4.4 单机服务压测
ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software:
Server Hostname: 127.0.0.1
Server Port: 3005
Document Path: /buy/ticket
Document Length: 29 bytes
Concurrency Level: 100
Time taken for tests: 2.339 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1370000 bytes
HTML transferred: 290000 bytes
Requests per second: 4275.96 [#/sec] (mean)
Time per request: 23.387 [ms] (mean)
Time per request: 0.234 [ms] (mean, across all concurrent requests)
Transfer rate: 572.08 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 8 14.7 6 223
Processing: 2 15 17.6 11 232
Waiting: 1 11 13.5 8 225
Total: 7 23 22.8 18 239
Percentage of the requests served within a certain time (ms)
50% 18
66% 24
75% 26
80% 28
90% 33
95% 39
98% 45
99% 54
100% 239 (longest request)
//stat.log
...
result:1,localSales:145
result:1,localSales:146
result:1,localSales:147
result:1,localSales:148
result:1,localSales:149
result:1,localSales:150
result:0,localSales:151
result:0,localSales:152
result:0,localSales:153
result:0,localSales:154
result:0,localSales:156
...
5
总结回顾
总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略,完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。
作者:绘你一世倾城
来源:juejin.im/post/5d84e21f6fb9a06ac8248149
点击「阅读原文」和栈长学更多~
12306 的架构也太 "牛X" 了吧!的更多相关文章
- 同样是高并发,QQ/微博/12306的架构难度一样吗?
开篇 同一个用户并发扣款时,有一定概率出现数据不一致,可以使用CAS乐观锁的方式,在不降低吞吐量,保证数据的一致性: UPDATE t_yue SET money=$new_money WHERE u ...
- Intellij IDEA 智能补全的 10 个姿势,简直不能太牛逼!
Java技术栈 www.javastack.cn 优秀的Java技术公众号 一年多前,栈长那时候刚从 Eclipse 转型 IDEA 成功,前面转了好多次,都是失败史,都是泪.. 后面我就在微信公众号 ...
- 2.5w字 + 36 张图爆肝操作系统面试题,太牛逼了!
欢迎各位大佬访问我的 github ,跪求 star bestJavaer 大家好,我是 cxuan,我之前汇总了一下关于操作系统的面试题,最近又重新翻阅了一下发现不是很全,现在也到了面试季了,所以我 ...
- 【leetcode】Wildcard Matching(hard) ★ 大神太牛了
Implement wildcard pattern matching with support for '?' and '*'. '?' Matches any single character. ...
- 太牛X了!神奇的故事 你猜得到开头,却猜不到结尾
他在北京发来消息:“我明天去看你,来接我,好么?” 她在南京,开心地回复:“恩啊” 第二天,她在车站搜寻过往人群中他的身影, 期待又焦急“你到底在哪儿?” “傻瓜,你不会真在车站吧,今天是愚人节哎,哈 ...
- Rem是什么,牛逼的Vue,Epub竟然可以实现阅读器功能。太牛了。
rem 相当于根元素font-size值的倍数. 1rem=根元素font-size 2rem=根元素font-size * 2 // 1rem = fontSize px // 1px = (1/f ...
- 这个立冬,我线下面基了一位TMD高级专家,太牛逼了!
立冬刚过,迎面而来的是一股寒气.天气如此,市场亦是如此.昨天周五,和1个认识的技术专家老刘约饭,也算是线下面基,增进感情.每年我都要向比我高阶的朋友讨教.不由自主聊到了他的职场生涯.鱼哥一直以为自己命 ...
- Intellij IDEA 智能补全的 10 个姿势,太牛逼了。。
一年多前,栈长那时候刚从 Eclipse 转型 IDEA 成功,前面转了好多次,都是失败史,都是泪..后面我就在微信公众号 "Java技术栈" 写了这篇文章:Intellij ID ...
- 【DOS】这个命令太牛逼了
删除一个程序了后 竟然上不了网了 运行下列命令重启后就可以了 实在是救了我系统一命 命令如下: netsh winsock reset
随机推荐
- 医院设置x
医院设置x 题目描述 Description 设有一棵二叉树,如下图 其中,圈中数字表示结点居民的人口.圈边上数字表示结点编号,.现在要求在某个结点上建立一个医院,使所有居民所走的路程之和为最小, ...
- B. Tell Your World(几何数学 + 思维)
B. Tell Your World time limit per test 1 second memory limit per test 256 megabytes input standard i ...
- P1200 [USACO1.1]你的飞碟在这儿Your Ride Is Here
输入格式: 第1行:一个长度为111到666的大写字母串,表示彗星的名字. 第2行:一个长度为111到666的大写字母串,表示队伍的名字. 输出格式: 如果能搭配,就输出“GO”,否则输出“STAY” ...
- JavaWeb-SpringBoot(抖音)_二、服务器间通讯
JavaWeb-SpringBoot(抖音)_一.抖音项目制作 传送门 JavaWeb-SpringBoot(抖音)_二.服务器间通讯 传送门 JavaWeb-SpringBoot(抖音)_三.抖音项 ...
- Ansible常用模块之系统类模块
cron模块 管理远程主机上的计划任务 [root@tiandong ansible]# ansible all -m cron -a "name='cron test' minute=5 ...
- Runtime Error R6034 Application has attempt to load the C runtime library incorrectly
1.问题描述 vs2015 去开发一个写入pg数据库的程序,使用libpqxx.dll,libpq.dll,这个库文件之前是用vs2008的程序中复制过来的,基于的运行时库应该是vs2008,现在开发 ...
- 关于java中对BigDecimal加减乘除的基本用法
Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算.双精度浮点型变量double可以处理16位有效数. 在实际应用中,需要对更大或者更小的数进 ...
- onReachBottom 注意事项
onReachBottom使用注意 可在pages.json里定义具体页面底部的触发距离onReachBottomDistance,比如设为50,那么滚动页面到距离底部50px时,就会触发onReac ...
- Python中循环及判断语句
循环判断条件是编程语言中一个很重要的部分,python也不例外,循环判断条件一般结合continue,return,break关键字来判断,这些关键字用法与java中基本一致 一.if判断语句 判断条 ...
- python 面对对象 类(继承, 多态)
继承,继承其它实例化样本的属性和方法,需要在声明里重新定义和使用 class School(object): def __init__(self, name, addr): self.name = n ...