news 2026/10/4 23:03:28

会议室预定系统微服务实战:SpringCloud+分布式锁+分布式事务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
会议室预定系统微服务实战:SpringCloud+分布式锁+分布式事务

去年接了一个会议室预定系统的项目,需求方把技术栈圈得很死:SpringBoot + Vue + SpringCloud,而且明确要求做成微服务分布式架构,不能拿单体应用糊弄。心里第一反应是,会议室预定这种业务也要上微服务?但做完之后我必须承认,这个系统太适合用来拆微服务了。它的核心业务只是"选会议室、定时间段、提交预订",可完整踩遍了微服务那套东西:服务边界怎么划、注册中心怎么选、网关怎么收敛入口、并发抢同一间会议室怎么加分布式锁、跨服务的数据一致性怎么保证。

这个项目的体量不大不小,非常适合作为微服务入门到落地的完整样本。如果你正打算学SpringCloud,或者想找一个能写到简历里的分布式项目,会议室预定系统是一个很好的载体,业务不复杂但技术点齐全。下面我把整个项目的拆解思路、选型理由、核心难点和踩过的坑完整写出来。

1. 为什么一个会议室预定系统要拆成微服务

1.1 需求拆开看:这真不是一张表能搞定的

很多人看到"会议室预定"这个名字,第一反应是:给张表,选时间,插入一条记录,完事。实际做需求调研时就会发现,系统要处理的是"资源、时间、人、流程、消息"五个维度叠在一起的事,每个维度都不省心。

会议室资源管理:每间会议室有名称、位置、楼层、容量、设施标签(投影、白板、视频终端)、开放时段(比如08:00-21:00),还有预定提前量和最短时长限制。有些公司还会按级别区分会议室:普通会议室随便订,高管会议室和对外接待室必须走审批。

预订流程管理:用户按日期查看会议室占用日历,选择空闲时间段,填写议题、参会人数、投影需求,提交后系统要做冲突校验。如果撞车了,要明确提示哪段时间哪间会议室已被占用,再顺手推荐附近的空闲会议室。

审批链路:高级会议室发起预订后生成审批任务,审批人可以是部门主管或行政负责人。审批通过、驳回后要通知申请人,还要更新会议室占用状态。审批超过N小时未处理要有超时提醒,甚至自动释放。

通知与提醒:预订成功通知、审批结果通知、会前15分钟提醒、会议取消/变更通知。通知渠道一般不止一个,站内信、邮件、企业IM都可能要接。

统计与展示:会议室使用率、热门时段、部门预订排行、爽约率。这个模块刚开始没人提,一到月底行政就要报表,没有就得临时写SQL从业务表里捞。

把这些需求摆在一起,会议室预定是典型的业务中台场景:资源域(会议室)、流程域(预订与审批)、用户域(账号权限)、消息域(通知)。它们之间的耦合点只有"预订单"这一个核心对象,非常适合按领域切开。上微服务不是技术表演,而是业务复杂度到了一定程度之后,天然会往这个方向走。

1.2 单体也能跑,但后面一定会卡壳

这种系统用单体SpringBoot写,初期开发速度确实快。但有两个现实问题会在项目中期开始发作。

第一是团队协作。三四个人同时改一个工程,预订模块加字段和审批模块改状态机,代码冲突几乎不可避免。第二是数据模型互相侵蚀。单体架构下所有表都在一个库里,预订服务要改状态的时候直接碰占用表,审批服务也碰,字段命名和维护归属很快会乱。更别说后面要接大屏、门牌终端、IM机器人时,所有功能都堆在同一个进程里,任何一个模块发布都要连累整套系统重新部署。

我在这类项目上的经验是:微服务的价值通常不在超高并发,而在边界清晰。把"会议室资源"和"审批流程"分开后,审批规则怎么改都不会动到会议室数据表,业务流程之间的互相干扰会小很多。会议室预定系统虽然QPS不高,但业务域边界非常清楚,每个域有自己明显的数据归属和变化频率,拆完之后反而比单体更好维护。

1.3 什么样的项目才值得上微服务

这里想多说一句,因为会议室预定系统一直是被嘲讽"杀鸡用牛刀"的典型。我的判断标准很简单:第一,是否有清晰的业务域边界;第二,是否有跨域的数据流转;第三,是否有独立的扩展计划。三条都满足,即使并发量不高,也可以按微服务做。

如果只是单人维护的毕业设计或内部小工具,老老实实单体会比微服务省事十倍。但如果你是想拿这个项目当微服务技术栈的练手样本,或者公司明确要沉淀一套可以复用的预订、审批基础设施,那拆微服务就是合理的。很多人问SpringCloud入门有没有简洁路线,我的答案是:别去看那些虚构的电商项目,就拿会议室预定这种身边就有的场景动手,反而学得快,因为你对业务有直觉,遇到问题能判断对错。

2. 服务拆分:5个服务+网关,边界是怎么划出来的

2.1 服务清单与职责

最终我拆出来的结构是6个模块:网关服务加5个业务服务。每个服务都有自己的数据库,服务之间不能直接访问对方的表,只能通过接口调用。

服务名核心职责主要数据表
gateway统一入口、路由转发、token校验、跨域无数据库
user-service用户、部门、角色、登录鉴权、JWT签发sys_user, sys_role, sys_dept
meeting-room-service会议室资源、设施标签、开放时段room, room_facility, room_schedule
booking-service预订单创建、冲突校验、占用状态、签到booking, room_occupancy
approval-service审批任务、审批规则、审批历史approval_task, approval_rule
notify-service站内信、邮件/IM通知、重试队列notify_record

当时也有人建议把统计模块单独拆一个analytics-service,我算了算工作量,把统计功能先放在booking-service里,用定时任务跑汇总表。原因很简单:一个服务初期没有必要拆得比业务域还细,等报表需求复杂了再从booking里迁出去,成本也不高。微服务常见的错误是拆得太碎,拆出十几个服务,每个服务就一两张表,部署和联调成本反而把收益吃掉了。

2.2 拆分依据:三个标准缺一个我都会再合并回去

这次拆分的标准有三条,也是我后来做其他项目一直沿用的原则。

第一条,按业务域走,不按功能页面走。会议室列表页看起来是前端一个菜单,但它同时要查room-service和user-service,不能因为页面整合就把服务合并。第二,数据归属要清晰。每个服务拥有自己的数据库,这个规则写进开发规范,执行得很严格,谁跨库查表就重写接口。第三,变更频率隔离。审批规则和通知模板是变更最频繁的,会议室基础数据几乎不变,把它们放在不同服务里,审批规则升级时不会影响会议室资源查询。

后来验证下来,第三条标准最有用。单体项目里改一个审批规则配置往往要把整个工程重新打包发布;拆分后,approval-service单独发布,其他服务完全不受影响。会议室管理端半夜临时加一间会议室、改一段开放时间,只动room-service,不会因为审批服务正在发版就把资源查询也一起带挂。

2.3 拆分后立刻遇到的两个新问题

服务拆分带来的第一个问题是跨服务的数据引用。预订列表页需要显示会议室名称和预定人姓名,但这些数据在room-service和user-service里,booking-service只有ID。解决方案是冗余字段:在创建预订单时,把会议室名称、预定人姓名、部门名称一起冗余进booking表。查询时直接查本地表,不再跨服务调接口。冗余的代价是数据可能不一致,比如会议室改名,这个概率很低,且可以由管理端编辑后推送变更消息来更新冗余字段。

第二个问题是跨服务的数据一致性。预订、审批、通知分别落在三个服务里,本地事务肯定管不了整条链路。这个在第五章详细说,是整套系统里最需要设计的地方,也是面试时最能体现分布式功底的部分。

3. SpringCloud组件选型与版本兼容性踩坑

3.1 注册中心与配置中心:Nacos为什么比Eureka更合适

注册中心选了Nacos而不是Eureka,这是有实际情况支撑的。Eureka 2.0在2020年后基本停止演进,SpringCloud官方对Eureka的维护也逐步弱化。Nacos同时提供注册中心和配置中心,配置修改后能实时推送到客户端,这一点在微服务场景里太重要了。审批规则、通知模板、开关策略这些配置如果每次都要改代码重新发布,微服务的优势就废了一半。

选Nacos的过程中踩了一个典型的版本坑。当时开发机器上默认装了SpringBoot 2.7.5,直接去集成Nacos客户端,结果服务一直报连接超时,反复排查才发现是SpringCloud Alibaba的版本对应关系没对上。这里整理了一份当时锁定的稳定版本组合,后面所有服务都用的这一套:

组件版本备注
SpringBoot2.6.13别用太新的2.7.x,兼容性坑多
SpringCloud2021.0.5与SpringBoot 2.6配套
SpringCloud Alibaba2021.0.5.0这组版本号很容易记错
Nacos Server2.2.3客户端与服务端保持相近大版本
Nacos Client2.2.3由Alibaba依赖统一管理

版本统一之后,一切就顺利了。这里提醒一句:微服务项目里"能跑的版本组合"比"最新版本"重要得多,项目初始化时把版本矩阵锁死,写进根pom的dependencyManagement里,省下的都是排查时间。遇到网上教程给的代码跑不通,十有八九是版本差异导致的,而不是逻辑问题。

3.2 网关:Spring Cloud Gateway而不是Zuul

网关选了Spring Cloud Gateway,核心原因是它在Spring Cloud全家桶里的生态最好,基于WebFlux实现,网关本身非阻塞IO,适合作为所有请求的流量入口。Zuul 1.x基于Servlet,性能在网关这种高转发场景下不太够看。

网关上的核心配置有这几块:路由规则、token校验、跨域处理。下面是我实际用的路由配置片段:

spring: application: name: gateway cloud: gateway: routes: - id: user-route uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1 - id: room-route uri: lb://room-service predicates: - Path=/api/room/** filters: - StripPrefix=1 - id: booking-route uri: lb://booking-service predicates: - Path=/api/booking/** filters: - StripPrefix=1

lb://前缀表示按服务名从注册中心做负载均衡,前端只需要知道网关地址,不需要感知背后有几个服务。前端baseURL统一配置成http://网关地址:9000,/api/booking/**会被转发到booking-service,排查问题的时候看请求路径就知道落在哪个服务上。

网关里的GlobalFilter做了token校验:白名单路径(登录接口、静态资源)直接放行,其余请求从Header里取Authorization,能解析出合法JWT就放行,解析不到或过期就返回401。这个过滤器是全局的,后面接入别的终端时规则可以复用。

另外一定要提跨域。前端用Vite起在8080端口,接口在9000,浏览器跨域问题会直接拦截预检请求。我在网关里统一配置了CORS:允许的来源可以写死前端地址,也可以走配置中心动态下发;允许的方法和Header要包含Authorization和Content-Type,否则登录后请求还是会被浏览器拦截。

3.3 服务间调用:OpenFeign与Sentinel

服务内部调用统一用OpenFeign。声明式HTTP客户端的好处是代码风格接近本地调用,每个服务暴露的接口用FeignClient对上就行。比如booking-service要校验会议室占用状态时,调用room-service的/api/room/checkAvailable,在booking-service里定义一个Feign接口就能直接调用。

OpenFeign的坑主要在超时上。默认连接超时和读超时都很短,微服务之间调用涉及数据库查询、分布式锁获取,一旦碰上慢查询就会抛超时异常。我后来在配置里显式放开:

feign: client: config: default: connectTimeout: 2000 readTimeout: 5000

另外一个容易漏的是熔断。如果room-service出问题了,booking-service如果一直等着Feign返回,会把自身线程池打满。这里用Sentinel在provider侧做保护:核心接口设置QPS阈值,比如"查询会议室占用"这个接口单机阈值为200,超过直接降级返回繁忙。预订和审批这种核心链路,宁可短暂降级也不要让故障扩散到整个系统。

4. 分布式锁:守住"同一会议室同一时段不重复预定"

4.1 为什么不靠数据库唯一索引

会议室预定的核心约束是:同一会议室在同一时间段只能有一个有效预订。单体项目里直接在房间、日期、开始时间、结束时间上建唯一索引,配合数据库事务就能挡住大部分冲突。

但微服务拆分后,room-service管理"资源",booking-service管理"预订单",如果不做额外设计,一次预订操作要跨两个服务完成:先在room-service查占用,再去booking-service插入预订单,再回到room-service锁占用时段。这三步之间没有本地事务保护,两个用户同时抢最后一小时空闲,都是先查到空闲,然后各自插入预订单,最后都尝试锁占用,很容易出现两单都创建成功的脏数据。

所以光靠数据库约束不够,必须在应用层加一把跨服务的锁。这就是分布式锁的用武之地。

4.2 Redis分布式锁的正确姿势

分布式锁我用的是Redis,锁的key设计成业务维度的真实资源:room:lock:105:2025-06-04:14:00-15:00。这个粒度既能精确锁定"同一会议室同一时段",又不会把不同会议室的预订互锁,并发效率是最高的。

加锁用Redis的SET命令带上三个参数:

String lockKey = "room:lock:" + roomId + ":" + date + ":" + startTime + "-" + endTime; String requestId = UUID.randomUUID().toString(); boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);

setIfAbsent对应的是SET key value NX PX 30000,NX保证只有key不存在时才写入,PX保证锁有自动过期时间。requestId存的是本次请求的唯一标识,这个值非常关键,后面释放锁要靠它。

释放锁不能直接del,因为存在一种危险情况:线程A的锁快过期了,线程B抢到了同一把锁,A处理完业务后执行del,把B的锁删了。为了避免这种误删,释放锁必须做"先验证再删除",而且这个验证加删除必须是原子操作,用Lua脚本实现:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

脚本先GET锁里的value,和当前线程的requestId比对,相等才DEL。不是自己的锁就返回0,这样就杜绝了误删别人锁的问题。

4.3 锁超时、续期与粒度选择

加锁时设置了30秒过期时间,但预订流程里要写数据库、调Feign、组装返回结果,耗时很可能超过30秒。锁一旦过期,并发请求就会再次进入临界区。核心问题是:过期时间设多长才合适?设长了,服务宕机锁迟迟不释放;设短了,业务还没执行完锁就散了。

正规的做法是给锁加自动续期,也就是看门狗机制。用Redisson的RLock时,默认会启动一个后台线程,在锁过期前自动续期,直到业务线程主动释放锁。如果不想引入Redisson,也可以自己写一个续期线程,每10秒执行一次续期,业务结束后关闭线程。

锁粒度也值得说。最初我把锁key设计成了整间会议室:room:lock:105,结果预订105会议室的所有时间段都被串行化了,明明下午2点、3点是两个完全不相干的预订,都要排队等待。改成"会议室+日期+时间段"后并发度上来了。这个粒度设计在分布式锁面试题里经常被追问,核心回答思路是:锁粒度要和业务资源粒度保持一致,锁的东西越具体,并发越高。

4.4 压测验证锁的效果

为了验证分布式锁到底靠不靠谱,我用JMeter做了压测:100个线程同时抢同一间会议室同一天下午14:00-15:00,请求在booking-service入口并发提交。

不加锁时,100个请求几乎全部返回预订成功,会议室占用表里出现几十条重叠记录。加锁之后,同一时段最终只有1个请求成功,其余返回"该时段已被预订",再叠加数据库唯一索引做兜底,双保险下数据正确率100%。压测数据记录在第七章表格里。

5. 分布式事务:预定、审批、通知的最终一致性

5.1 一个预订动作牵动三个服务的数据

一次完整的预订动作,逻辑上要做的事包括:创建预订单(booking-service)、锁定会议室占用(room-service)、若走审批则创建审批任务(approval-service)、发出预订成功通知(notify-service,通常由MQ异步完成)。

这些数据分布在三个数据库里,不可能用本地事务一次提交。如果不用分布式事务方案,会出现很多怪现象:预订单提示成功但占用没锁上,两个用户都以为自己订到了;审批通过了但预订单状态还停在待审批;通知先发出去了,审批后又被驳回,用户收到"预订成功"后又收到"申请被驳回",体验很差。

5.2 为什么选"可靠消息最终一致性"而不是Seata

分布式事务的主流方案有Seata的AT/TCC模式、本地消息表可靠消息、MQ事务消息等。对会议室预定这个场景,我排除了Seata。

Seata AT模式能提供接近强一致的效果,依赖全局锁和undo_log回滚日志,代价是事务时间变长、锁冲突变多、运维要额外部署TC服务端。会议室预订对一致性的容忍度是"最终一致就够":通知晚到几秒没人介意,审批结果晚同步几分钟也能接受。所以用可靠消息来保证业务数据的最终一致,性价比高得多。

5.3 本地消息表方案落地细节

方案核心是:业务数据和消息数据在同一个本地事务里写入,然后由后台任务把消息可靠地发送到MQ,MQ消费者在目标服务里执行后续业务并幂等去重。

具体流程拆成五步:

  1. booking-service在创建预订单时,同一个本地事务里写入booking表和outbox消息表。outbox记录的状态是0(待发送),消息体是整个预订事件JSON。
  2. 一个定时任务每2秒扫描outbox表,查status=0且send_time小于当前时间的记录,逐条发送到RabbitMQ的booking.exchange。
  3. RabbitMQ根据路由键分发给approval-service和notify-service消费。
  4. 消费成功后,notify-service写站内信并调用booking-service的接口,把对应的outbox记录状态置为1(已确认)。
  5. 如果MQ消息丢失或消费异常,定时任务会在下轮继续重发;消费方通过message_id唯一索引去重,同一消息重复消费也不会产生两条通知。

outbox表结构大致是这样:

CREATE TABLE `outbox` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `message_id` varchar(64) NOT NULL COMMENT '全局唯一消息ID', `event_type` varchar(32) NOT NULL COMMENT '事件类型:BOOKING_CREATED / APPROVAL_RESULT', `payload` text NOT NULL COMMENT '业务消息体JSON', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待发送 1已确认', `send_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_message_id` (`message_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

业务上还有一个反向链路:approval-service审批通过后,要把结果通知给booking-service去更新预订单状态。这条链路走同样的"本地消息表到MQ到消费到确认"流程,只是事件类型变成了APPROVAL_RESULT。整套方案里没有强一致,但任何一步失败都能靠定时任务重试找到根因,不会留脏数据。

5.4 兜底的对账任务

可靠消息方案不是银弹,极端情况下会出现写成功但MQ一直消费不了的消息积压。所以我还加了一个对账任务:每天凌晨2点,扫描状态为"待审批"超过24小时、或审批通过但预订单状态异常的数据,生成异常清单发给管理员。

对账任务里也用到了分布式锁,防止多个实例同时跑对账导致重复处理。这两部分内容其实是在互相支撑:分布式锁保证并发操作不越界,分布式事务保证跨服务业务最终一致,缺了任何一个,会议室预定系统在分布式环境下都会出问题。

6. Vue前端与微服务的协作:网关、动态路由、实时状态

6.1 前端技术栈与页面规划

前端用的是Vue 3 + Vite + Element Plus + Pinia + Axios。Vite的开发体验比Webpack好太多,热更新基本秒级。

页面结构围绕业务场景拆成几个模块:登录页、会议室列表页、预订日历页、我的预订页、审批中心页、会议室管理页、统计报表页。整个前端只有一套代码,但不同角色登录后看到的菜单和操作按钮完全不同,这套差异是用动态路由实现的。

6.2 前端只面对网关一套接口

前端axios实例的baseURL直接指向网关:http://192.168.1.100:9000,所有接口路径都带模块前缀,由网关路由分发到对应微服务。这样做有几个好处:前端不感知服务拆分,后端服务拆分变化不影响前端;登录接口走/api/user/login,用户信息接口走/api/user/info,路径即模块,排障时看网络面板就能知道请求落在哪个服务。

axios拦截器里统一做了两件事:请求拦截器把本地存的token加到Authorization头;响应拦截器对401做统一跳转登录处理,对业务错误码弹出Message提示。这样每个页面都不需要重复写鉴权和错误处理代码。

如果不想单独部署nginx,也可以把前端npm run build之后的dist静态资源直接放进网关或某一个SpringBoot服务的static目录里统一发布,适合内网小规模场景。但正规环境还是建议前端独立部署,用nginx托管静态文件,再反向代理到网关,这样前端静态资源和后端服务互不影响。

6.3 登录鉴权与动态路由

登录流程:用户提交用户名密码,user-service验证后返回JWT;前端把token存到localStorage,再调/api/user/info拿到用户信息、角色和菜单权限列表。

动态路由是关键点,前端不能把所有菜单都写死在静态路由表里。我用后端返回的菜单标识数组,由前端把已注册的异步组件映射到路由配置,然后通过router.addRoute动态挂载。一个非常常见的坑是刷新白屏:刷新页面时,router初始化发生在菜单接口返回之前,动态路由还没来得及挂载,匹配不到页面就白屏了。解决方法是把"获取用户信息+挂载动态路由"放到main.js初始流程的前置await里,确保刷新后路由表先就绪再渲染页面。

权限粒度上,按钮级权限用自定义指令v-permission去控制。会议室管理页面的"删除""导出"按钮只有管理员角色可见,普通用户只显示"预订""取消"按钮。这样权限不是只停留在菜单隐藏层面,而是真正控制到每个操作入口。

6.4 日历视图与实时状态推送

会议室预定的核心交互都在日历页。我用了FullCalendar做日、周、月视图,把一天切成会议室的时间轴:灰色块表示已占用,绿色块表示空闲,点击空闲时间段弹出预订表单。时间段选择器做了简单的步进对齐,只允许整点或半点开始,避免用户提交任意时间导致后续冲突校验复杂化。

前端做的时段冲突校验只是体验优化,真正的保护在后端的分布式锁。这一点必须跟用户和团队成员明确:前端校验可以挡掉80%的常规误操作,剩下20%的并发碰撞必须由后端兜底。

实时状态更新用WebSocket实现。会议室的占用状态变化、审批结果、通知未读数都会通过WebSocket推送到前端,页面不需要手动刷新就能看到最新结果。链路是这样的:后端服务处理完业务后,通过MQ通知WebSocket服务端点,再由WebSocket把事件推到对应在线用户。这里和第五章的分布式事务消息是同一条链路,最终一致性完成后,状态自然就推过来了。

7. 压测数据、部署方式与几个值得记住的教训

7.1 部署架构与资源规划

整套系统用Docker Compose编排部署:mysql、redis、rabbitmq、nacos-server各一个容器,5个业务服务各自一个容器,前端用nginx容器托管静态文件并反代网关。

网关是唯一暴露给外部的端口,前端nginx也只配置网关地址作为上游。这种部署方式的好处是开发和联调环境完全一致,本地起不来整栈的时候,直接docker compose up就能复现。服务实例数量按压力情况调节:初期每个服务1个实例,压测后发现booking-service和gateway是热点,各扩到2个实例,room-service和notify-service保持1个也够。

微服务部署后有一个容易被忽略的资源问题:每个JVM默认堆内存可能很大,5个服务全部启动,整台服务器16G内存基本被占掉大半。我后来统一给每个服务加了-Xms256m -Xmx512m参数,QPS不降但内存占用大幅下降,一台4核8G的测试机就能跑完整套环境。

7.2 JMeter压测结果

压测主要针对两个核心场景:并发创建预订、同一时段抢订。并发抢订的压测结果记录如下:

场景并发数无锁方案加锁后
同一时段抢订100线程约93个请求返回成功,占用表大量重叠仅1个成功,其余返回已占用
整体预订接口200QPS持续5分钟平均RT约220ms平均RT约280ms,错误率0.2%

加锁后整体RT上升了约60ms,原因是每次预订要先获取分布式锁、再查占用、再写库、再释放锁。对会议室预定这种低频但强一致要求的场景,这60ms完全可接受,换来的是数据正确率100%。

7.3 踩坑记录

踩坑一:SpringBoot版本太高导致的Nacos连接异常。项目启动时用了SpringBoot 2.7.x,Nacos客户端始终报ConnectTimeout,折腾好几个小时。版本矩阵表锁死之后,问题直接消失。微服务项目第一件事就是锁版本矩阵。

踩坑二:释放锁时误删别人的锁。早期实现直接用redisTemplate.delete(lockKey),压测时出现了A请求释放B请求锁的偶发现象,后来改成Lua脚本先比对requestId再删除,问题彻底解决。

踩坑三:OpenFeign调用超时。booking-service调用room-service的占用校验接口,默认超时太短,测试环境一有慢SQL就抛超时,最后不仅调长了超时时间,还在room-service侧加了索引优化,双管齐下才稳定。

踩坑四:通知消息重复发送。本地消息表方案上线后的前两天,用户反映偶尔收到两条预订成功通知。排查发现是消费成功后回调标记outbox状态的接口超时,定时任务重发了消息。给notify-service的消息消费加上message_id去重,站内信表建唯一索引后,重复通知消失。

踩坑五:Vue动态路由刷新白屏。这个问题前面已经说过,本质是动态挂载路由的时机问题,把用户信息获取前置到应用初始化流程里就解决了。

7.4 一点收尾的个人体会

做完这个项目,我对微服务架构的理解比看十篇博客都深。会议室预定系统没有任何高并发的炫技点,但它天然跨了好几个业务域,逼着我去思考服务边界、分布式锁、分布式事务、消息可靠性这些问题。如果你也想系统练一遍SpringBoot+Vue+SpringCloud这套技术栈,会议室预定系统是个很合适的载体,既不会因为业务太复杂学不动,又覆盖了微服务里最常被问到的核心难点。

最后再分享一个实用技巧:做这种分布式系统,边开发边把关键链路画成时序图,存在项目文档里。排查线上问题的时候,对照时序图看日志会直观很多,比自己现场回忆要可靠得多。

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

OpenClaw人人养虾:Kilocode 接入 Kilo Gateway 的 API Key 配置与验证

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

作者头像 李华
网站建设 2026/10/4 22:53:07

让Agent接管GitHub Issue到PR全链路:工程实践与避坑指南

1. 为什么我决定让 Agent 接管 Issue 到 PR 这条链路第一次冒出"让代码 Agent 处理 GitHub Issue"这个念头,是在一个再普通不过的深夜。项目仓库里堆了三十多个 open issue,一半是"这个按钮点不动",一半是"文档里的…

作者头像 李华
网站建设 2026/10/4 22:13:27

从零手搓AI工程:手写推理引擎与动态组批实战

1. 从零手搓AI工程:为什么我不建议你直接调包很多人一听到“AI工程”这四个字,第一反应就是打开某个云平台,拖几个组件,调一下API,然后跑通一个Demo,就觉得自己已经掌握了。我刚开始也是这么想的&#xff0…

作者头像 李华