商城高并发秒杀系统架构分析和设计

本文分析了高并发秒杀系统架构,探讨了负载均衡、预扣库存等策略。通过Nginx加权轮询实现负载均衡,讨论了下单减库存、支付减库存与预扣库存的方法,强调预扣库存的优化,如本地扣库存和远程统一减库存,以保证系统在高并发下的稳定性和防止超卖、少卖。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

高并发、高可用,都是面试的重点,如果你在简历中的项目与商城、秒杀相关,亦或是头部大厂、中厂,这样的问题你一定不会陌生。

之前面试某头部大厂,一面、二面均提到了关于商城中库存一致性和高并发中防止超卖、少卖、减库存,大概率三面也会问相关问题,趁这个机会,整理一下关于高并发秒杀系统的知识点。

这篇文章主要分析:如何在100万人同时抢一万张火车票时,系统提供正常、稳定的服务。

1. 高并发系统架构

高并发系统一般都会采用分布式集群部署,服务层上有层层负载均衡,并提供各种容灾手段(双活数据中心、异地多活、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。

image-20210201234017898

1.1 负载均衡

图中使用了三种负载均衡,分别是OSPF负载均衡、Lvs负载均衡、Nginx负载均衡。

  • OSPF(开放式最短路径优先)是一个内部网关协议(IGP)。OSPF通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF会自动计算路由接口上的Cost值,但也可以通过手工指定(优先)该接口的Cost值。OSPF计算的Cost,同样是和接口带宽成反比,带宽越高,Cost值越小。到达目标相同Cost值的路径,可以执行负载均衡,默认为4条线路的负载均衡,最大支持6条线路的负载均衡,我们可以在OSPF路由进程中的maximuum-paths=6修改负载均衡的线路数。

    image-20210202004053976
  • LVS(Linux Virtual Server),是一种集群技术,已经被集成到Linux内核模块中,实现了基于IP数据请求负载均衡的调度和内容请求分发技术。调度器具有很好的吞吐率,将请求均衡的转移到不同的服务器执行,且调度器能够屏蔽掉服务器的故障,从而将一组服务器构成一个高性能、高可用的虚拟服务器。

    image-20210202005033811
  • Nginx是一款高性能的HTTP代理/反代理服务器,服务器开发中经常用来做负载均衡,Nginx的负载均衡主要方式包括:①轮询 ②加权轮询 ③ip hash 轮询

1.2 Nginx加权轮询

Nginx实现负载均衡通过upstream模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器性能、负载能力设置响应的负载。

通过一个示例来实现加权轮询负载的配置,在本地监听8001-8004端口,权重配置为2、4、6、8。

#配置负载均衡
    upstream load_rule {
       server 127.0.0.1:8001 weight=2;
       server 127.0.0.1:8002 weight=4;
       server 127.0.0.1:8003 weight=6;
       server 127.0.0.1:8004 weight=8;
    }
    ...
    server {
    listen       80;
    server_name  baidu.com www.baidu.com;
    location / {
       proxy_pass http://load_rule;
    }
}

其他更细致的轮询策略等问题,可以查看理解 Nginx 源码中的这一篇:Nginx 中 upstream 机制的负载均衡

1.3 补充:压测实验

在本地/etc/hosts目录下配置了 www.baidu.com 的虚拟域名地址,接下来使用Go语言开启四个http端口监听服务,下面是监听在8001端口的Go程序,其他几个只需要修改端口即可:

package main

import (
	"net/http"
	"os"
	"strings"
)

func main() {
   
   
	http.HandleFunc("/buy/ticket", handleReq)
	http.ListenAndServe(":8001", 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"}, "8001")
	buf := []byte(content)
	fd.Write(buf)
}

我将请求的端口日志信息写到了./stat.log文件当中,然后使用ab压测工具做压测:

ab -n 1000 -c 100 http://www.baidu.com/buy/ticket

统计日志中的结果,8001-8004端口分别得到了200、400、600、800的请求量,这和我在nginx中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。

2. 秒杀抢购系统机制

Question:假如有100万人同时抢一张火车票,如何提供正常、稳定的服务呢?

在已经使用了负载均衡的情况下,用户秒杀的流量通过了每一层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机承受的QPS也是非常高的。

通常来说,订票系统需要处理生成订单、减库存、用户支付这三个基本的阶段,我们系统要做的事情就是保证火车票订单不超卖、不少卖,每一张出售的火车票都是必须支付才有效,还要保证在持续的高并发环境中运行稳定。

下面我们分析一下这三个基本阶段的先后顺序:

2.1 下单减库存

当用户请求并发到达服务端的时候,首先创建订单,然后扣除库存,等待用户支付。这是一种最常见的思路,这种情况可以保证订单不超卖,因为创建订单之后减库存(利用Redis increment 原子操作)这是一个原子操作。

但是这样也会产生一些问题:

  • **第一是:**在极限并发的情况下,任何一个内存操作的细节都至关重要的影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是非常巨大的;
  • **第二是:**如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制IP和用户的购买订单数量,这也不算是一个好方法。
image-20210202031939480

2.2 支付减库存

如果等待用户支付了订单再减库存,就可以避免少卖。这是并发架构的大忌,因为在极限并发的情况下,用户可能会不断点击下单键,导致下了多单,当库存减为0的时候很多用户会发现抢到的订单支付不了,这就是所谓的**“超卖”**。同时,这也不能避免并发操作数据库和磁盘IO。

image-20210202034154116

2.3 预扣库存

从上面两种方案考虑,我们得出结论:只要创建订单,就要频繁操作数据库IO。

那么我们优化的点就是:有没有一种方案可以不需要直接操作数据库IO呢?

这就是预扣库存(具体实现和优化参考3.预扣库存优化):

  1. 保证不超卖,然后异步生成用户订单,这样响应给
java实现秒杀系统@Controller @RequestMapping("seckill")//url:/模块/资源/{id}/细分 /seckill/list public class SeckillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method = RequestMethod.GET) public String list(Model model){ //获取列表页 List list=seckillService.getSeckillList(); model.addAttribute("list",list); //list.jsp+model = ModelAndView return "list";//WEB-INF/jsp/"list".jsp } @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model){ if (seckillId == null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null){ return "forward:/seckill/list"; } model.addAttribute("seckill",seckill); return "detail"; } //ajax json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){ SeckillResult result; try { Exposer exposer =seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true,exposer); } catch (Exception e) { logger.error(e.getMessage(),e); result = new SeckillResult(false,e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"} ) @ResponseBody public SeckillResult execute(@PathVariable("seckillId")Long seckillId,
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值