☰
SpringBoot演唱会抢票系统:高并发秒杀与防超卖实战
2026/9/30 3:27:53 网站建设 项目流程

基于SpringBoot的演唱会抢票系统

做毕设的同学或者准备面试问秒杀场景的朋友,应该能在这个项目上找到你需要的东西。演唱会抢票系统,说白了就是用SpringBoot搭一个能扛住高并发瞬时流量的售票后端,核心要解决的就是“超卖”“并发抢同一张票”“接口被刷”这几件事。市面上很多教程把抢票讲得玄乎,其实落到代码层面,就是缓存、锁、队列和限流那几板斧。这篇文章我会把这个系统从架构设计到核心代码怎么写、再到压测怎么调优、答辩怎么讲,完整捋一遍,把我在实际项目里踩过的坑和验证过有效的方法都放进来,算是给后来人一份能直接上手的实践笔记。

适合谁看?两类人。一类是正在做SpringBoot课设或毕设的学生,想知道怎么把“抢票”这个题目做出技术含量,而不是写一个简单CRUD然后被答辩老师问住;另一类是准备Java后端面试的开发者,想通过一个具体业务场景把Redis分布式锁、消息队列削峰、接口幂等这些知识点串起来。当然,如果你就是对这类高并发后台系统感兴趣,想搞懂“为什么12306的票又没了但我这边还在转圈”,这篇也能给你一些直观答案。

有人可能会问,这系统到底能干什么?咱们把场景想象一下:某个歌手官宣开演唱会,门票分档位(看台、内场、VIP),总数比如5万张,开票时间是周六上午10点整。几十万人在同一秒涌进来,有人抢到了,有人页面直接崩了,还有黄牛脚本在疯狂刷接口。这个系统的存在价值,就是在这么大的瞬时压力下,保证正常的购票用户能公平抢到票,系统不挂,数据不错,钱和票对得上。所有技术选型,都是围绕这个目标展开的。

1. 内容整体设计与思路拆解

1.1 从需求痛点到技术选型:为什么偏偏是SpringBoot

我做这类系统之前先把需求掰开揉碎看了一遍,不夸张地说,抢票系统的难点不是“能卖票”,而是“在大家都点的时候还能正常卖票”。咱们梳理一下核心痛点:

  • 瞬时高并发:几十万请求在开票瞬间涌入,普通接口根本扛不住。
  • 库存一致性:5万张票,卖出去5万张,一张不能多卖(超卖),一张也不能少卖(少卖影响收入)。
  • 用户公平性:不能让脚本刷子把票都抢走,也不能让手速慢的普通用户完全没机会。
  • 接口安全:要有防刷、防重、防恶意请求机制。
  • 数据一致性:生成订单、扣减库存、支付回调,这几个动作要保证最终一致,不能出现“订单生成了但库存没扣”这种脏数据。

针对这些痛点,技术选型上SpringBoot几乎是现阶段最合理的选择。为什么这么说?因为SpringBoot有一套成熟的生态,自动装配把配置成本砍掉一大截,内置Tomcat支持并发连接处理,结合Spring MVC做接口层、MyBatis-Plus做数据持久层、Redis做缓存层,整个骨架一周左右就能搭起来。这不是说SpringBoot性能比别的框架强,而是它在开发效率、生态完整度、人群普及度上最适合这类教学型项目。

在存储选型上,MySQL存订单和用户数据,Redis扛实时库存。很多人会疑惑,为什么库存不直接放MySQL?因为MySQL的磁盘IO和事务机制在高频更新下撑不住,而且行锁竞争会直接拖垮性能。Redis是内存操作,单线程模型天然避免并发竞争,配合Lua脚本做原子扣减,性能上可以轻松支撑几万QPS。两者配合的套路是:库存预热到Redis,抢票时直接操作Redis,异步把订单落库。

1.2 整体架构:一张图理清数据流向

整个系统的组件职责我用最简单的方式捋一下。前端用的是Vue,用户打开页面后看到票档和余票数,点击“抢票”按钮后,请求通过Nginx负载均衡打到后台服务,后台服务先做参数校验和风控判断,然后走Redis扣减库存,扣减成功就发消息给MQ,由MQ的消费者异步创建订单、锁定座位,最后返回给用户“抢票成功”或者“已售罄”,其中一些高频查询(比如查余票)就直接走Redis缓存。MySQL在这里扮演的角色是最终数据落盘的地方,保证数据不会丢。

这里每个组件都在自己的岗位上有明确分工,我画个最简单的分工表:

组件角色定位承担的职责
Nginx流量入口负载均衡、连接限制、静态资源缓存
SpringBoot服务业务核心接口提供、参数校验、业务逻辑、异常处理
Redis高速缓存库存预热与原子扣减、分布式锁、接口限流、用户抢票状态记录
RabbitMQ消息队列订单异步化、流量削峰、失败重试缓冲
MySQL数据持久层订单表、用户表、场次表、座位表的最终存储
Vue前端用户交互抢票页面渲染、倒计时展示、结果反馈

依赖关系也非常清晰:请求进来先到Nginx,再到SpringBoot,SpringBoot同时依赖Redis做实时数据处理,通过MQ把业务数据异步传给消费者,消费者最终把结果写入MySQL。这种架构说白了就是一个“前后端分离 + 缓存加速 + 异步解耦”的经典组合,它的优雅之处在于,业务高峰期核心链路不直接碰数据库,数据库压力被削平了一大截。

1.3 为什么不用纯数据库方案:超卖问题演示

这里必须解释一下“超卖”是怎么发生的,否则你理解不了后面为什么又是缓存又是锁的。超卖的本质是在高并发条件下,多个请求同时读到同一个剩余库存数量,然后各自认为“还有票”,各自扣减后库存变成负数。

举个具体的场景:数据库里有一张票表,剩余库存字段stock初始值是1。两个用户同时发起抢票请求,在事务隔离级别是默认的可重复读下,两个事务都执行SELECT stock FROM ticket WHERE id=1,读到的都是1。两个用户都判断“stock>0,可以卖”,然后执行UPDATE ticket SET stock=stock-1 WHERE id=1。第一个事务提交后,stock变成0,第二个事务再提交,就变成-1。一张票卖给了两个人,超卖发生了。

有人说,那我在UPDATE语句里加条件WHERE id=1 AND stock>0不就行了?这能解决一部分问题,但数据库行锁会让所有请求排队处理,并发能力大幅下降。在最极端的情况下,10万人同时抢,数据库连接池瞬间被打满,整个服务直接雪崩。简单说,数据库方案不是不能做,是适合流量不大的场景,做抢票这种业务,性能瓶颈太明显了。所以我做实践的时候,首选是Redis+Lua脚本扣库存,这个方案后面详细说。

2. 核心细节解析与实操要点

2.1 初始化环境与依赖版本选择

动手写代码之前,先保证环境干净。我用的是JDK 1.8(做毕设和大多数公司的线上环境依然是这个主力版本)、SpringBoot 2.7.x、Maven 3.6+,IDE用的IDEA,数据库MySQL 5.7,缓存Redis 6.x,消息队列RabbitMQ 3.9。这几个版本搭配起来非常稳定,网上资料也多,出了问题好排查。

如果你问我为什么不用SpringBoot 3.x,我的建议是如果你是做毕设,别追新。SpringBoot 3.x要求JDK 17起步,很多老教程和老依赖都不兼容,出了问题你找到的解决方案都是英文社区的,时间成本高。用2.7.x版本踩坑的人多,搜个中文报错都能找到答案。

pom.xml里的核心依赖我这里贴一份关键部分的参考:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

Redis和MQ的连接配置在application.yml里,核心就是设置好地址、端口、密码这些。Redis的配置有一点经验分享:lettuce是SpringBoot 2.x默认的Redis客户端,比Jedis性能好,不要乱换。

2.2 数据库表设计:字段不啰嗦,索引要给力

做抢票系统,数据库表设计直接决定了后续代码好不好写。别把表搞得太花哨,核心就这几张:

用户表(user):id、手机号、昵称、密码(加密存储)、创建时间。字段少,基本就是标准用户信息。

场次表(session):id、演唱会名称、演出时间、场馆、总票数、开抢时间、状态(未开始/抢票中/已结束)。这张表的status字段特别重要,秒杀系统里必须有一个开关状态,防止用户在非开抢时间通过接口抢票。

票档表(ticket_type):id、场次id、档位名称(看台/内场/VIP)、原价、售价、总库存(实际数量来自Redis)、剩余库存(这个字段可以保留给后台管理查询用,真实扣减以Redis为准)。

订单表(order):id、订单号、用户id、场次id、票档id、数量、总金额、状态(待支付/已支付/已取消/已退款)、创建时间、支付时间。订单号需要唯一索引,因为订单号是幂等判断的核心依据。

支付记录表(payment_record):id、订单号、支付流水号、支付渠道、支付金额、支付状态、回调时间。这张表主要是做支付回调的幂等处理用的,防止支付平台重复通知导致订单状态被覆盖。

建表的时候有两个容易疏忽的点,我必须单独提醒。一个是所有涉及查询的字段都要建索引:order表的user_id、session_id,payment_record表的order_id,这些都是高频查询路径。另一个是金额字段用DECIMAL(10,2),千万别用FLOAT或者DOUBLE,浮点数算钱是新手最爱犯的错,账目会差得离谱。

2.3 库存预热与Redis数据结构选择

库存放在Redis里,但不是开抢前才放进去的,你想想,如果50万用户同时来抢,服务端还要实时去数据库读库存再写缓存,那缓存层就没意义了。所以要在开抢前把所有票档的库存提前加载到Redis里。

这个环节我叫它“库存预热”。做法很简单,开启一个SpringBoot启动后的监听器,或者用@PostConstruct注解,在应用启动完成后自动执行一次库存加载方法。伪代码大致是:

@PostConstruct public void initStock() { List<TicketType> ticketTypes = ticketTypeMapper.selectList(null); ticketTypes.forEach(item -> { String key = "ticket:stock:" + item.getId(); stringRedisTemplate.opsForValue().set(key, String.valueOf(item.getTotalStock())); }); }

所有票档的库存键都统一用ticket:stock:{ticketTypeId}这种格式,方便管理。等真正开抢时,Redis里已经有库存了,抢票接口直接操作这个键。

这里有一个结构选型问题。为什么库存用String类型的键,而不是Hash或者List?因为库存扣减是一个数值变化操作,String类型配合Lua脚本里的DECR或GET指令最方便。Hash虽然能一次性管理多个字段,但Lua脚本里取值和扣减要多一层操作,没必要。List类型适合做队列,不适合做计数。

2.4 防超卖核心:Redis Lua脚本原子扣减

这是整个系统的技术灵魂,我单独拿出来讲透。

Lua脚本的好处在于,Redis是单线程处理脚本的,脚本执行期间不会有其他命令插进来,所以“判断库存+扣减库存”这个组合操作是原子性的,不会出现并发问题。

我的扣减脚本长这样:

-- KEYS[1]: 库存key -- ARGV[1]: 扣减数量 local stock = redis.call('GET', KEYS[1]) if not stock then return -1 end if tonumber(stock) < tonumber(ARGV[1]) then return 0 end redis.call('DECRBY', KEYS[1], ARGV[1]) return 1

这里三个返回值非常直观:-1表示库里没有这个key(说明库存没预热),0表示库存不足,1表示扣减成功。种判断逻辑完全放在Redis端执行,性能极高,而且多线程环境下不会出现同时读到相同库存的问题。

SpringBoot端的调用方式用Spring Data Redis提供的DefaultRedisScript:

@Autowired private StringRedisTemplate stringRedisTemplate; private static final DefaultRedisScript<Long> STOCK_SCRIPT = new DefaultRedisScript<>(); static { STOCK_SCRIPT.setLocation(new ClassPathResource("stock.lua")); STOCK_SCRIPT.setResultType(Long.class); } public boolean deductStock(Long ticketTypeId) { String key = "ticket:stock:" + ticketTypeId; Long result = stringRedisTemplate.execute(STOCK_SCRIPT, Collections.singletonList(key), "1"); return Long.valueOf(1).equals(result); }

这段代码是整个抢票接口调用的第一道关卡,也是防止超卖最核心的保证。压测数据我下面会放出来,效果非常明显。

3. 实操过程与核心环节实现

3.1 抢票接口完整流程:从参数校验到结果返回

抢票接口不能只做库存扣减,完整的链路必须包含业务校验、防重、限流、扣减、订单异步处理这几个环节。下面是我实际项目里的接口逻辑,按步骤拆开:

接口路径定义:POST /api/seckill,参数传sessionId(场次id)、ticketTypeId(票档id)、userId(从token里解析,不推荐前端传)。

第一步,参数校验。sessionId和ticketTypeId不能为空,userId不能为空,这些基础校验不通过直接返回错误。第二步,校验场次状态。从缓存或数据库查当前场次的status,必须是“抢票中”状态,还没开抢或者已经结束都不能继续。第三步,接口限流。用Redis做简单的计数器限流,比如固定窗口内单个用户只能请求N次,或者全局限流。第四步,校验用户是否已经抢过这个场次的票。这里用Redis的Set或者String记录,键格式user:bought:{sessionId}:{userId},如果已经存在就返回“每人限购一张”,防止同一个用户重复下单。第五步,执行上面的Lua脚本扣减库存。第六步,如果扣减成功,准备发送消息到MQ,然后返回“抢票成功,请尽快支付”;如果扣减失败,返回“很遗憾,票已售罄”。

这里有一个细节必须注意:用户重复抢票的判断必须放在库存扣减之前还是之后?我的实践经验是放在扣减之前。因为如果先扣了库存再判断用户买过,那就会导致退票逻辑,而且要补偿库存,非常麻烦。先判断用户是否已购,通过了再去扣库存,逻辑链路更清爽。

3.2 消息队列异步下单:把压力从主链路剥离

扣减库存成功之后,不能直接去数据库插入订单。因为MySQL的事务提交是有成本的,抢票高峰期每秒可能几千个订单,全部同步落库会把数据库压垮。正确做法是:把订单数据封装成消息发到RabbitMQ,由消费者异步去写订单表。

这里我定义了一个消息对象SeckillMessage,包含userId、sessionId、ticketTypeId等字段。生产者发送消息之前,先检查交换机是否已声明。发送代码大致如下:

rabbitTemplate.convertAndSend("seckill.exchange", "seckill.order", message);

消费者端监听队列,收到消息后调用订单服务创建订单。这里有一个关键点:消费者创建订单时不能直接信任消息内容,必须重新查询Redis库存扣减记录做校验。为什么?因为消息是异步的,如果消费失败要重试,重试时不能重复创建订单。我的做法是在创建订单前先查一次Redis里是否有该用户的抢购成功标记,这个标记是在扣减库存成功时用同一个Lua脚本里加上的。有标记才创建订单,创建成功后删除标记。这样即使消息重复投递,也不会生成两条订单。

异步下单的好处是响应速度快,用户点击抢票后几乎立即能收到结果,不用等数据库落盘。缺点是用户收到“抢票成功”后,订单可能还没建好,所以前端页面展示的是“已锁定,请支付”,等几秒刷新就会出现订单信息。

3.3 分布式锁的使用边界:什么时候真正需要它

很多人一听说抢票系统就想到Redis分布式锁,张口就是Redisson、ZK分布式锁。但实际上在库存扣减这个环节,Lua脚本已经把原子性问题解决了,根本不需要再用分布式锁锁库存操作。分布式锁在这个系统里更多的是用在“防止重复下单”这个场景。

具体来说,当消费者创建订单时,同一个用户的两个请求可能因为网络延迟等问题被重复消费,这时需要在创建订单前获取一把分布式锁,锁的key是order:lock:{userId}:{sessionId},锁住之后其他请求就不能同时创建这个用户的同场次订单。代码上用RedisTemplate的setIfAbsent方法实现一个简易锁,配合过期时间防止死锁:

Boolean locked = stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(30)); if (Boolean.TRUE.equals(locked)) { try { // 执行订单创建逻辑 } finally { stringRedisTemplate.delete(lockKey); } }

我这里只用了30秒过期,是因为订单创建是快速操作,30秒足够。超过30秒还锁着,说明程序有异常,让它自动释放总比死锁强。简单说,这个系统里用到锁的地方不多,如果你在答辩里把“Redis分布式锁防止库存超卖”挂在嘴边,老师一问Lua脚本你就露馅了。正确讲法是:Redis+Lua保证库存原子扣减,分布式锁保证订单创建的幂等性。

3.4 限流与风控:黄牛脚本的克星

抢票系统不做限流,就是对正常用户的不公平。接口限流我用的是Redis计数器窗口算法,思路很朴素:同一个用户ID在1秒内最多请求5次抢票接口,超过就直接拒绝,甚至可以把该用户拉入黑名单一段时间。

具体代码逻辑:

String key = "rate:limit:" + userId; Long count = stringRedisTemplate.opsForValue().increment(key); if (count == 1) { stringRedisTemplate.expire(key, Duration.ofSeconds(1)); } if (count > 5) { throw new BusinessException("操作太频繁,请稍后再试"); }

这就是一个简单的固定窗口限流。对于毕设来说这个方案足够,也容易在答辩时讲清楚。如果面试官追问滑动窗口和令牌桶的区别,你就说这个场景用固定窗口足矣,因为用户点击抢票的间隔本身就比较长,5次的阈值已经卡得很死,滑动窗口的精度优势在这个量级上体现不出来。

再往深一点,可以加一个IP维度的限流,用Nginx的limit_req模块限制同一个IP的连接频率。这个配置很小但非常管用,Nginx配置片段:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; location /api/seckill { limit_req zone=mylimit burst=20 nodelay; proxy_pass http://backend_server; }

这个配置的意思是每个IP每秒最多10个请求,突发情况下可以放宽到20个但不排队等待。配合后端Redis限流,双保险。我在实战中测试过,加了Nginx限流之后,脚本刷票的请求量直接断崖式下降。

3.5 前后端交互与轮询设计

前端Vue页面在用户点击“抢票”后,会弹出“正在排队”的提示,然后向后端轮询抢票结果。为什么不直接用异步通知?因为HTTP长连接在后端没有做推送通道(比如WebSocket或SSE)的情况下,前端拿不到主动通知,轮询是最简单可靠的方式。

轮询接口定义为GET /api/seckill/result?sessionId=xxx&userId=xxx,后端从Redis查询该用户在这个场次的抢票状态。抢票状态在扣库存成功时写入Redis,比如seckill:result:{sessionId}:{userId},值为“成功”或“失败”。前端每2秒轮询一次,最多轮询10次就停止,避免无效请求过多占用服务器资源。

4. 常见问题与排查技巧实录

4.1 Redis库存扣成负数了,怎么回事

这是我第一次测试时真的遇到过的问题。当时的扣减逻辑没有用Lua脚本,而是Java代码里先GET判断再DECR,两个操作之间不是原子的。并发一上来,20个线程同时GET到库存都是1,然后全部DECR,库存直接变成-19。

解决办法就是把判断和扣减合并成一个Lua脚本,用Redis的原子性保证这两个操作之间不会被插入其他命令。改完之后我用JMeter压测,1000个并发线程抢100张票,最终Redis里库存正好是0,订单数也正好是100张,一张不多一张不少。这个压测结果当时就让我松了一口气,也是整个项目最关键的验证环节。

4.2 用户收到“抢票成功”但没有订单记录

这个是异步下单模式下的典型问题。出现原因通常是消费者消费消息时抛了异常,比如数据库连接闪断、订单号生成冲突等,消息进入死信队列而没有被正确处理。

我的排查思路是三步:第一步,看RabbitMQ的管理后台里死信队列中是否有堆积消息;第二步,看消费者日志中是否有异常堆栈;第三步,拿到消费失败的消息体,手动重放一次,看具体是哪个环节报错。一般来说,消息里带了个空字段或者数据库字段长度不够,是最常见的两个坑。解决方案是在消费者里增加异常重试机制,比如Spring AMQP自带的@Retryable注解,重试3次仍失败就记录日志并转人工处理。

值得强调的是,因为下单是异步的,必须在前端页面上明确提示用户“支付前请先刷新查看订单状态”,否则用户会以为系统吞了他的钱和票,体验非常糟糕。

4.3 Redis宕机了怎么办,系统还能用吗

这个问题在答辩时被老师问的概率极高。我的回答是:抢票系统对Redis有强依赖,Redis宕机意味着库存数据不可用,系统为了保证数据一致性,会直接熔断抢票接口返回“系统繁忙”,同时后台通过监控告警通知运维快速恢复Redis。为什么不能降级到数据库扣库存?因为数据库方案扛不住并发,一旦降级反而可能把数据库打崩,造成更大故障。

所以我的设计里有一个Redis连通性检测的定时任务,每5秒ping一次Redis,连续3次失败就自动把场次状态置为“暂停抢票”,页面显示“系统维护中”,这是典型的“快速失败”策略。另外Redis本身要做持久化,开启RDB和AOF双持久化,这样即使宕机重启,库存数据也不会丢失太多。

4.4 高并发下数据库连接池被占满

刚开始做的版本里,查询订单、查询场次信息这些操作都直接访问数据库,压测到2000并发时,HikariCP连接池直接打满,接口响应时间从50ms飙升到10秒,紧接着就是404和超时。

解决思路是把高频读操作全部改成走Redis缓存。场次信息、票档余票数这些变化频率低或者可以容忍短暂延迟的数据,全部在做完写操作后主动更新缓存。比如抢票扣减库存后,把票档的剩余库存从Redis里直接查最新值同步更新到数据库,同时更新Redis里的场次信息缓存。这样绝大多数请求都是在打Redis,只有消息消费者才偶尔访问数据库。调整之后压测表现完全不一样,3000并发下数据库连接池占用率稳定在10%左右。

4.5 JMeter压测环境的搭建和参数选择

压测数据能证明你系统不是“纸面性能”,所以建议把这个环节做扎实。JMeter的配置其实很简单:线程组设置1000个线程(模拟1000个用户),Ramp-Up Period设置1秒(所有用户在1秒内同时发起请求),循环次数1次,这就模拟了瞬时高并发场景。加一个HTTP请求默认值,填入你的服务器IP和端口,请求路径填抢票接口。再加一个聚合报告监听器,就能看到平均响应时间、吞吐量、错误率这些关键指标。

切记:压测时本地电脑跑JMeter,SpringBoot服务和Redis数据库都要部署在一台独立的服务器上,不能全挤在本地跑,否则网络和资源竞争会严重影响测试数据。我当时用的是一台4核8G的云服务器,压测结果供参考:1000并发下扣库存接口平均响应时间13ms,吞吐量每秒2200次请求,错误率为0%。这个成绩在答辩上已经非常有说服力了。

4.6 技术亮点总结:答辩时这样讲

如果你拿这个项目去答辩或者面试聊项目,建议按“场景引出问题,方案给出答案,数据证明效果”的思路来组织。举一个例子,老师问你“库存怎么防超卖”,你回答:“我在Redis里用Lua脚本做库存扣减,脚本内先查库存是否充足,不足返回0,充足则原子扣减,靠Redis单线程执行脚本的特性避免并发数据竞争。我用JMeter压了1000并发,100张票最终成交100单,没有超卖,且有压测数据截图。”这种带着数据回答的方式,比背概念要加分得多。

另外主动提一下你在项目里做过的取舍也非常加分。比如你可以说“我额外考虑了DB和缓存的一致性”,具体做的是:下单直接操作Redis,订单表里记录Redis扣减的流水号,定时任务每分钟扫描订单表,对缺失流水号的异常单进行库存补偿。这种不完美但可控的设计,才是工程实践里的常态。

5. 项目部署与上线经验补充

5.1 jar包打包与服务器部署实战

项目跑通之后,部署是最后一道关卡。SpringBoot项目最直接的部署方式就是打成可执行jar包,在服务器上直接用java -jar启动。打包前先确认pom里配了spring-boot-maven-plugin,然后执行:

mvn clean package -DskipTests

打包完成后target目录下会生成一个xxx.jar文件,一般有50MB左右。把这个jar传到服务器上,用nohup命令后台启动:

nohup java -jar -Xms512m -Xmx1024m seckill-system.jar > app.log 2>&1 &

这里-Xms和-Xmx是JVM堆内存设置,云服务器4G内存的话推荐512M到1G,不要贪大,留足够内存给Redis和MySQL。启动后看看app.log日志输出,确认没报错基本就成功了。

5.2 Docker部署方式回顾(备选方案)

如果你不想在服务器上手动装JDK和MySQL,用Docker编排会方便很多。简单的方案是写一个docker-compose.yml,一次性把Redis、MySQL、RabbitMQ、应用服务全部启动。这一块最常用的经验是:Redis、MySQL、RabbitMQ用镜像直接拉取,SpringBoot服务用Dockerfile构建。Dockerfile参考:

FROM openjdk:8-jdk-alpine WORKDIR /app COPY seckill-system.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

用docker-compose编排时注意服务启动顺序,应用要依赖数据库和Redis,所以加depends_on配置,但最好在应用内部做重试连库,因为depends_on只保证容器启动了,不代表数据库已经就绪。

5.3 配置里的坑:你一定要避开的

第一坑:Redis密码不要写在代码里,放在application.yml的配置文件中,用环境变量占位符${REDIS_PASSWORD}形式引用。第二坑:RabbitMQ的消费者在分布式部署时注意,如果同一个队列有多个消费者实例,默认是轮询分发而不是广播,这个特性在做幂等设计时要考虑到。第三坑:服务器时间校准也很关键,抢票接口判断开抢时间用的是服务器时间,如果服务器时间不准,开票时间就会出问题。具体操作是安装ntpdate并定时同步时间,这个细节很多教程都不提,但线上一旦出问题就是大事。

6. 项目迭代方向与学习建议

这个抢票系统做出来的版本是一个标准的单体应用,但它的架构思路可以继续扩展。我列几个后续迭代方向,学生朋友可以根据自己精力选做:第一个方向是引入Sentinel做更细粒度的熔断限流,相比Redis计数器限流,Sentinel的滑动窗口和熔断降级更专业,面试讲出来更有深度。第二个方向是把订单服务、用户服务拆开做微服务化,用OpenFeign做远程调用,用Nacos做注册中心,这就能把话题引向微服务。第三个方向是增加WebSocket推送,抢票成功之后通过WebSocket实时通知用户,省掉前端的轮询逻辑。

如果时间有限,以我的实战经验来看,最值得投入的优化是压测数据记录。多测几组数据,比如500并发、1000并发、2000并发,记录响应时间和错误率,画个表格放到论文里或者答辩PPT里,比任何文字描述都有说服力。

学习路径上给一个真诚的建议:这个系统涉及的知识点比较多,如果你对Redis操作还不熟,先把Redis的五种基本数据结构玩熟再动手;如果你对RabbitMQ的交换机、队列、路由键还不理解,先去搭个简单的发送接收Demo。基础打牢了,后面写代码就是顺畅的流水线,不会卡壳。

做这类项目最忌讳的就是照抄代码不求甚解。把抢票接口的每一步都在脑子里过一遍——“为什么这个操作要先做限流”“为什么这里要用异步”“为什么库存不在代码里判断而在Lua里判断”,这些“为什么”想通了,答辩问不倒你,面试官也看到了你的思考深度。如果这篇文章能帮你把这些问题梳理清楚,那我的目的就达到了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询