FEATURED · 精选文章

Spring Boot微服务挂号系统实战:Dubbo+Nacos+Redis分布式架构

发布时间 / 2026/9/16 14:10:29
来源 / 创域科博编辑部
栏目 / 资讯中心
Spring Boot微服务挂号系统实战:Dubbo+Nacos+Redis分布式架构 简介本资源是一套基于Java实现的微服务分布式医院挂号系统源码面向Java后端开发者、微服务初学者及医疗信息化项目实践者解决传统单体架构在高并发挂号、多科室协同、弹性扩展等方面的瓶颈问题。压缩包共77个文件含60个Java核心业务与微服务模块代码如service_user、service_hosp等、10个XML配置文件用于MyBatis映射与Spring Boot集成、2个Properties配置文件支持多环境部署以及mvnw构建脚本、readme说明文档和jar依赖包整体仅178KB轻量易导入。已有295人学习下载适合快速理解Spring Cloud Alibaba或Dubbo Nacos MySQL的典型医疗微服务落地结构。读者可直接运行并调试完整挂号流程掌握服务拆分逻辑、API网关路由、分布式事务协调预览中可见service层独立pom与common_util模块复用设计并参考清晰的目录分层model→common→service→src开展二次开发。1. 这不是又一个Spring Boot单体Demo77个文件背后的真实微服务挂号系统长什么样医院挂号系统从来不是“用户填表→存数据库→返回成功”这么简单。高峰期每秒上百并发请求、跨科室号源动态锁定、退号后号段实时释放、多院区号池隔离与协同——这些需求天然排斥单体架构。本项目用77个文件含60个Java类构建了一个可落地的分布式挂号系统核心不在“能跑”而在“怎么扛住真实业务压力”。它没用Spring Cloud Alibaba全家桶堆砌而是基于Maven多模块Spring Boot 2.7.xDubbo 3.2隐式依赖Nacos 2.2配置中心注册中心Redis 6分布式锁号源缓存的务实组合。适合已有Spring Boot基础、正从单体转向微服务的中级开发者拆解学习你能看到service_user和service_hosp如何通过Dubbo接口契约解耦也能在src/main/resources/application.yml里找到Nacos配置中心地址和Redis连接池参数。这不是教学玩具是挂号业务逻辑与分布式基础设施的真实咬合。2. 微服务模块拆分逻辑与Maven多模块结构解析2.1 为什么用Maven多模块而非独立Git仓库微服务拆分常陷入“物理隔离陷阱”每个服务单独建库结果CI/CD链路爆炸、版本对齐困难、公共代码重复维护。本项目采用单仓库多模块结构根目录pom.xml定义统一父POM强制所有子模块继承spring-boot-starter-parent和spring-cloud-dependenciesFinchley.SR2确保Spring Boot 2.7.18与Spring Cloud Finchley版本严格对齐。关键设计点在于common_util模块被service_user和service_hosp同时依赖但仅包含工具类如IdWorker雪花ID生成器、ResultT统一封装类不包含任何业务实体或DAO层——这避免了模块间隐式耦合。执行mvn clean install -Dmaven.test.skiptrue时Maven会按common_util → service_util → service_user → service_hosp顺序编译依赖传递性由scopecompile/scope保障。提示service_util模块看似冗余实为业务通用层。它包含HospOrderService挂号订单服务接口和UserAccountService用户账户服务接口但不提供实现类。实现类分散在各业务模块中这是Dubbo服务契约的核心体现。2.2 模块职责边界与Dubbo服务暴露机制查看service_user/pom.xml其dependencies中明确引入dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-dubbo/artifactId /dependency dependency groupIdcom.example/groupId artifactIdservice_util/artifactId version1.0-SNAPSHOT/version /dependency这表明service_user作为Dubbo服务提供方需实现service_util中定义的接口。进入service_user/src/main/java/com/example/service/user/impl/UserAccountServiceImpl.java可见关键注解Service // Dubbo的Service非Spring的Service public class UserAccountServiceImpl implements UserAccountService { Override public ResultBoolean deductBalance(Long userId, BigDecimal amount) { // 实际扣款逻辑含Redis分布式锁校验 String lockKey lock:balance: userId; boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 5, TimeUnit.SECONDS); if (!locked) { return Result.fail(余额操作冲突请重试); } try { // 数据库扣减 Redis缓存更新 int rows userAccountMapper.updateBalance(userId, amount.negate()); if (rows 0) throw new RuntimeException(余额不足); redisTemplate.opsForValue().set(user:balance: userId, getBalanceFromDB(userId)); return Result.success(true); } finally { redisTemplate.delete(lockKey); // 必须释放锁 } } }2.2.1 Dubbo服务暴露的关键配置项service_user/src/main/resources/application.yml中Dubbo配置dubbo: application: name: service-user # 服务名Nacos中注册的唯一标识 registry: address: nacos://127.0.0.1:8848 # Nacos注册中心地址 protocol: name: dubbo port: 20880 # Dubbo协议端口避免与HTTP端口冲突 scan: base-packages: com.example.service.user.impl # 自动扫描Service注解的实现类此处port: 20880是硬性要求若多个服务在同一台机器启动必须为每个服务分配不同Dubbo端口否则端口冲突导致服务注册失败。base-packages路径必须精确指向实现类包漏写impl会导致Dubbo无法发现服务。2.3 Nacos配置中心的实际应用方式service_hosp/src/main/resources/bootstrap.yml注意是bootstrap.yml而非application.yml定义配置中心接入spring: cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml group: HOSP_GROUP # 配置分组隔离挂号相关配置 namespace: 5c9a1e2f-3b4d-4a8c-9e1f-2d3a4b5c6d7e # 命名空间ID对应Nacos控制台创建的命名空间实际配置存于Nacos控制台HOSP_GROUP分组下Data ID为service-hosp.yaml内容示例hosp: source: pool-size: 50 # 号源池大小动态调整依据 refresh-interval: 300000 # 5分钟刷新一次号源缓存 redis: lock: timeout: 10000 # 分布式锁超时时间毫秒 retry-times: 3 # 锁获取失败重试次数注意bootstrap.yml在应用启动最早期加载用于拉取application.yml中可能引用的配置如数据库密码。若此处Nacos地址错误应用将因无法获取配置而启动失败日志中会出现NacosConfigService连接超时错误。3. 分布式挂号核心流程号源锁定与事务一致性实现3.1 挂号请求的完整链路与跨服务调用用户发起挂号请求时service_user作为前端网关服务接收请求但不直接操作号源。其核心逻辑在UserController.java中PostMapping(/order) public ResultOrderVO createOrder(RequestBody OrderDTO dto) { // 1. 校验用户状态调用自身UserService ResultUserVO userResult userService.getUserById(dto.getUserId()); if (!userResult.isSuccess()) return Result.fail(用户不存在); // 2. 调用service_hosp获取号源信息Dubbo远程调用 ResultHospSourceVO sourceResult hospSourceService.getAvailableSource( dto.getHospId(), dto.getDeptId(), dto.getDoctorId(), dto.getOrderDate() ); if (!sourceResult.isSuccess()) return Result.fail(sourceResult.getMsg()); // 3. 执行挂号本地事务远程服务调用 return orderService.createOrder(dto); // 此方法内含分布式事务协调 }3.1.1 Dubbo跨服务调用的序列化陷阱hospSourceService.getAvailableSource(...)调用实际走Dubbo协议参数OrderDTO需满足序列化要求。检查common_util/src/main/java/com/example/dto/OrderDTO.javaData Builder NoArgsConstructor AllArgsConstructor public class OrderDTO implements Serializable { // 必须实现Serializable private static final long serialVersionUID 1L; // 显式声明serialVersionUID private Long userId; private Long hospId; private Long deptId; private Long doctorId; private LocalDate orderDate; }若遗漏Serializable接口或serialVersionUIDDubbo反序列化时会抛出ClassNotFoundException或InvalidClassException错误日志显示Failed to deserialize object。这是微服务间DTO传递最常见的坑。3.2 分布式号源锁定的三重保障机制挂号本质是“抢号”必须解决并发冲突。本项目采用Redis分布式锁数据库乐观锁号源缓存预热三层防护层级技术实现作用失败处理Redis锁setIfAbsent(key, value, expire)粗粒度锁定整个号源池如lock:hosp:1001:dept:2001:date:20240520获取失败则返回“号源繁忙”前端提示重试数据库乐观锁UPDATE hosp_source SET remain_count remain_count - 1 WHERE id ? AND remain_count 0 AND version ?细粒度校验剩余号数version字段防止ABA问题SQL影响行数为0时抛出NoAvailableSourceException号源缓存预热启动时PostConstruct加载未来7天号源到Redis减少数据库查询压力提升响应速度缓存失效时自动回源查询并刷新service_hosp/src/main/java/com/example/service/hosp/impl/HospSourceServiceImpl.java中关键方法Override Transactional(rollbackFor Exception.class) public ResultBoolean reserveSource(Long sourceId, Long userId) { // 1. Redis锁锁粒度单个号源ID String lockKey lock:source: sourceId; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS); if (!locked) { return Result.fail(号源锁定中请稍后重试); } try { // 2. 数据库乐观锁更新 HospSource source hospSourceMapper.selectById(sourceId); if (source.getRemainCount() 0) { return Result.fail(号源已约满); } int rows hospSourceMapper.updateRemainCount( sourceId, source.getRemainCount() - 1, source.getVersion() // 传入当前version ); if (rows 0) { return Result.fail(号源已被抢刷新页面重试); } // 3. 更新Redis缓存 redisTemplate.opsForValue().decrement(hosp:source: sourceId :remain, 1); return Result.success(true); } finally { redisTemplate.delete(lockKey); // 必须释放锁 } }3.3 分布式事务的轻量级解决方案挂号涉及service_user扣用户余额、service_hosp减号源、service_order生成订单三条链路。项目未使用Seata等重量级方案而是采用本地消息表定时任务补偿service_order模块中order_message表存储待发送消息service_user扣款成功后向该表插入一条状态为SENDING的消息独立的MessageSenderJob定时扫描SENDING消息调用service_hosp的Dubbo接口若调用失败消息状态变更为FAILED后续重试最多3次成功后状态变更为SENT。service_order/src/main/java/com/example/job/MessageSenderJob.java核心逻辑Scheduled(fixedDelay 5000) // 每5秒执行一次 public void sendMessages() { ListOrderMessage messages messageMapper.selectByStatus(SENDING, 100); for (OrderMessage msg : messages) { try { // 调用service_hosp的Dubbo接口 ResultBoolean result hospOrderService.confirmOrder(msg.getOrderId()); if (result.isSuccess()) { messageMapper.updateStatus(msg.getId(), SENT); } else { int retryTimes msg.getRetryTimes() 1; if (retryTimes 3) { messageMapper.updateStatus(msg.getId(), FAILED); } else { messageMapper.updateRetry(msg.getId(), retryTimes); } } } catch (Exception e) { log.error(消息发送失败orderId{}, msg.getOrderId(), e); } } }提示fixedDelay值需根据业务吞吐量调整。若挂号峰值QPS为2005秒内需处理完1000条消息否则消息积压。建议监控order_message表中statusSENDING的记录数超过阈值触发告警。4. 关键组件配置与生产环境适配要点4.1 Nacos高可用部署与服务发现优化单机Nacos127.0.0.1:8848仅适用于开发。生产环境必须集群部署本项目预留了配置扩展点。service_user/src/main/resources/application-prod.yml中spring: cloud: nacos: discovery: server-addr: 192.168.1.10:8848,192.168.1.11:8848,192.168.1.12:8848 # 三个Nacos节点 heartbeat-interval-ms: 5000 # 心跳间隔避免频繁探测 ephemeral: true # 临时实例宕机自动剔除必须修改的三项参数server-addr替换为真实Nacos集群IP列表用英文逗号分隔heartbeat-interval-ms默认30秒心跳易导致服务误剔除生产建议设为5秒ephemeral设为true默认值确保服务宕机后Nacos自动注销避免僵尸实例。验证服务注册是否成功访问http://nacos-ip:8848/nacos在“服务列表”中搜索service-user应看到多个实例对应不同服务器IP且健康状态为UP。4.2 Redis分布式锁的可靠性增强原生setIfAbsent存在锁过期后业务未执行完、其他线程获取锁导致并发问题的风险。项目在common_util/src/main/java/com/example/util/RedisLockUtil.java中实现了看门狗机制public class RedisLockUtil { private static final String LOCK_SUCCESS OK; private static final Long LOCK_EXPIRE_TIME 30L; // 锁默认30秒 public static boolean tryLock(RedisTemplateString, Object redisTemplate, String lockKey, String requestId, long expireTime) { String script if redis.call(set, KEYS[1], ARGV[1], NX, PX, ARGV[2]) then return 1 else return 0 end; Object result redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), requestId, String.valueOf(expireTime)); return (Long) result 1L; } // 看门狗后台线程定期续期 public static void keepAlive(RedisTemplateString, Object redisTemplate, String lockKey, String requestId, long expireTime) { new Thread(() - { while (isLocked(redisTemplate, lockKey, requestId)) { try { Thread.sleep(expireTime / 3); // 每1/3过期时间续期一次 redisTemplate.expire(lockKey, expireTime, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }).start(); } }在HospSourceServiceImpl.reserveSource()中调用// 获取锁后立即启动看门狗 if (RedisLockUtil.tryLock(redisTemplate, lockKey, requestId, 30000)) { RedisLockUtil.keepAlive(redisTemplate, lockKey, requestId, 30000); // ... 执行业务逻辑 }注意看门狗线程必须与业务线程共享同一requestId否则续期操作会失败。requestId建议用UUID生成确保全局唯一。4.3 MySQL连接池与慢SQL治理service_hosp/src/main/resources/application.yml中Druid连接池配置spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20生产必须调整的参数max-active: 根据服务器CPU核数设置公式为2 × CPU核数 有效等待线程数。例如4核服务器挂号业务平均响应时间200ms则max-active ≈ 2×4 (1000ms/200ms)×2 8 10 18设为20合理time-between-eviction-runs-millis: 连接空闲检测间隔设为60秒60000避免频繁探活validation-query: Oracle用SELECT 1 FROM DUALMySQL必须改为SELECT 1否则启动报错。慢SQL治理在service_hosp/src/main/resources/mapper/HospSourceMapper.xml中getAvailableSource查询添加索引提示select idgetAvailableSource resultTypeHospSourceVO /* INDEX(hosp_source idx_hosp_dept_date) */ !-- 强制使用复合索引 -- SELECT * FROM hosp_source WHERE hosp_id #{hospId} AND dept_id #{deptId} AND order_date #{orderDate} AND remain_count 0 ORDER BY doctor_id, create_time /select对应MySQL索引语句CREATE INDEX idx_hosp_dept_date ON hosp_source(hosp_id, dept_id, order_date) WHERE remain_count 0;5. 本地调试与多服务联调实战技巧5.1 使用Maven Profiles快速切换环境项目根目录pom.xml定义了dev、test、prod三个Profileprofiles profile iddev/id properties spring.profiles.activedev/spring.profiles.active /properties activation activeByDefaulttrue/activeByDefault /activation /profile profile idprod/id properties spring.profiles.activeprod/spring.profiles.active /properties /profile /profiles启动service_user时指定Profile# 开发环境默认 mvn spring-boot:run -pl service_user # 生产环境读取application-prod.yml mvn spring-boot:run -pl service_user -Pprod # 同时启动两个服务需先install公共模块 mvn clean install -Dmaven.test.skiptrue -pl common_util,service_util mvn spring-boot:run -pl service_user mvn spring-boot:run -pl service_hosp 提示-pl参数指定要构建的模块符号后台运行。若端口冲突如两个服务都用8080需在各自application.yml中修改server.port。5.2 接口测试与数据准备脚本service_user/src/test/java/com/example/service/user/UserControllerTest.java提供JUnit测试用例但更实用的是Postman集合。项目附带postman_collection.json位于根目录包含用户登录接口POST /user/login查询可挂号科室GET /hosp/{hospId}/dept创建挂号订单POST /order关键测试数据准备在MySQL中插入测试医院INSERT INTO hospital (id, name, address) VALUES (1001, 第一人民医院, 北京市朝阳区XX路1号);初始化号源service_hosp/src/main/resources/sql/init_hosp_source.sqlINSERT INTO hosp_source (id, hosp_id, dept_id, doctor_id, order_date, total_count, remain_count, version) VALUES (1, 1001, 2001, 3001, 2024-05-20, 50, 50, 1);启动Nacos后在控制台创建HOSP_GROUP分组并上传service-hosp.yaml配置。5.3 日志追踪与问题定位分布式环境下一次挂号请求横跨3个服务需统一TraceID。项目使用spring-cloud-starter-sleuth已在父POM声明但需手动注入RestController RequestMapping(/order) public class OrderController { private final Logger logger LoggerFactory.getLogger(OrderController.class); PostMapping public ResultOrderVO createOrder(RequestBody OrderDTO dto) { // Sleuth自动生成traceId打印到日志 logger.info(开始处理挂号请求traceId{}, userId{}, Tracer.currentSpan().context().traceIdString(), dto.getUserId()); // ... 业务逻辑 } }查看日志时搜索traceId即可串联所有服务日志。若发现service_hosp返回号源已约满但数据库remain_count仍为正说明Redis缓存与DB不一致需检查hospSourceService.reserveSource()中Redis decrement操作是否被异常中断。验证Redis缓存一致性# 进入Redis CLI redis-cli -h 127.0.0.1 -p 6379 # 查看号源剩余数key格式hosp:source:{id}:remain 127.0.0.1:6379 GET hosp:source:1:remain 49 # 对比数据库 mysql SELECT remain_count FROM hosp_source WHERE id 1; -------------- | remain_count | -------------- | 49 | --------------若两者不一致说明缓存更新逻辑有缺陷需检查reserveSource()方法中redisTemplate.opsForValue().decrement(...)是否在事务回滚时被忽略。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻