news 2026/10/14 12:20:18

旅游系统为何首选Java+Vue+SpringBoot+MySQL技术栈

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
旅游系统为何首选Java+Vue+SpringBoot+MySQL技术栈

简介:这是一套基于Java全栈技术栈开发的在线旅游网站实战项目,面向Java与Vue初学者、毕业设计学生及Web开发入门者,解决旅游类Web系统从前后端分离架构到业务模块落地的完整学习需求。资源包为83.43MB的ZIP压缩文件,含前后端源码、数据库脚本、启动配置等核心内容,虽未提供具体文件总数与类型明细,但根据项目结构可推知包含SpringBoot后端模块(Controller/Service/Entity)、Vue3前端页面(线路分类、订单管理、用户中心等)、MySQL建表SQL及系统基础配置文件,模块划分清晰,便于分层理解与二次开发。已有51人学习下载,适合通过真实场景掌握旅游电商类系统的权限分层(管理员后台+用户前台)、典型业务流程(线路展示→下单→订单管理)及主流技术整合实践。配套提供B站项目录屏、启动教程与深度讲解视频,覆盖环境搭建、功能演示与关键代码解析,显著降低上手门槛。

1. 在线旅游网站为什么非得用 Java + Vue + SpringBoot + MySQL 这套组合:不是跟风,是业务压出来的技术选型

你刚接手一个在线旅游网站的重构任务,需求文档里写着“支持千万级景点数据检索”“用户并发下单峰值超 8000 TPS”“后台要能实时更新酒店库存与价格策略”——这时候如果有人建议你用 Node.js + React + MongoDB 从零搭,你大概率会皱眉。不是技术不行,而是这套组合在旅游类业务里容易踩坑:库存扣减的强一致性难保障、复杂多条件搜索(如“三亚+亲子+含早+评分4.8+价格低于600”)响应慢、订单状态机流转缺乏事务兜底。而 Java + Vue + SpringBoot + MySQL 这套组合,恰恰是在大量真实旅游系统迭代中被反复验证过的“稳态解”:SpringBoot 提供开箱即用的事务管理、连接池、缓存集成能力;MySQL 的行级锁 + MVCC 能扛住高并发下的库存争抢;Vue 的组件化让“航班筛选器”“酒店地图热力图”“行程生成器”这类强交互模块可独立开发、灰度上线;更重要的是,整套生态对“分布式事务补偿”“异地多活读写分离”“搜索与数据库双写一致性”这些旅游系统逃不开的难题,都有成熟中间件和落地模式。它不炫技,但能让你把精力聚焦在“怎么让用户3秒内刷出带实时余票的海南酒店列表”,而不是调试异步回调丢失导致的订单重复创建。


2. 搭建最小可运行骨架:5 分钟跑通首页加载与用户登录流程

旅游网站的核心不是炫酷动效,而是“信息准确、操作可靠、响应及时”。我们先抛开推荐算法、支付网关、地图 SDK,用最简路径验证技术栈协同是否顺畅——目标:本地启动后,浏览器打开http://localhost:8080能看到带搜索框的首页,点击登录按钮弹出表单,提交后跳转至用户中心页,且控制台无跨域报错、无 SQL 异常、无 Vue 组件未注册警告。

2.1 后端:SpringBoot 初始化 + 用户表基础 CRUD

用 start.spring.io 生成基础项目(勾选 Spring Web、Spring Data JPA、MySQL Driver、Lombok),关键配置如下:

# src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发期自动建表,上线必须改为 validate show-sql: true properties: hibernate: format_sql: true

创建用户实体与 Repository(注意:旅游系统中用户密码必须加盐加密,此处仅示意结构):

// src/main/java/com/example/travel/entity/User.java @Entity @Table(name = "t_user") @Data public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; // 手机号或邮箱 @Column(nullable = false) private String password; // BCrypt 加密后存储 @Column(name = "real_name") private String realName; @Column(name = "id_card") private String idCard; // 实名认证字段,旅游行业强要求 }
// src/main/java/com/example/travel/repository/UserRepository.java @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); }

逻辑说明:JpaRepository自带save()、findById()、findAll()等方法,无需手写 SQL;findByUsername是 Spring Data JPA 的命名查询,框架自动生成 SQL,避免拼接风险。idCard字段虽非登录必需,但后续实名购票、保险购买、签证服务都依赖它,旅游系统中用户模型必须前置包含实名字段,不能等业务扩展时再加。

2.2 前端:Vue CLI 创建项目 + Axios 配置代理解决跨域

用 Vue CLI 5.x 初始化(vue create travel-web),安装axios和vue-router:

cd travel-web npm install axios vue-router

配置vue.config.js解决开发期跨域(生产环境由 Nginx 反向代理):

// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 后端 SpringBoot 端口 changeOrigin: true, pathRewrite: { '^/api': '' // 把 /api/user/login → /user/login } } } } }

编写登录 API 调用(src/api/user.js):

// src/api/user.js import axios from 'axios' export function login(username, password) { return axios.post('/api/user/login', { username, password }) } // 注意:此处未加 token 存储逻辑,实际需存入 localStorage 或 pinia store

参数说明:changeOrigin: true是关键,它修改请求头中的Host为target地址,避免后端因 Origin 校验失败拒绝请求;pathRewrite确保前端调用/api/user/login时,代理层自动转发到后端/user/login接口,前后端路径解耦,避免硬编码 URL。旅游网站常需对接第三方票务接口(如航司、酒店 PMS),这种代理模式也方便后续 mock 第三方响应。

2.3 前后端联调:验证登录流程闭环

后端添加登录 Controller(简化版,不含 JWT):

// src/main/java/com/example/travel/controller/UserController.java @RestController @RequestMapping("/user") @RequiredArgsConstructor public class UserController { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @PostMapping("/login") public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials) { String username = credentials.get("username"); String password = credentials.get("password"); User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); if (!passwordEncoder.matches(password, user.getPassword())) { return ResponseEntity.badRequest().body(Map.of("code", 401, "msg", "密码错误")); } // 实际应返回 JWT token,此处仅返回模拟成功 Map<String, Object> result = new HashMap<>(); result.put("code", 200); result.put("msg", "登录成功"); result.put("data", Map.of("userId", user.getId(), "username", user.getUsername())); return ResponseEntity.ok(result); } }

前端调用并跳转(src/views/Login.vue):

<template> <div class="login-form"> <input v-model="form.username" placeholder="手机号/邮箱" /> <input v-model="form.password" type="password" placeholder="密码" /> <button @click="handleLogin">登录</button> </div> </template> <script> import { login } from '@/api/user' import { useRouter } from 'vue-router' export default { setup() { const router = useRouter() const form = reactive({ username: '', password: '' }) const handleLogin = async () => { try { const res = await login(form.username, form.password) if (res.data.code === 200) { // 存 token(实际用 pinia 或 localStorage) localStorage.setItem('token', 'mock-jwt-token') router.push('/dashboard') // 跳转用户中心 } } catch (e) { alert(e.response?.data?.msg || '登录失败') } } return { form, handleLogin } } } </script>

验证要点:启动mvn spring-boot:run和npm run serve后,在浏览器 Network 面板中检查:

  • 登录请求是否发出到http://localhost:8080/user/login(代理生效标志);
  • 响应状态码是否为200,且返回 JSON 中含data.userId;
  • Vue 控制台无Failed to resolve component报错(路由组件已注册);
  • 页面跳转后地址栏变为http://localhost:8080/#/dashboard。
    这一步看似简单,却是整个技术栈协同的“信任起点”——只要它通了,后续所有功能模块(景点搜索、订单提交、评价上传)都能复用同一套通信契约。

3. 旅游业务核心:景点搜索与酒店列表的高性能实现策略

旅游网站的“心脏”不是首页 Banner,而是搜索结果页。用户输入“大理古城附近民宿”,期望 1 秒内返回 200+ 条结果,每条含实时价格、评分、距离、可订房型。这远超普通电商搜索——它需要地理围栏(5km 内)、多维过滤(价格区间、设施标签、评分阈值)、动态排序(按“距离+评分+价格”加权)、以及库存状态实时渲染(“仅剩2间”)。若直接SELECT * FROM hotel WHERE city='大理' AND type='民宿',MySQL 很快会成为瓶颈。我们必须分层优化。

3.1 数据库层:MySQL 表结构设计与索引策略

旅游数据有强空间属性,但 MySQL 8.0+ 的POINT类型 +ST_Distance_Sphere已足够支撑中小规模地理查询。关键在于避免 SELECT * + 全表扫描:

-- 创建酒店表(精简核心字段) CREATE TABLE `t_hotel` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(200) NOT NULL COMMENT '酒店名称', `city` varchar(50) NOT NULL COMMENT '城市', `district` varchar(50) DEFAULT NULL COMMENT '区域(如:大理古城)', `address` varchar(300) NOT NULL COMMENT '详细地址', `location` point NOT NULL COMMENT '经纬度(WGS84)', `price_low` int NOT NULL DEFAULT '0' COMMENT '最低房价(元)', `price_high` int NOT NULL DEFAULT '0' COMMENT '最高房价(元)', `score` decimal(2,1) NOT NULL DEFAULT '0.0' COMMENT '综合评分', `review_count` int NOT NULL DEFAULT '0' COMMENT '点评数', `has_wifi` tinyint(1) NOT NULL DEFAULT '0', `has_parking` tinyint(1) NOT NULL DEFAULT '0', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:0-下架,1-上架', PRIMARY KEY (`id`), SPATIAL KEY `idx_location` (`location`), KEY `idx_city_district` (`city`,`district`), KEY `idx_price_score` (`price_low`,`score`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='酒店主表';

索引设计逻辑:

  • SPATIAL KEY idx_location:用于ST_Distance_Sphere(location, POINT(100.2, 25.6)) < 5000计算 5km 内酒店,比lat/lng双字段 B-tree 索引快 3~5 倍;
  • KEY idx_city_district:覆盖“大理+古城”这类高频组合查询,避免回表;
  • KEY idx_price_score:支撑“价格 200-500 且评分 ≥4.5”的过滤,B-tree 索引天然支持范围查询;
  • 严禁在status上单独建索引:该字段区分度极低(99% 是 1),反而拖慢 INSERT/UPDATE。旅游系统中“下架”是小概率事件,用WHERE status=1即可,优化器会走全表扫描+ICP(Index Condition Pushdown)。

3.2 应用层:SpringBoot 多条件动态查询构建

用户搜索条件千变万化:可能只选城市,可能叠加价格+评分+设施,可能还要按“距离最近”或“评分最高”排序。硬写 SQL 模板易出错,用 QueryDSL 或 MyBatis-Plus 的 LambdaQueryWrapper 更安全:

// src/main/java/com/example/travel/service/HotelService.java @Service @RequiredArgsConstructor public class HotelService { private final HotelRepository hotelRepository; public Page<Hotel> searchHotels(HotelSearchCriteria criteria, Pageable pageable) { Specification<Hotel> spec = (root, query, cb) -> { List<Predicate> predicates = new ArrayList<>(); // 城市与区域(模糊匹配,但索引仍有效) if (StringUtils.hasText(criteria.getCity())) { predicates.add(cb.equal(root.get("city"), criteria.getCity())); } if (StringUtils.hasText(criteria.getDistrict())) { predicates.add(cb.like(root.get("district"), "%" + criteria.getDistrict() + "%")); } // 价格区间(利用 idx_price_score 索引) if (criteria.getMinPrice() != null) { predicates.add(cb.greaterThanOrEqualTo(root.get("price_low"), criteria.getMinPrice())); } if (criteria.getMaxPrice() != null) { predicates.add(cb.lessThanOrEqualTo(root.get("price_high"), criteria.getMaxPrice())); } // 评分阈值 if (criteria.getMinScore() != null) { predicates.add(cb.greaterThanOrEqualTo(root.get("score"), criteria.getMinScore())); } // 设施标签(使用位运算或 JSON 字段,此处简化为布尔字段) if (criteria.isHasWifi()) { predicates.add(cb.equal(root.get("has_wifi"), true)); } // 状态:只查上架中 predicates.add(cb.equal(root.get("status"), 1)); // 地理距离(MySQL 8.0+ Spatial 函数) if (criteria.getLat() != null && criteria.getLng() != null) { Expression<Double> distance = cb.function("ST_Distance_Sphere", Double.class, root.get("location"), cb.function("POINT", Object.class, cb.literal(criteria.getLng()), cb.literal(criteria.getLat()))); predicates.add(cb.lessThan(distance, criteria.getMaxDistanceMeters())); } return cb.and(predicates.toArray(new Predicate[0])); }; // 排序:距离优先,其次评分,最后价格 if (criteria.getLat() != null && criteria.getLng() != null) { // 自定义排序:先算距离,再排序 return hotelRepository.findAll(spec, pageable); } else { return hotelRepository.findAll(spec, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("score").descending())); } } }

参数说明:HotelSearchCriteria是 DTO,封装所有搜索参数;Specification是 JPA Criteria API 的谓词构建器,避免字符串拼接 SQL 导致的注入风险;ST_Distance_Sphere返回单位为米的距离值,maxDistanceMeters直接传入(如 5000 表示 5km)。旅游搜索中“距离”是强排序因子,但计算开销大,线上环境必须配合 Redis 缓存热点区域(如“三亚亚龙湾”)的预计算结果集,本节暂不展开。

3.3 前端层:Vue 实现搜索条件联动与防抖

用户在搜索框输入时,若每敲一个字就发请求,服务器瞬时压力巨大。必须加防抖(debounce)且限制最小字符数:

<!-- src/components/HotelSearchBar.vue --> <template> <div class="search-bar"> <input v-model="searchKeyword" @input="debouncedSearch" placeholder="搜索城市、区域或酒店名..." minlength="2" /> <select v-model="selectedCity" @change="onCityChange"> <option value="">全部城市</option> <option v-for="c in cities" :key="c" :value="c">{{ c }}</option> </select> </div> </template> <script> import { ref, watch } from 'vue' import { debounce } from 'lodash' import { searchHotels } from '@/api/hotel' export default { setup() { const searchKeyword = ref('') const selectedCity = ref('') const cities = ['北京', '上海', '广州', '深圳', '大理', '三亚', '成都'] // 防抖:300ms 内只执行最后一次 const debouncedSearch = debounce(async () => { if (searchKeyword.value.trim().length < 2) return try { const res = await searchHotels({ keyword: searchKeyword.value, city: selectedCity.value, page: 1, size: 20 }) // 触发父组件更新列表 emit('update:hotels', res.data) } catch (e) { console.error('搜索失败', e) } }, 300) const onCityChange = () => { // 城市切换时立即触发搜索,不防抖 debouncedSearch.flush() // 清空待执行队列 debouncedSearch() } return { searchKeyword, selectedCity, cities, debouncedSearch, onCityChange } } } </script>

避坑点:debounce必须用lodash的函数式版本,而非 Vue 的v-debounce指令——后者在 Composition API 中难以控制执行时机;debouncedSearch.flush()是关键,它确保用户从“北京”切到“三亚”时,不会因旧的北京搜索请求未完成而阻塞新请求。旅游用户决策链短,搜索响应延迟超过 800ms 就会导致 35% 以上的跳出率,防抖参数必须实测调整(300ms 是平衡灵敏度与负载的常见值)。


4. 高并发下的致命陷阱:订单创建与库存扣减的强一致性保障

旅游网站最怕什么?不是页面卡顿,而是“显示有房,下单却提示售罄”。这通常源于库存扣减与订单创建的非原子性操作。典型翻车场景:A、B 用户同时点击“预订大理XX酒店 2024-05-01 标准间”,后端先查库存(剩余2间),然后创建订单,最后扣减库存——若 A、B 查询几乎同时发生,都会得到“剩余2间”,最终生成2个订单但库存只扣1次,造成超卖。这不是理论风险,是旅游系统日均发生的血泪事故。

4.1 MySQL 行锁 + 事务:最简可靠的扣减方案

放弃“先查后减”的两阶段模式,改用UPDATE ... SET stock = stock - 1 WHERE id = ? AND stock > 0原子语句。SpringBoot 中这样实现:

// src/main/java/com/example/travel/service/OrderService.java @Service @Transactional(rollbackFor = Exception.class) public class OrderService { private final HotelRepository hotelRepository; private final OrderRepository orderRepository; public Order createOrder(Long hotelId, String userId, LocalDate checkIn, LocalDate checkOut) { // 1. 尝试扣减库存(悲观锁,for update) Hotel hotel = hotelRepository.findByIdAndStockGreaterThan(hotelId, 0) .orElseThrow(() -> new RuntimeException("酒店已售罄")); // 2. 扣减库存(此 SQL 保证原子性) int updated = hotelRepository.decreaseStock(hotelId, 1); if (updated == 0) { throw new RuntimeException("库存扣减失败,请重试"); } // 3. 创建订单(此时库存已锁定,不会超卖) Order order = new Order(); order.setHotelId(hotelId); order.setUserId(userId); order.setCheckIn(checkIn); order.setCheckOut(checkOut); order.setStatus("UNPAID"); return orderRepository.save(order); } } // src/main/java/com/example/travel/repository/HotelRepository.java @Repository public interface HotelRepository extends JpaRepository<Hotel, Long> { @Query("SELECT h FROM Hotel h WHERE h.id = :id AND h.stock > 0") Optional<Hotel> findByIdAndStockGreaterThan(@Param("id") Long id, @Param("stock") int stock); @Modifying @Query("UPDATE Hotel h SET h.stock = h.stock - 1 WHERE h.id = :id AND h.stock > 0") int decreaseStock(@Param("id") Long id, @Param("stock") int stock); }

原理说明:findByIdAndStockGreaterThan使用SELECT ... FOR UPDATE(JPA 默认开启),在查询时对 hotel 行加写锁,其他事务无法修改该行直到当前事务结束;decreaseStock的UPDATE语句自带AND stock > 0条件,即使多个请求同时执行,MySQL 的行锁机制保证只有一个能成功更新,其余返回updated=0。这是 MySQL 在旅游系统中最常用、最可靠的库存方案,无需引入 Redis 或 RocketMQ。

4.2 分布式场景:Redis + Lua 脚本兜底方案

当系统拆分为多个 SpringBoot 实例(如订单服务、库存服务),MySQL 行锁作用域限于单实例,需升级为分布式锁。但旅游系统中,优先用 MySQL 本地事务,仅在极端高并发(如秒杀)时才启用 Redis。Lua 脚本保证原子性:

-- Redis Lua 脚本:decrease_stock.lua local stockKey = KEYS[1] -- 如: hotel:123:stock local orderId = ARGV[1] local expireTime = tonumber(ARGV[2]) -- 锁过期时间,单位秒 -- 尝试加锁(SETNX + EXPIRE) if redis.call("set", stockKey .. ":lock", orderId, "NX", "EX", expireTime) == 1 then local currentStock = tonumber(redis.call("get", stockKey)) if currentStock and currentStock > 0 then redis.call("decr", stockKey) return 1 -- 扣减成功 else return -1 -- 库存不足 end else return 0 -- 加锁失败 end

Java 调用:

// RedisTemplate 执行 Lua public Boolean decreaseStockWithRedis(Long hotelId, String orderId) { String script = Files.readString(Paths.get("decrease_stock.lua")); DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Long result = redisTemplate.execute(redisScript, Collections.singletonList("hotel:" + hotelId + ":stock"), orderId, "30"); // 锁 30 秒 return result != null && result == 1L; }

适用边界:此方案仅用于“酒店秒杀”“特价机票抢购”等瞬时流量洪峰场景。日常预订中,MySQL 行锁完全够用,且更易监控(通过SHOW ENGINE INNODB STATUS查看锁等待)。盲目上 Redis 会增加运维复杂度,而旅游系统稳定性压倒一切。

4.3 订单状态机:用状态流转替代 if-else 判断

订单状态(UNPAID → PAID → CONFIRMED → USED → CLOSED)不是简单字段,而是有严格流转规则的有限状态机。硬编码if(status==UNPAID) pay()极易出错。用 Spring State Machine 或轻量级枚举约束:

// src/main/java/com/example/travel/enums/OrderStatus.java public enum OrderStatus { UNPAID("待支付", Arrays.asList(PAY)), PAID("已支付", Arrays.asList(CONFIRM, CANCEL)), CONFIRMED("已确认", Arrays.asList(USE, REFUND)), USED("已使用", Arrays.asList(CLOSE)), REFUNDED("已退款", Arrays.asList(CLOSE)), CLOSED("已关闭", Collections.emptyList()); private final String desc; private final List<OrderAction> allowedActions; OrderStatus(String desc, List<OrderAction> allowedActions) { this.desc = desc; this.allowedActions = allowedActions; } public boolean can(Action action) { return this.allowedActions.contains(action); } } // 支付操作校验 public void payOrder(Long orderId, String paymentId) { Order order = orderRepository.findById(orderId).orElseThrow(); if (!order.getStatus().can(OrderAction.PAY)) { throw new IllegalStateException("订单状态不允许支付:" + order.getStatus()); } order.setStatus(OrderStatus.PAID); order.setPaymentId(paymentId); orderRepository.save(order); }

价值点:旅游订单涉及多方(用户、酒店、支付平台、保险),状态不一致是客诉主因。显式定义状态流转规则,比任何文档都可靠。当某天产品提出“已确认订单允许部分退款”,只需修改CONFIRMED的allowedActions,所有调用处自动拦截非法操作。


5. 避坑指南:旅游系统开发中 5 个高频翻车点与血泪解法

旅游业务逻辑复杂、外部依赖多、监管要求严,很多坑不是代码写错,而是对行业特性理解不足。以下是我在多个旅游系统交付中踩过的真坑,按“现象→原因→解法”结构整理,每一条都配真实日志片段。

5.1 现象:用户实名认证通过,但提交订单时提示“身份证格式错误”

日志片段:
2024-04-15 10:23:42 ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: Invalid ID card number: 11010119900307299X] with root cause

原因:
前端 JS 校验只做了长度和正则(/^\d{17}[\dXx]$/),但未验证最后一位校验码。中国身份证第18位是根据前17位加权求和 mod 11 计算得出的,299X这种尾号根本不存在。旅游系统中,实名信息直连公安库或航司系统,格式错误会导致订单被拒、用户投诉。

解法:
后端必须做完整校验,引入idcard-validator库(Maven):

<dependency> <groupId>com.github.houbb</groupId> <artifactId>idcard-validator</artifactId> <version>1.0.3</version> </dependency>
// 校验逻辑 if (!IdCardUtil.isValid18CardNo(idCard)) { throw new IllegalArgumentException("身份证号码不合法"); }

注意:不要自己实现校验算法,网上流传的 JS 版本存在闰年、地区码过期等兼容性问题。用成熟库,且定期更新版本。

5.2 现象:搜索“上海迪士尼”返回 0 结果,但数据库明明有数据

日志片段:
Hibernate: select ... from t_attraction where name like ?
Binding parameter [1] as [VARCHAR] - [%上海迪士尼%]
Result set: []

原因:
MySQL 默认排序规则utf8mb4_general_ci对中文支持弱,like '%上海迪士尼%'无法匹配name='上海迪士尼乐园'(因“乐园”二字被忽略)。旅游 POI 名称常含后缀(“景区”“度假区”“风景区”),模糊搜索必须精准。

解法:
修改字段排序规则为utf8mb4_unicode_ci,并重建索引:

ALTER TABLE t_attraction MODIFY COLUMN name VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE t_attraction DROP INDEX idx_name; ALTER TABLE t_attraction ADD INDEX idx_name (name);

验证命令:SHOW FULL COLUMNS FROM t_attraction LIKE 'name';确认Collation列为utf8mb4_unicode_ci。此操作需停机,务必在低峰期执行。

5.3 现象:凌晨 2 点定时任务批量同步酒店价格,导致数据库 CPU 100%

日志片段:
2024-04-15 02:00:01 INFO c.e.t.s.PriceSyncService : Start syncing 12,458 hotels...
SHOW PROCESSLIST显示 200+UPDATE t_hotel SET price_low=?, price_high=? WHERE id=?连接堆积

原因:
价格同步任务未分片,单线程遍历 12,458 条记录,每条执行一次 UPDATE。MySQL 在高并发 UPDATE 下,行锁竞争激烈,事务排队,CPU 被锁管理耗尽。

解法:
改用批量更新(Batch Update)+ 分页处理:

// 每 100 条一批 int batchSize = 100; for (int i = 0; i < hotels.size(); i += batchSize) { int end = Math.min(i + batchSize, hotels.size()); List<Hotel> batch = hotels.subList(i, end); // 使用 JPA 的 saveAll,底层生成 INSERT ... ON DUPLICATE KEY UPDATE hotelRepository.saveAll(batch); // 主动触发 GC,避免内存溢出 if (i % 1000 == 0) { System.gc(); } }

关键参数:spring.jpa.properties.hibernate.jdbc.batch_size=50(application.yml),让 Hibernate 合并 SQL。旅游系统中,价格/库存同步是刚需,但必须控制对线上库的影响。

5.4 现象:Vue 前端展示“¥599起”,但用户点击后跳转详情页显示“¥699”

原因:
首页列表用 Redis 缓存了“最低价”,但酒店详情页读取的是 MySQL 实时数据。缓存未设置失效策略,当酒店调价后,首页缓存未更新,产生价格不一致。

解法:
采用“Cache Aside”模式,写操作时主动删除缓存,而非更新:

// 更新价格后 public void updateHotelPrice(Long hotelId, Integer newLowPrice) { hotelRepository.updatePrice(hotelId, newLowPrice); // 删除缓存,下次读取时重建 redisTemplate.delete("hotel:price:" + hotelId); }

注意:不要用SET更新缓存,因为更新操作可能失败(网络抖动、事务回滚),导致缓存与 DB 不一致。删缓存是幂等操作,最安全。

5.5 现象:用户投诉“订单支付成功,但酒店说没收到”

原因:
支付回调通知(Alipay/Wechat)到达后,系统先更新订单状态为PAID,再调用酒店 PMS 接口确认库存。若 PMS 调用超时,订单状态已变,但酒店侧未收到确认,形成“单边账”。

解法:
引入本地消息表 + 定时任务补偿:

CREATE TABLE `t_local_message` ( `id` bigint NOT NULL AUTO_INCREMENT, `business_type` varchar(50) NOT NULL COMMENT '业务类型:HOTEL_CONFIRM', `business_id` varchar(50) NOT NULL COMMENT '业务ID:order_123456', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-待发送,1-已发送,2-已确认', `content` text COMMENT 'JSON内容:{"hotelId":123,"orderId":"order_123456"}', `created_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

支付成功后,先插入消息表(事务内),再发 MQ 或调用 PMS;定时任务扫描status=0的消息,重试调用 PMS,成功后更新status=2。旅游系统中,资金流与业务流必须严格对账,本地消息表是成本最低、可靠性最高的方案。


6. 生产就绪 checklist:从本地开发到高可用部署的 7 个关键动作

写完代码只是开始,旅游网站面对的是真实用户、真实订单、真实监管。我经手的每个上线项目,都会在发布前逐项核对这份清单。它不追求“高大上”,只解决那些会让值班工程师凌晨三点被电话叫醒的问题。

6.1 数据库连接池:HikariCP 参数必须调优

SpringBoot 默认 HikariCP 最大连接数maximumPoolSize=10,这对旅游系统是灾难。一个用户浏览首页(查城市列表)+ 搜索(查酒店)+ 点击详情(查房型)+ 下单(查库存+写订单)可能占用 4~5 个连接。按 5000 并发估算,至少需maximumPoolSize=200。但盲目调大会导致 MySQL 连接数爆满。我的经验值:

场景maximumPoolSizeconnection-timeoutidle-timeoutmax-lifetime
日常流量(QPS<200)5030000(30s)600000(10min)1800000(30min)
大促峰值(QPS>5000)20010000(10s)300000(5min)1200000(20min)
# application-prod.yml spring: datasource: hikari: maximum-pool-size: 200 connection-timeout: 10000 idle-timeout: 300000 max-lifetime: 1200000 leak-detection-threshold: 60000 # 检测连接泄漏(毫秒)

血泪经验:leak-detection-threshold必须开启!曾有个项目因 DAO 层未关闭ResultSet,连接泄漏,凌晨 3

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 12:20:16

MATLAB实现声发射信号熵值计算与优化

1. 项目背景与核心价值声发射技术作为无损检测领域的重要方法&#xff0c;在材料性能评估、结构健康监测等方面有着广泛应用。熵值计算作为信号特征提取的关键环节&#xff0c;直接影响分析结果的可靠性。传统手工计算方式不仅效率低下&#xff0c;而且难以保证计算精度的一致性…

作者头像 李华
网站建设 2026/10/14 12:19:36

PS与3DMAX无缝贴图制作全流程:从接缝修复到平铺验证

简介&#xff1a;面向3D建模初学者的一份PS无缝贴图制作教程&#xff0c;针对3DMAX场景中纹理表面出现明显接缝、影响渲染真实感的问题&#xff0c;给出从图像编辑到材质贴图的全流程解决方法。资源包共1个文件&#xff0c;类型为docx&#xff0c;压缩后大小1.75MB&#xff0c;…

作者头像 李华
网站建设 2026/10/14 12:18:45

股票操作知识体系搭建:从框架设计到实操复盘完整指南

1. 股票操作的知识框架到底该怎么搭很多人一上来就问“买哪只”&#xff0c;这问题本身就问错了。股票操作知识不是一套选股公式&#xff0c;而是一套从认知到执行再到复盘的完整链条。我见过太多人把精力全花在打听消息上&#xff0c;结果连最基本的仓位管理都没搞明白&#x…

作者头像 李华
网站建设 2026/10/14 12:18:11

Oracle EBS系统管理员实战:用户、责任、并发与弹性域配置全解析

简介&#xff1a;Oracle EBS系统管理员培训手册.doc是一份面向Oracle EBS系统管理员的应用产品培训教材&#xff0c;围绕系统管理核心模块展开&#xff0c;涵盖安全管理、并发程序及请求管理、预置文档、弹性域与值集设置、单据序列、安全性控制及打印机配置等关键操作。资源以…

作者头像 李华
网站建设 2026/10/14 12:18:06

Spring Data Redis读取超时问题分析与优化

1. 问题现象与背景分析最近在项目中使用Spring Data Redis时遇到了"Read timed out"的报错&#xff0c;这个问题在分布式系统中相当常见。当我们的应用尝试从Redis服务器读取数据时&#xff0c;客户端在指定的超时时间内没有收到响应&#xff0c;就会抛出这个异常。这…

作者头像 李华