Spring Boot 3.2旅游系统实战:Redis+MySQL库存防超卖与Saga分布式事务

发布时间:2026/10/29 19:53:50

Spring Boot 3.2旅游系统实战:Redis+MySQL库存防超卖与Saga分布式事务 简介本资源是一套基于Java技术栈开发的旅游系统网站完整源码面向Java Web初学者与全栈开发学习者旨在通过真实业务场景掌握SSMSpringSpringMVCMyBatis框架整合、前后端协同及数据库设计等核心能力。压缩包共725个文件大小7.94MB包含111个JSP页面实现动态服务端渲染、415个GIF含界面动效与操作示意、69个JPG景点与UI素材、40个CSS含Bootstrap样式与自定义主题、23个JSjQuery交互逻辑、5个DB文件SQLite数据库实例以及Java源码、配置文件和项目元数据结构完整开箱即用。已有1510人学习下载资源涵盖用户注册登录、景点展示、线路查询、订单管理等典型模块代码注释清晰目录层次分明配套数据库脚本与部署说明齐全适合用于课程设计、毕业设计参考或SSM框架实战训练。1. 用 Java 搭建旅游系统网站不是写个 Servlet 就叫“系统”而是让景点、订单、用户三者在真实业务流里稳住不翻车你见过太多“Java 旅游系统课程设计”——首页放几张轮播图点进去跳转到硬编码的景点列表点击预订弹出一个alert(已下单)后台用ArrayListBooking存数据重启 Tomcat 全丢。这不是旅游系统这是交互式 PPT。真正的旅游系统网站要扛住节假日瞬时并发查线路、锁库存、生成电子凭证要让地接社能按区域筛团期让游客能实时看到余位变化让财务能按天对账订单状态流转是否完整。它不是 Java 语法练习场而是 Spring Boot MyBatis-Plus Redis MySQL 的协同战场是事务边界划在哪、缓存穿透怎么防、超卖怎么拦的实战沙盘。适合刚脱离单机 Demo、正卡在「能写 CRUD 却不敢上线」临界点的 Java 开发者——本文不讲“什么是 MVC”只告诉你当用户凌晨三点抢敦煌莫高窟预约名额时你的代码哪一行该加Transactional(timeout 30)哪一行该抛IllegalStateException而不是NullPointerExceptionRedis 缓存键为什么必须带业务前缀而非直接set(spots)。我们从零跑通一个可部署、可调试、可扩缩的最小可行旅游系统网站所有代码基于 JDK 17 Spring Boot 3.2 MyBatis-Plus 4.3拒绝过时的 XML 配置和手动 JDBC。2. 用 Spring Boot 3.2 快速搭建旅游系统骨架选型理由与模块划分逻辑旅游系统不是功能堆砌而是业务域拆解。我坚持把系统拆成四个核心模块user-center用户认证与权限、spot-service景点/线路管理、order-service订单生命周期、payment-gateway支付对接模拟。这种拆法不是为微服务而微服务而是为了后续能独立压测——比如节前重点压测spot-service的余位查询接口而不必连带启动整个用户登录链路。Spring Boot 3.2 是当前生产环境最稳妥的选择它原生支持 Jakarta EE 9告别javax.*包名迁移之痛内置 GraalVM 原生镜像支持虽本次不用但预留升级路径且对 Lombok 1.18.30、MyBatis-Plus 4.3 的兼容性经过大量项目验证。相比 Spring Boot 2.7它强制要求 JDK 17这反而帮我们规避了LocalDateTime时区处理的老坑——所有时间字段统一用java.time.LocalDateTime数据库字段类型设为datetime非timestamp彻底避开 MySQL 时区自动转换的玄学问题。2.1 初始化工程Maven 多模块结构与依赖锁定我采用 Maven 多模块结构根 POM 定义统一版本避免子模块各自声明导致的依赖冲突。关键点在于不依赖 Spring Initializr 默认模板手动控制依赖范围。例如Web 层只引入spring-boot-starter-web绝不加spring-boot-starter-tomcat内嵌容器已包含数据层显式排除 HikariCP 的旧版 transitive 依赖强制使用com.zaxxer:HikariCP:5.0.1修复了连接池在高并发下偶发的Connection is not available问题。!-- 根 pom.xml -- properties java.version17/java.version spring-boot.version3.2.7/spring-boot.version mybatis-plus.version4.3.2/mybatis-plus.version lombok.version1.18.30/lombok.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency !-- 显式导入 MyBatis-Plus BOM -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-bom/artifactId version${mybatis-plus.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement提示mybatis-plus-bom的作用是统一管理 MP 及其所有依赖如mybatis-spring-boot-starter、mybatis-plus-core的版本避免手动指定多个依赖时出现版本错配。这是 MyBatis-Plus 3.5 推荐的依赖管理方式比老式exclusions更可靠。2.2 模块职责与包结构约定让新同事 5 分钟看懂代码归属每个模块的包结构严格遵循com.example.tour.[module].[layer]例如spot-service模块的实体类放在com.example.tour.spot.domain.entityDTO 放在com.example.tour.spot.interfaces.dtoController 放在com.example.tour.spot.interfaces.web。这种结构看似繁琐但极大降低协作成本——当发现SpotVO字段缺失时开发者能立刻定位到spot-service的 VO 类而非在user-center或order-service里大海捞针。特别强调所有跨模块调用必须走定义好的 API 接口禁止直接 new 对象或注入其他模块的 Service。例如order-service需要获取景点信息必须通过 Feign Client 调用spot-service的/api/spots/{id}接口而非Autowired SpotService spotService。这为未来拆分为独立服务留出平滑演进路径。2.3 数据库初始化用 Flyway 管理 SQL 脚本而非手动执行旅游系统涉及多张关联表景点、线路、订单、用户手动建表易遗漏外键约束或索引。我选用 Flyway而非 Liquibase管理数据库变更因其轻量、社区活跃、与 Spring Boot 集成简单。在spot-service模块中创建src/main/resources/db/migration/V1__init_spot_schema.sql-- V1__init_spot_schema.sql CREATE TABLE t_spot ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(100) NOT NULL COMMENT 景点名称, city varchar(50) NOT NULL COMMENT 所在城市, max_capacity int NOT NULL DEFAULT 0 COMMENT 最大承载量, current_stock int NOT NULL DEFAULT 0 COMMENT 当前余位, status tinyint NOT NULL DEFAULT 1 COMMENT 状态0-下架1-上架, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_city_status (city, status) COMMENT 按城市状态查询加速 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点表; INSERT INTO t_spot (name, city, max_capacity, current_stock, status) VALUES (敦煌莫高窟, 敦煌, 6000, 6000, 1), (九寨沟风景区, 阿坝, 3000, 3000, 1), (西湖断桥, 杭州, 10000, 10000, 1);Flyway 会自动按文件名顺序执行脚本并在flyway_schema_history表中记录执行状态。好处是本地开发、测试环境、生产环境使用同一套脚本杜绝“我在本地能跑上线就报表不存在”的低级错误。注意V1__前缀表示版本号后续新增变更只需添加V2__add_spot_category.sqlFlyway 自动识别并执行。3. 实现景点余位实时查询与扣减MySQL 行锁 Redis 缓存双保险旅游系统最核心的业务场景是“查余位 → 锁库存 → 生单”这三步必须原子化。常见错误是先SELECT current_stock FROM t_spot WHERE id ?再UPDATE t_spot SET current_stock current_stock - 1 WHERE id ? AND current_stock 0—— 这种写法在高并发下必然超卖。正确做法是一条 SQL 完成校验与扣减并用SELECT ... FOR UPDATE显式加行锁。但纯数据库方案有性能瓶颈每秒上千次余位查询会压垮 MySQL。因此必须引入 Redis 缓存形成“缓存兜底 DB 最终一致”的双保险架构。3.1 Redis 缓存设计Key 命名规范与失效策略缓存 Key 必须携带业务上下文避免不同模块 Key 冲突。我采用tour:spot:stock:{spotId}格式例如tour:spot:stock:1。Value 存储 JSON 字符串包含currentStock和version用于乐观锁比对{ currentStock: 5999, version: 1234567890 }缓存失效策略采用主动更新 被动淘汰组合主动更新每次UPDATE t_spot成功后立即SET tour:spot:stock:{id} {json} EX 3005 分钟过期被动淘汰当缓存未命中时从 DB 查询并回填缓存同时设置相同过期时间。注意绝不使用DEL key清缓存因为DEL是阻塞操作高并发下会导致 Redis 线程卡顿。应改用EXPIRE key 1立即过期让下次读取触发回源。3.2 扣减余位的原子操作MySQL 行锁 Redis Lua 脚本扣减操作必须保证“DB 更新成功 ↔ 缓存更新成功”否则出现 DB 余位已扣但缓存仍是旧值导致后续请求误判有余位。解决方案是用 Redis Lua 脚本将缓存读取、校验、更新打包为原子操作再由 MySQL 事务兜底。以下是decreaseStock.lua脚本-- decreaseStock.lua local spotId KEYS[1] local expectedStock tonumber(ARGV[1]) local newStock tonumber(ARGV[2]) -- 1. 获取当前缓存值 local cacheJson redis.call(GET, tour:spot:stock: .. spotId) if not cacheJson then return -1 -- 缓存不存在需回源 end local cacheData cjson.decode(cacheJson) if cacheData.currentStock ~ expectedStock then return -2 -- 库存不匹配发生并发修改 end -- 2. 更新缓存 cacheData.currentStock newStock cacheData.version cacheData.version 1 redis.call(SET, tour:spot:stock: .. spotId, cjson.encode(cacheData), EX, 300) return 1Java 调用逻辑// SpotService.java public boolean decreaseStock(Long spotId, Integer expectedStock) { String cacheKey tour:spot:stock: spotId; // 1. 尝试 Lua 脚本原子扣减缓存 Long result (Long) redisTemplate.execute( decreaseStockScript, Collections.singletonList(spotId.toString()), String.valueOf(expectedStock), String.valueOf(expectedStock - 1) ); if (result 1L) { return true; // 缓存扣减成功 } else if (result -1L) { // 缓存不存在走 DB 扣减流程 return decreaseStockByDb(spotId, expectedStock); } else { // -2库存不匹配说明其他线程已修改业务层重试或返回失败 throw new IllegalStateException(库存校验失败可能已被其他请求占用); } } private boolean decreaseStockByDb(Long spotId, Integer expectedStock) { // 2. DB 扣减SELECT ... FOR UPDATE 确保行锁 Spot spot spotMapper.selectById(spotId); if (spot.getCurrentStock() ! expectedStock || spot.getStatus() ! 1) { return false; } // 使用 MyBatis-Plus 的 update 方法自动生成 WHERE 条件 UpdateWrapperSpot wrapper new UpdateWrapper(); wrapper.eq(id, spotId) .eq(current_stock, expectedStock) // 乐观锁条件 .eq(status, 1); Spot updateSpot new Spot(); updateSpot.setCurrentStock(expectedStock - 1); updateSpot.setUpdatedTime(LocalDateTime.now()); boolean updated spotMapper.update(updateSpot, wrapper) 0; if (updated) { // DB 更新成功同步刷新缓存 refreshSpotCache(spotId); } return updated; }关键细节wrapper.eq(current_stock, expectedStock)是数据库层面的乐观锁确保只有当前余位等于预期值时才更新。这比单纯WHERE id ? AND current_stock 0更精确——后者无法防止两个请求同时读到current_stock100然后都扣减为99造成超卖。3.3 余位查询接口缓存穿透防护与空值缓存前端频繁调用/api/spots/{id}/stock查询余位若恶意请求不存在的spotId如id9999999会导致大量请求穿透到 DB拖慢整体性能。解决方案是布隆过滤器 空值缓存布隆过滤器在应用启动时加载所有有效spotId到内存布隆过滤器使用guava:32.0.0-jre的BloomFilter查询前先mightContain(id)若返回false直接返回0空值缓存若布隆过滤器返回true但 DB 查无此 ID则向 Redis 写入tour:spot:stock:9999999值为{currentStock:0,version:0}过期时间设为60秒短于正常缓存避免长期占用内存。// SpotController.java GetMapping(/{id}/stock) public ResultInteger getStock(PathVariable Long id) { // 1. 布隆过滤器快速拦截 if (!spotBloomFilter.mightContain(id)) { return Result.success(0); } // 2. 查询缓存 String cacheKey tour:spot:stock: id; String cacheJson redisTemplate.opsForValue().get(cacheKey); if (cacheJson ! null) { try { JSONObject obj new JSONObject(cacheJson); return Result.success(obj.getInt(currentStock)); } catch (JSONException e) { // JSON 解析失败视为缓存脏数据删除后回源 redisTemplate.delete(cacheKey); } } // 3. 回源查询 DB Spot spot spotMapper.selectById(id); if (spot null || spot.getStatus() ! 1) { // 空值缓存写入 {currentStock:0,version:0} redisTemplate.opsForValue().set( cacheKey, {\currentStock\:0,\version\:0}, Duration.ofSeconds(60) ); return Result.success(0); } // 4. 回填缓存并返回 String stockJson String.format( {\currentStock\:%d,\version\:%d}, spot.getCurrentStock(), System.currentTimeMillis() ); redisTemplate.opsForValue().set(cacheKey, stockJson, Duration.ofSeconds(300)); return Result.success(spot.getCurrentStock()); }4. 订单状态机与分布式事务用 Saga 模式替代两阶段提交旅游订单涉及多个服务spot-service扣减余位、user-center扣减用户余额、payment-gateway发起支付、order-service更新订单状态。传统Transactional只能保证单库事务跨服务必须引入分布式事务。但旅游系统对一致性要求并非强一致允许几秒延迟且支付环节天然具备幂等性因此我选择Saga 模式每个服务提供正向操作如createOrder和补偿操作如cancelOrder由order-service作为协调者驱动状态流转。相比 Seata 的 AT 模式Saga 更轻量、无全局锁、易于理解。4.1 订单状态定义与流转规则旅游订单状态不是简单的CREATED/PAYED/FINISHED而是细化到业务动作节点状态码状态名触发条件后续动作10待支付用户提交订单后调用支付网关发起支付20支付中支付网关返回PROCESSING启动定时任务轮询支付结果30已支付支付网关回调SUCCESS扣减景点余位40出行准备余位扣减成功生成电子凭证50已完成用户确认行程结束结算分佣-10已取消用户主动取消或超时未支付释放景点余位状态流转必须严格遵循规则10→20→30→40→50或10→-10禁止跳转如10→30。MyBatis-Plus 的TableField(fill FieldFill.INSERT_UPDATE)无法满足此需求因此我在Order实体中定义status字段为Integer并在 Service 层用switch显式校验// OrderService.java public void updateStatus(Long orderId, Integer targetStatus) { Order order orderMapper.selectById(orderId); if (order null) { throw new IllegalArgumentException(订单不存在 orderId); } // 状态校验只允许合法流转 boolean validTransition switch (order.getStatus()) { case 10 - targetStatus 20 || targetStatus -10; case 20 - targetStatus 30 || targetStatus -10; case 30 - targetStatus 40; case 40 - targetStatus 50; default - false; }; if (!validTransition) { throw new IllegalStateException( String.format(非法状态流转%d → %d, order.getStatus(), targetStatus) ); } // 更新状态 UpdateWrapperOrder wrapper new UpdateWrapper(); wrapper.eq(id, orderId).eq(status, order.getStatus()); // 乐观锁 Order updateOrder new Order(); updateOrder.setStatus(targetStatus); updateOrder.setUpdatedTime(LocalDateTime.now()); if (orderMapper.update(updateOrder, wrapper) 0) { throw new OptimisticLockException(订单状态更新失败可能已被其他操作修改); } }4.2 Saga 协调器实现本地消息表 定时扫描Saga 协调器不依赖外部消息中间件如 RocketMQ而是用本地消息表保证可靠性。在order-service数据库中新建t_order_saga_event表CREATE TABLE t_order_saga_event ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL COMMENT 订单ID, service_name varchar(50) NOT NULL COMMENT 目标服务名, action varchar(20) NOT NULL COMMENT 动作CREATE/CONFIRM/CANCEL, payload text COMMENT JSON 参数, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待发送1-已发送2-已确认3-已失败, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, retry_count int NOT NULL DEFAULT 0 COMMENT 重试次数, PRIMARY KEY (id), KEY idx_order_status (order_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;当用户提交订单时order-service执行创建订单记录状态10插入t_order_saga_event记录service_namepayment-gateway,actionCREATE,status0启动异步线程扫描status0的事件调用支付网关POST /api/payments支付网关返回成功后更新事件status1并触发下一步扣减余位。// OrderSagaCoordinator.java Transactional public void triggerSaga(Long orderId) { // 1. 创建初始事件调用支付网关 SagaEvent event new SagaEvent(); event.setOrderId(orderId); event.setServiceName(payment-gateway); event.setAction(CREATE); event.setPayload({\amount\:19900,\currency\:\CNY\}); event.setStatus(0); sagaEventMapper.insert(event); // 2. 异步触发避免阻塞主事务 CompletableFuture.runAsync(() - { // 定时扫描逻辑select * from t_order_saga_event where status 0 limit 100 ListSagaEvent pendingEvents sagaEventMapper.selectList( new QueryWrapperSagaEvent().eq(status, 0) ); for (SagaEvent e : pendingEvents) { try { // 调用远程服务 String response restTemplate.postForObject( http://payment-gateway/api/payments, e.getPayload(), String.class ); // 更新事件状态 e.setStatus(1); sagaEventMapper.updateById(e); // 触发下一步扣减余位 if (CREATE.equals(e.getAction())) { triggerDecreaseStock(e.getOrderId()); } } catch (Exception ex) { // 重试机制更新 retry_count若超过 3 次则标记为失败 e.setRetryCount(e.getRetryCount() 1); if (e.getRetryCount() 3) { e.setStatus(3); } sagaEventMapper.updateById(e); } } }); }提示Saga 的补偿操作如支付失败后释放余位同样写入t_order_saga_eventactionCANCEL由同一扫描线程执行。这种设计将分布式事务的复杂度收敛到单个服务内避免引入额外中间件运维成本。4.3 支付结果异步通知幂等性校验与状态闭环支付网关如模拟的微信支付通过 HTTP POST 向order-service的/api/callback/payment接口推送结果。该接口必须做到绝对幂等同一笔支付通知重复推送 100 次订单状态只变更一次。实现方式是以out_trade_no商户订单号为唯一键插入t_payment_callback_log表利用数据库唯一索引拦截重复。CREATE TABLE t_payment_callback_log ( id bigint NOT NULL AUTO_INCREMENT, out_trade_no varchar(64) NOT NULL COMMENT 商户订单号, notify_id varchar(64) NOT NULL COMMENT 支付平台通知ID, status varchar(20) NOT NULL COMMENT 支付状态SUCCESS/FAILED, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_out_trade_no (out_trade_no) -- 唯一索引防重 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;// PaymentCallbackController.java PostMapping(/callback/payment) public String handlePaymentCallback(RequestBody MapString, String params) { String outTradeNo params.get(out_trade_no); String notifyId params.get(notify_id); String tradeStatus params.get(trade_status); // 1. 幂等校验插入日志表唯一索引拦截重复 PaymentCallbackLog log new PaymentCallbackLog(); log.setOutTradeNo(outTradeNo); log.setNotifyId(notifyId); log.setStatus(tradeStatus); try { callbackLogMapper.insert(log); // 若唯一索引冲突抛 DuplicateKeyException } catch (DuplicateKeyException e) { return success; // 已处理过直接返回 success } // 2. 更新订单状态 if (TRADE_SUCCESS.equals(tradeStatus)) { orderService.updateStatus(Long.valueOf(outTradeNo), 30); // 支付成功 → 已支付 // 触发余位扣减 spotService.decreaseStock(...); } else { orderService.updateStatus(Long.valueOf(outTradeNo), -10); // 支付失败 → 已取消 } return success; }5. 避坑指南旅游系统开发中踩过的 4 个血泪坑旅游系统上线前的压测和灰度阶段我连续三天没睡好就为解决这几个看似简单实则致命的问题。它们不是教科书里的理论陷阱而是真实业务流量打过来时让你凌晨三点爬起来改代码的“玄学”。5.1 现象节假日期间景点余位显示为负数但数据库current_stock字段却是正数原因Redis 缓存与 MySQL 数据库未最终一致且缓存更新逻辑存在竞态。具体是decreaseStockByDb()方法中DB 更新成功后调用refreshSpotCache(spotId)但该方法是异步执行Async而此时另一个请求恰好读取缓存拿到的是旧值。更糟的是refreshSpotCache内部用了redisTemplate.opsForValue().set()而set操作本身不是原子的——它先DEL再SET中间存在极短时间窗口缓存为空导致请求穿透到 DBDB 返回current_stock100但缓存被设为100而实际 DB 已被其他请求扣减为99。解决废除异步刷新改为同步set且使用SET key value EX seconds原子命令。同时在decreaseStockByDb()中DB 更新后立即执行redisTemplate.opsForValue().set(cacheKey, newStockJson, Duration.ofSeconds(300))确保缓存与 DB 更新在同一事务内完成虽然 Redis 不参与 MySQL 事务但应用层保证顺序。5.2 现象用户支付成功后订单状态卡在支付中后台日志显示OptimisticLockException原因支付网关回调与用户主动查询订单状态的请求并发修改同一订单。支付回调线程调用updateStatus(orderId, 30)而用户在前端不断刷新/api/orders/{id}触发OrderService.getOrderDetail()该方法内部也调用了orderMapper.selectById()并可能更新last_viewed_time字段导致version字段被修改。当支付回调的UPDATE语句执行时WHERE status 20 AND version X条件不成立更新行数为 0抛出乐观锁异常。解决将订单状态字段与非状态字段分离。t_order表中status和version为状态专用字段last_viewed_time等非关键字段单独存入t_order_ext表。getOrderDetail()只查主表不触发t_order的version更新支付回调的updateStatus()仅更新status和version避免无关字段干扰乐观锁。5.3 现象Redis 缓存击穿大量请求涌向 DBCPU 突然飙升至 95%原因热点景点如故宫的缓存 Key 过期时间设为固定300秒当大量请求在同一毫秒内发现缓存失效全部穿透到 DB 执行SELECT ... FOR UPDATEMySQL 行锁排队导致响应延迟激增进而引发更多请求超时重试形成雪崩。解决对热点 Key 实施随机过期时间 逻辑过期。SET tour:spot:stock:1 {json} EX 300改为EX 280 random(0,40)使过期时间在280~320秒间随机同时 JSON 中增加expireAt字段应用层读取缓存后先判断expireAt now()若过期则尝试GETSET原子更新缓存失败则回源成功则继续使用旧值——这相当于给缓存加了一层“软过期”避免瞬时穿透。5.4 现象MySQL 慢查询日志中SELECT * FROM t_spot WHERE city ? AND status 1执行耗时 2.3 秒原因t_spot表数据量达 50 万行idx_city_status索引虽存在但city字段区分度极低全国就 300 多个地级市status只有两个值0/1复合索引选择性差MySQL 优化器放弃使用索引转为全表扫描。解决重构查询逻辑用覆盖索引 延迟关联。新建索引KEY idx_city_status_id (city, status, id)确保SELECT id FROM t_spot WHERE city ? AND status 1能走索引再用id列表批量SELECT * FROM t_spot WHERE id IN (1,2,3...)。虽然多一次查询但避免了大表全扫。同时在SpotController.listByCity()接口中对city参数做白名单校验只允许传入预设的热门城市并限制page_size 20从源头控制结果集大小。6. 验证系统健壮性的 3 个硬核技巧从本地压测到线上灰度写完代码只是开始旅游系统真正的考验在流量进来之后。我不会等用户投诉“抢不到票”才去查问题而是用一套组合拳在上线前就把潜在风险打掉。这些技巧不依赖昂贵的 APM 工具全是 Linux 命令 开源组件 业务日志的朴素组合。6.1 用 wrk 模拟真实购票链路不只是压接口而是压状态流转别只用ab或jmeter测单个/api/spots/1/stock接口。旅游系统的核心是状态流转闭环查余位 → 提交订单 → 支付回调 → 更新状态。我用wrk编写 Lua 脚本模拟真实用户行为-- buy_ticket.lua math.randomseed(os.time()) local spotIds {1, 2, 3} -- 敦煌、九寨、西湖 local token fake-token-123 function setup(thread) thread:set(token, token) end function init(args) print(Starting ticket purchase test...) end function request() local spotId spotIds[math.random(#spotIds)] local path /api/spots/ .. spotId .. /stock local headers { [Authorization] Bearer .. token, [Content-Type] application/json } return wrk.format(GET, path, headers) end function response(status, headers, body) if status ~ 200 then print(Error: .. status .. on spot .. spotId) end end执行命令wrk -t12 -c400 -d30s -s buy_ticket.lua http://localhost:8080。关键参数-t12启动 12 个线程-c400保持 400 并发连接-d30s持续 30 秒。观察点不是 QPS而是订单表t_order中status30已支付的记录数是否与支付回调日志匹配。若 DB 订单数远少于回调日志数说明支付回调丢失或状态更新失败——这暴露了 Saga 协调器的可靠性缺陷。6.2 用 Prometheus Grafana 监控三个黄金指标旅游系统不需要监控 100 个指标盯死以下三个即可Redis 缓存命中率rate(redis_keyspace_hits_total[5m]) / rate(redis_keyspace_hits_total[5m] redis_keyspace_misses_total[5m])低于 95% 说明缓存设计有问题MySQL 行锁等待时间avg(rate(mysql_global_status_innodb_row_lock_time_avg[5m]))超过 50ms 需检查SELECT ... FOR UPDATE是否锁定了过多行订单状态机异常率自定义指标order_status_transition_failure_total{status10-20}统计IllegalStateException日志数量若每分钟超过 5 次说明状态流转逻辑存在漏洞。Grafana 面板配置用Alert功能设置阈值告警例如“缓存命中率 90% 持续 2 分钟”触发企业微信通知。这比等用户反馈快 10 倍。6.3本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/29 19:43:25

Git - Linux离线安装

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/29 19:43:25

【无需VIP】机器码一键修改:3 步搞定机器码修改

【无需VIP】机器码一键修改:3 步搞定机器码修改 【免费下载链接】机器码修改工具 本仓库提供了一个强大的机器码修改工具,支持一键修改功能。无论您是开发者还是普通用户,都可以轻松使用此工具进行机器码的修改操作 项目地址: https://gitc…

2026/10/29 20:59:31

具身智能与大模型融合的技术实践与优化

1. 具身智能与大模型的融合革命 去年在调试一个机械臂抓取实验时,我遇到了一个典型问题:传统视觉算法对透明物体的识别准确率始终徘徊在60%左右。直到尝试将视觉信号输入到经过具身数据微调的大模型中,准确率一夜之间提升到92%——这就是具身…

2026/10/29 20:59:31

IT自动化运维平台建设:Zabbix告警、脚本任务与工单闭环

简介:《IT自动化运维平台建设解决方案》是一份面向运维工程师、运维主管及技术管理者的PPT方案,聚焦组织从人工运维向自动化、智能化演进中的平台规划与落地问题。文件共1个PPT,压缩包约4.35MB,已有1024人学习,适合用作…

2026/10/29 20:59:31

YOLOv8优化技术在混凝土裂缝检测中的应用实践

1. 项目背景与核心价值混凝土结构作为现代建筑的主要承重构件,其健康状态直接关系到建筑物的安全性和使用寿命。裂缝作为最常见的混凝土缺陷类型,往往是结构损伤的早期征兆。传统的人工巡检方式存在效率低、主观性强、高空作业风险大等问题,而…

2026/10/29 20:59:31

FPGA通过SPI配置ADC:4线/3线时序与Verilog实现避坑

简介:《FPGA通过SPI对ADC配置系列文章》是一份面向FPGA工程师、硬件开发者和高速ADC调试入门者的技术文档,围绕如何用FPGA经SPI接口完成ADC寄存器配置展开,兼顾协议基础与工程时序分析。压缩包共1个docx文件,约787KB,内…

2026/10/29 20:59:31

MCP 开发环境搭建:Mac 与 Windows 跑通 Python 服务和 stdio 客户端

MCP 实战第 2 篇。固定使用官方 Python SDK mcp2.3.0,在独立虚拟环境中完成安装、工具发现、实际调用和错误验证。服务只读取虚构缺陷数据,不需要大模型密钥,也不连接真实业务平台。 上一篇已经区分 MCP、API、插件与 Agent,并准备…

2026/10/29 20:54:29

服务器物理架构与带外管理:理解BMC、ECC和冗余设计的工程本质

简介:本资源是一份系统全面的服务器基础知识入门文档,面向IT运维初学者、系统管理员及备考认证的技术人员,旨在帮助读者建立对服务器硬件架构、核心组件与管理技术的完整认知框架。内容覆盖服务器基本概念、关键部件(CPU/内存/硬盘…

2026/10/29 17:05:52

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/29 17:06:02

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/29 17:06:13

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/29 0:03:42

学生选课系统DFD图绘制指南:边界划分与分层校验实战

简介:文档以顶层与第一层DFD图为核心,完整刻画学生选课系统的逻辑模型,面向软件工程课程设计、系统分析学习及毕设前期建模人群。顶层图界定系统边界,外部实体包括管理员、教师和学生三类角色;第一层图进一步细化用户登…

2026/10/29 0:03:42

JIRA敏捷落地:Scrum角色权限、Sprint与燃尽图避坑指南

简介:围绕“基于JIRA的敏捷开发项目管理”这一主题,面向产品负责人、Scrum Master及研发测试成员,梳理Scrum角色分工、迭代流程与JIRA工具配置的对应关系,适合初、中级团队导入敏捷管理时参考。压缩包共1个docx文件,大…

2026/10/29 0:03:42

SEMI S8人机工程验收实战:从条款拆解到现场实测的完整指南

简介:面向半导体设备设计、制造与运维工程师及安全合规人员,这份资料聚焦SEMI标准体系下的设备安全规范,重点收录SEMI S8-0308《半导体制造设备人体工程学工程安全指南》。内容围绕设备设计原则、错误与事故预防、人体工程学适配三大主线展开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑