秒杀的基本知识点,了解一下(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 秒杀的基本知识点,了解一下

4. 代码演示


Go语言原生为并发设计,我采用go语言给大家演示一下单机抢票的具体流程。


4.1 初始化工作


go包中的init函数先于main函数执行,在这个阶段主要做一些准备性工作。我们系统需要做的准备工作有:初始化本地库存、初始化远程redis存储统一库存的hash键值、初始化redis连接池;另外还需要初始化一个大小为1的int类型chan,目的是实现分布式锁的功能,也可以直接使用读写锁或者使用redis等其他的方式避免资源竞争,但使用channel更加高效,这就是go语言的哲学:不要通过共享内存来通信,而要通过通信来共享内存。redis库使用的是redigo,下面是代码实现:

...
//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 本地扣库存和统一扣库存


本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回bool值:

package localSpike
//本地扣库存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
  spike.LocalSalesVolume = spike.LocalSalesVolume + 1
  return 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) 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
}


我们使用hash结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的bool值。在启动服务之前,我们需要初始化redis的初始库存信息:

hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0


4.3 响应用户信息


我们开启一个http服务,监听在一个端口上:

package main
...
func main() {
  http.HandleFunc("/buy/ticket", handleReq)
  http.ListenAndServe(":3005", nil)
}


上面我们做完了所有的初始化工作,接下来handleReq的逻辑非常清晰,判断是否抢票成功,返回给用户信息就可以了。

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)
}


前边提到我们扣库存时要考虑竞态条件,我们这里是使用channel避免并发的读写,保证了请求的高效顺序执行。我们将接口的返回信息写入到了./stat.log文件方便做压测统计。


4.4 单机服务压测


开启服务,我们使用ab压测工具进行测试:

ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket


下面是我本地低配mac的压测信息

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)


根据指标显示,我单机每秒就能处理4000+的请求,正常服务器都是多核配置,处理1W+的请求根本没有问题。而且查看日志发现整个服务过程中,请求都很正常,流量均匀,redis也很正常:

//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.总结回顾


总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略,完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对DB数据库IO的操作,对Redis网络IO的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。我觉得其中有两点特别值得学习总结:

  • 负载均衡,分而治之。通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致,这样系统的整体也就能承受极高的并发了,就像工作的的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。合理的使用并发和异步。自epoll网络架构模型解决了c10k问题以来,异步越来被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果,这点在nginx、node.js、redis上都能体现,他们处理网络请求使用的epoll模型,用实践告诉了我们单线程依然可以发挥强大的威力。服务器已经进入了多核时代,go语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如go处理http请求时每个请求都会在一个goroutine中执行,总之:怎样合理的压榨CPU,让其发挥出应有的价值,是我们一直需要探索学习的方向。

相关文章
|
机器学习/深度学习 存储 算法
数据结构 | 算法的时间复杂度和空间复杂度【详解】(一)
什么是数据结构? 数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
|
存储 安全 数据管理
Linux系统编程教程之Linux线程函数的使用:讲解Linux线程函数
Linux系统编程教程之Linux线程函数的使用:讲解Linux线程函数
163 1
|
12月前
|
自然语言处理 JavaScript 前端开发
一文梳理JavaScript中常见的七大继承方案
该文章系统地概述了JavaScript中七种常见的继承模式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承等,并探讨了每种模式的实现方式及其优缺点。
一文梳理JavaScript中常见的七大继承方案
|
JavaScript UED
HTML中的<a>标签使用指南
HTML中的<a>标签使用指南
471 6
|
前端开发 JavaScript Java
计算机Java项目|SSM智能仓储系统
计算机Java项目|SSM智能仓储系统
130 0
|
运维 Kubernetes jenkins
测试流程--测试发版规范
为了保证系统稳定性,对软件项目的上线过程进行规范,确保项目符合产品需求。对于已经开发完毕的系统,需要正式部署到生产环境前必须严格按照以下流程规范实施。 规范发版的流程,指定发版的相关输出,相关信息的收集,并通知相关业务方了解发版信息。防止或减少因发版造成的系统抖动对业务产生的影 响,并有利于追溯发版过程,方便后续优化迭代。
2015 0
测试流程--测试发版规范
|
Unix Linux Windows
Qwt开发笔记(一):Qwt简介、下载以及基础demo工程模板
QWT开发笔记系列整理集合,这是目前使用最为广泛的Qt图表类(Qt的QWidget代码方向只有QtCharts,Qwt,QCustomPlot),使用多年,系统性的整理,本系列旨在系统解说并逐步更新其各种Demo示例。
Qwt开发笔记(一):Qwt简介、下载以及基础demo工程模板
|
Web App开发 索引
抓取猫眼电影top100的正则、bs4、pyquery、xpath实现方法
抓取猫眼电影top100的正则、bs4、pyquery、xpath实现方法
1240 0
|
Web App开发 缓存 网络协议
android开发之http协议
http协议学习系列 1. 基础概念篇 1.1 介绍   HTTP是Hyper Text Transfer Protocol(超文本传输协议)的缩写。它的发展是万维网协会(World Wide Web Consortium)和Internet工作小组IETF(Internet Engineering Task Force)合作的结果,(他们)最终发布了一系列的RFC,
2278 0