# distributed-seckill-service **Repository Path**: 521069205/distributed-seckill-service ## Basic Information - **Project Name**: distributed-seckill-service - **Description**: 个人复盘:分布式商品秒杀服务模块 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2025-08-04 - **Last Updated**: 2025-08-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 高并发场景分布式的商城商品"秒杀"服务模块 ### 1. 项目简介 所谓的商品“秒杀”,就是卖家通过网上商城或其它类似的平台发布一些超低价格的商品,而所有买家将会同一时间内在网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时整个过程甚至只花了一秒钟,由此可见.这非常考验商城系统服务器的配置,也考验系统的设计. ### 2. 需求分析阶段 #### 2.1 基本需求分析 1. **商品不能超卖!**作为一个和交易相关的功能模块,这个是最重要的硬指标没有之一. 2. 接口的性能,尽可能的提高并发量和吞吐量. 3. 水平拓展性,考虑到用户量会随着业务发展而增加,PV也会随之指数级增长.为了应对这种甜蜜的痛苦,系统的架构应该选择水平拓展性好的分布式,而且为了方便系统负载能力的紧急扩容,应该要有一种类似jenkins这样的自动部署方案. 4. 明确抢购成功和失败的条件,怎么才算抢购成功或者失败,这个需要明确规定,不然怎么做下去呢是吧. #### 2.2 用户画像和行为分析 为什么需要分析这个?因为分析用户的行为和特点有助于提高系统的可用性,安全性(保证不能出现超卖!)为需求一二服务. 1. 正常用户,即是正常通过商城页面下单的用户.他们的行为单一,比较容易控制.如前端通过倒计时模块对按钮置灰,在活动开始前把下单按钮置灰即可控制用户发起请求的时间段,以及通过按钮点击间隔,即可对其限流.![avatar](images/tmp/1.jpg) 2. 同行,是你身边的初中级同事,会按F12康康下单URL,也会通过抓包分析动态URL..他们会在秒杀活动贡献相当一部分的请求量.其实这类用户也是挺好控制的,通过nginx的限流算法即可限制这类用户的请求量. ![avatar](images/tmp/2.jpg) 考虑到请求只需要提交一次,而且不需要保证请求一定能到达后端,所以我采用的是nginx的漏桶算法. 3. 专业黄牛,这一类用户拥有大量的肉鸡和大量的机器人账号,能在活动开始的一瞬间内清空商品库存,但我目前能力有限,做不了.不过这方面好像本来就不是JAVA佬搞的 ### 3. 设计阶段 既然需求已经提出来了,那么就开始设计吧. #### 3.1 架构的选择 从该项目的名字中你就可以知道是分布式的了,但为什么是分布式呢?原因很简单,那就是为了水平拓展,以及后续开发/维护的难度。而微服务也是分布式的一种。 #### 3.2 技术选型 1. 注册中心以及配置中心`nacos`,要做分布式(微服务),那当然就要有注册中心,同类的产品有`springcloud-netfilx-eureka`和`zookeeper`可以用,但是呢eureka已经闭源了,我在验证的时候发现eureka甚至不能和`springcloud-gateway`一起使用 ,那还算什么,赶紧换掉!那么同类的产品还有zk,但是zk是CP的,而注册中心AP是铁律,所以看了一圈,发现nacos是最合适的了,能配合gateway使用/中文文档/还自带配置中心. 2. 网关`springcloud-gateway`,还是和eureka一个原因,因为同属netfilx的zuul闭源了. 3. 微服务负载均衡器/熔断器`springcloud-openfeign`,openfeign其实就是feign的升级版,这个集负载均衡和熔断于一身的组件,不是选不选的问题,是必须要用的. 4. 缓存`redis`,支持持久化,支持多种数据结构,速度快,有哨兵这种成熟的故障转移方案,社区活跃,缓存的不二之选. 5. 分布式协调系统`zookeeper`,原因有两个,在下面穿插讲. 6. 消息队列`rabbitmq`,这个我倒没考虑什么,就因为现在只用过这个mq. 7. 数据库中间件`mycat`,数据库分库分表协调中的不二之选. 8. 数据库`mysql`,原因同mq. 9. ORM`spring-data-jpa`,本来这个模块也没什么复杂的sql操作,所以用jpa够了. 10. 容器化技术`docker`,俺认为,模块化技术是大趋势,所以趁这次机会把这个加进来熟练下对其的操作也好.另外,我真感谢自己选择了docker,因为在后期部署测试中`docker-compose`实在是帮我省去了太多的繁杂重复劳动了,你没用过的话真应该学习一下. 11. web服务器`nginx`,能做正反代理,分流限流,性能也不错,也仅此一家,我还选个屁啊. #### 3.3 微服务模块设计和分析 在设计过程中,考虑到将来会加入消息队列,想起了消息队列的AMQP模型,然后发现这个模型的架构很适合我这个项目,遂采用了类似的架构.即把后端的微服务组件分出了producer和consumer模块,对应AMQP的生产者消费者.然后加上其它模块,该项目所有模块分为以下四个 1. `seckill-core-gateway` 是nginx后的流量主入口.对该项目中的api模块负载均衡调用,其实也就是微服务集群之间的负载均衡调器. 2. `seckill-core-api` 用于在同一集群内对其它微服务组件负载均衡的节点,同时也具备熔断功能,避免因为网络波动或后续调用的节点发生故障,造成调用链阻塞,最终造成整个集群不可用状态.是保证集群高可用性的其中一环. 3. `seckill-core-producer` 我把整个秒杀核心逻辑都塞进这个模块了.先康康我这个模块的设计,下面会解释![avatar](images/2.png) 4. `seckill-core-consumer` 系消息消费和消息处理后入库模块,除了需要保证消息的可靠性,这个模块没什么值得说的. #### 3.4 确定架构和技术 ![avatar](images/1.png) 其实这一张图只是我目前部署的拓扑图,实际中是不会这样部署的,只是受限于我个人财力(没钱买机器啊...这两台其中一台还是我的旧电脑用内网穿透来凑合使用的,云服务器太贵了) #### 3.5 实现需求一 - 怎么保证商品不能超卖? 造成超卖的原因很简单,譬如库存就剩一个商品了.有第一个抢购线程进来读取库存,发现还有商品然后去下单,下单完毕后再去修改库存,由于修改库存需要时间.如果这时候有第二个抢购线程进来,读了还没更新完毕的库存数据,以为库存还有然后去下单了.这样就出现了超卖. 那应该怎么去规避这种情况呢?说到底这就是个多线程并发读写的安全问题,常规的做法就是对资源加锁,也就是对库存资源加锁.不过,对于这个项目是分布式的,常规的锁是不适用的,需要采取分布式锁,而且根据需求,我认为这个锁还应该是乐观锁。最终,这个分布式乐观锁我最终选择用了zk来实现,而貌似现在也只有zk这种分布式协调系统能够做这种强一致性的分布式锁,所以这也是我使用zk的第一个原因,而使用zk的第二个原因我在下面会解释,这里不方便说话~ #### 3.6 实现需求二 - 怎么提高接口的性能? 很多类似的文章博客都讲烂的,这里只提几个关键词:限流/分流/缓存/异步.至于为什么用我就不解释了. 1. 限流,就是刚刚提到的在nginx层做了一个漏桶算法限流. 2. 分流,nginx的负载均衡是第一层分流,而通讯层的springcloud-gateway/feign则分别为第二第三层的分流.gateway主要作用于服务机器集群之前的负载均衡,feign主要作用于某机器内的微服务集群之间的负载均衡,如果你不打算在每个机器都部署多个微服务,可以省去feign模块,但熔断和降级还是要做的,交给gateway来做. 3. 缓存,这个项目中,我决定让缓存系统来保存商品库存的数据,而且让缓存系统直接落地.这样的做法可以省去缓存的防击穿/穿透/雪崩策略.不过也加大了数据丢失的风险.而怎么处理这个风险,下面会详细讲. 4. 异步,用消息队列来处理抢购线程的入库操作,加入消息队列同样也增加了数据的风险,怎么处理下面讲. #### 3.7 实现需求三 - 怎么设计水平拓展性? 关于这个,我目前只考虑了业务层(关于其它层次的水平拓展,可能在之后的更新中补充,也可能不会。) ##### 业务层的水平拓展(关键词:注册中心、一致性哈希) 因为业务层涉及缓存,所以分为两部分: 1. `seckill-core-producer`,既然已经选用了nacos作为注册中心,那么这种常规的微服务模块的水平拓展就特别简单了,直接部署就好了,只要保证能向注册中心注册,并且和其它节点能通讯就行. 2. `缓存系统`,在我实际部署的拓扑图中可以看见redis服务集群就三个节点,但是如果在活动中某个节点挂了,虽然有哨兵会进行故障转移,但是剩下的节点不足以应对压力怎么办,那就要拓展啊,还有就是从一开始就低估了活动的请求量,节点没挂但也是需要进行拓展的.怎么拓展?其实也很好办,那就是引入一致性哈希算法,让该算法来告诉抢购线程应该访问哪一个redis集群或节点就好了.使用一致性哈希的时候会出现资源倾斜的问题,同样的我放到下面去讲. #### 3.8 实现需求四 - 明确抢购的成功和失败条件(情况)是什么? 我参考了一些资料,从法律层面来说,了解到的是“网络买家通过交易凭证来确定交易(成功)的结果”,也就是说所有能充当交易凭证(该项目中的交易凭证是订单号)的信息一旦返回给了用户,当用户感知到了凭证,那么就已经在法律上完成了一次交易(订单也是交易,是0元预付款的交易。)。在这之后这个交易就可以产生纠纷了。而纠纷涉及的东西实在太多了我作为开发人员只需做好我的本职工作就好了,即“保护卖方凭证”。这也是为什么我producer的设计是订单号生成后会通过消息队列入库(持久化交易凭证),而且,还是在万一消息入队失败了,打好了日志记录之后才返回交易凭证(订单号)给用户,确保卖方数据库或者日志里必须会有交易凭证的存在。 说到这里,抢购什么时候什么情况算成功或失败就很明朗了,即 只要保证后续操作不会让用户感知到交易凭证,都可以算作抢购失败。 ### 4. 解决使用中间件所带来的问题 该项目中用到了redis ,zk ,rabbitmq,这些中间件,中间件有利当然有弊端,弊端即增加了系统的复杂性,降低了系统的可靠性。既然知道了这些弊端那当然就要把这些弊端通过一些方案/操作来解决掉啦,不然还用个🔨的中间件啊,是吧? #### 4.1 为什么使用zk做分布式锁? zk没有什么需要解决的,不过倒是要讲讲为什么我要使用zk做锁,原因有两个,第一个是可以用其做分布式锁,前面我已经讲啦自己倒回去康康。而第二个原因,是出于对性能的考虑,因为使用zk做的分布式锁,在锁存在的时候,其它线程需要修改资源发现锁存在,并不需要一直询问锁是否释放,而是通过zk监听器的回调,以异步的方式通知线程锁释放了,可以去尝试申请了。这样就不会造成一大堆等待加锁的线程存活在内存中,缓解了很大的压力。只是zk才能做到的。 ![avatar](images/tmp/11.jpg) 另外,我认为秒杀商品嘛,用户能不能抢到商品本来很大部分取决于运气,而且秒杀逻辑对用户来说也是个黑盒,所以我决定监听器线程没必要一直存在,在创建监听器到释放的之间,如果锁释放了就申请再次竞争秒杀,如果没释放,放弃就放弃吧。反正到这里订单还没生成,直接告知用户抢购失败就好了啦。 ### 4.2 如何保证缓存系统的高可用性和可靠性? 在该项目中,我根据需求考虑过后,决定让缓存系统直接落地,虽然这样设计就不用再考虑缓存三防(缓存穿透,击穿,雪崩)了,也不需要考虑缓存和DB的一致性.但也加大了数据丢失的风险.所以保证可靠性是首当其冲的,其次才是可用性 #### 4.2.1 缓存系统的可靠性 缓存的可靠性比较简单,开启RDB和AOF备份缓存数据即可: ![avatar](images/tmp/8.jpg) ![avatar](images/tmp/9.jpg) AOF我选择每条指令都持久化一次,确保了数据只会丢失一条.当然这很消耗性能.但是这毕竟是非常重要订单数据,性能消耗就消耗吧,至于为什么开了AOF还需要开RDB的持久化,那是因为这RDB快照恢复得快呀,在redis节点宕机重启时,能够快速恢复数据.因为RDB比AOF恢复快得多了.(有AOF了就不怕数据大量丢失,所以这里RDB我用的是默认的策略) #### 4.2.2 缓存系统的高可用性 这个嘛,我缓存集群使用的是主从架构(一主双从),加上哨兵系统的自动故障转移,这对于缓存系统的可用性来说已经很足够了。关于哨兵的故障转移,是投票机制的,它根据一个票数是否占哨兵数量的一半多至少一个来决定一个受监控节点的死活.投票成功后还会再进行一次选举,选出一个哨兵老大来着手执行故障转移。我这个讲那么多只是为了告诉你,哨兵的故障转移机制和选举有关,而选举机制嘛,那就决定了哨兵的数量应该是大于1的奇数。 哨兵的其余配置如下,都是能直接看懂的,我就不解释了啊。 ![avatar](images/tmp/10.jpg) ### 4.3 如何保证消息队列的可靠性? 在该项目中,消息队列用于处理订单,是需要保证数据传输过程中安全的,即是消息队列的可靠性,消息队列的可靠性可以分为三个部分,分别是 `生产者` `MQ自身` `消费者` #### 4.3.1 消息生产者的可靠性 其中生产者开启来手动签收模式(即confirm模式),意在当消息发送失败时,系统能够感知,并对其记录,供其它系统进行补偿 ![avatar](images/tmp/5.jpg) #### 4.3.2 MQ自身的可靠性 MQ自身使用消息持久化来避免消息在还来不及消费或者刚送达MQ发送故障宕机时造成数据丢失![avatar](images/tmp/6.jpg) #### 4.3.3 消息消费者的可靠性 使用手动ACK模式,旨在只有消息消费并处理成功时,才通知MQ服务端把消息处理掉,否则MQ不会让这条消息消失,而是会选择重发,直到消息消费成功.![avatar](images/tmp/7.jpg) ### 5.全链路压测 什么是全链路压测捏?有小伙伴可能第一次接触这个词不知道这是啥意思我简单解释一下。 想象一下,一个系统的入口是一颗树结构的根节点,然后其每个能调用的请求过程就是根节点到达另一个节点的过程,途中的每个节点都代表一个新的调用者。譬如一个常规的三层架构,http->前端控制器->后端控制器->业务层->持久层->本地数据层。该调用链中间走过了四个节点。(感觉越讲越离谱..百度也没有好的解释,我就尝试解释一下不要打我!),当然这只是其中一个链路,有可能持久层调用的是第三方的数据层,这又是另一条链路。那么在全链路压测就是尽可能把颗"树",所有叶子节点遍历出来。即把这些链路都压测了个遍,那就叫全链路压测。 回归正题,在我这个项目里,其实链路也就一条,那就是抢购订单。限于抢购需要用户ID,也限制用户只能购买一次,所以我的压测脚本是这样滴,详细可以自己去`seckill-test\seckill压力测试.jmx`康康 ![avatar](images/tmp/13.jpg) 偷懒,只测了两组测试的部署配置都是api单点,producer双点,redis集群,zk集群,mq单点。gateway因为api只开了一个就没必要用了,同样的consumer是异步操作的,没有性能压测的必要我就不开了。 1. `1000持续并发全链路测试`,请求把所有中间件都调用过了,包括申请分布式锁。因为有用户只能购买一次的限制,所以也没必要测上几分钟。我随便测测就好了。 ![avatar](images/tmp/17.jpg) 2. `1500持续并发测试`,不是全链路的,仅仅到达`判断用户是否抢购过`而已(其实就是仅仅访问了一次缓存就返回)。![avatar](images/tmp/15.jpg) ### 6. 如果你想测试该项目 虽然有限流模块,但我实际部署已经关了限流。以供你压测验证该项目的性能。 你可以使用我的jmeter压测脚本 `seckill-test/seckill压力测试.jmx` 另外由于部署的机器是靠内网穿透来连接外网的,再加上我没有钱,用的是白嫖的乞丐版...so,没有固定IP,如果有需要的话,请联系我,索要最新的压测IP地址。 ### 7. 如果你想部署该项目 大哥请看下列配置文件/脚本/部署指南 #### 7.1 配置文件概览 1. zk集群: seckill-core/config/zookeeper 2. redis集群: seckill-core/config/redis/server 3. 哨兵集群: seckill-core/config/redis/sentinel 4. 注册中心/配置中心: seckill-core/config/nacos  5. 自定义的微服务节点docker镜像的制作脚本: seckill-core/config/deploy/dockerfiles #### 7.2 部署脚本概览 1. 基于windows部署控制脚本: seckill-core/deploy/bin 2. docker相关脚本:seckill-core/deploy/docker/docker-compose-yml #### 7.3 部署指南(同时也是真实情况中,活动开始前的准备) 1. 部署redis集群 ,部署脚本在`seckill-core/deploy/docker/docker-compose-yml/redis-cluster.yml` 2. 部署mq,部署脚本在`seckill-core/deploy/bin` 3. 部署注册中心以及配置中心,部署脚本在`seckill-core/deploy/bin` 4. 部署服务节点,部署脚本在`seckill-core/deploy/docker/docker-compose-yml/core-cluster.yml` 5. 部署zk集群,部署脚本在`seckill-core/deploy/bin` 6. 进行系统预热,访问`/nodePreheat?code=admin123`来预热系统.  #### 7.4 为什么需要预热系统? 因为MQ的交换器和队列我使用的是声明式创建的,需要produce模块来调用创建,所以要先走一次。而且要更新缓存系统中的库存信息。另外还有一个需要预热的原因,即是在系统部署后,如果长时间出于休眠状态,linux资源管理系统会把服务节点的内存回收到SWAP持久化硬盘,所以要在活动开始前的几分钟,用一组请求来贯穿整个系统,以求把SWAP里的信息重新加载到内存中。![avatar](images/tmp/12.jpg) 这个不算完整的预热,只是临时做的嘿嘿.. ### 8. 不足之处 你可能注意到,每个模块我都尽量做了一主多从的模式,这并不是我根据实际要求的高可用性来做的,而是我想实践一次这些高可用方案。当然,也受限于机器配置,最终也有一些模块我没有做成集群模式,只是做了单点。 1. mysql,单点的,既然mysql是单点的,那么mycat我也没有做了嘿嘿。 2. nacos,单点的,这玩意,太太太太消耗内存了,我的云服务器只能部署一个,因为我还需要部署其它项目。 ### 9. 未来(可能)更新的内容 1. redis一致性哈希的资源倾斜解决方案 2. 用redis替换掉原来用zookeeper做的分布式锁. 3. 加入消息异步处理模块的重试机制 4. 基于keepalived的nginx部分的高可用方案