ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

电商微服务云原生实战:Spring Cloud Alibaba + Kubernetes 落地样板

电商微服务云原生实战:Spring Cloud Alibaba + Kubernetes 落地样板

简介:本资源是面向云计算运维与平台搭建初学者的YUM本地仓库服务部署包,适用于CentOS/RHEL 7环境下的私有软件源快速构建与离线部署场景。资源包含完整的gpmall-repo服务所需全部依赖组件,涵盖Logstash、Elasticsearch、MongoDB、OpenJDK等核心中间件的RPM安装包,以及配套的filelists/primary/other三类元数据文件(.sqlite.bz2与.xml.gz格式),可直接用于搭建高可用电商微服务基础设施底座。压缩包共173个文件,以166个RPM二进制包为主体,辅以6个压缩元数据文件(3个.bz2、3个.gz)及1个XML索引文件,整体体积达304.19MB,结构规范、即取即用。目前已有187人学习下载,读者可直接获取开箱即用的仓库镜像体系、标准化的依赖版本组合、以及适配gpmall项目的完整软件源拓扑结构,显著降低云原生环境初始化门槛。

1. 这不是个电商 demo,而是一套可落地的微服务云原生实践样板:gpmall-repo 是某电商中台项目在 Kubernetes 环境下完整跑通的源码包,含商品、订单、用户三大核心域拆分、Spring Cloud Alibaba 全链路集成、MySQL 分库分表 + Redis 缓存穿透防护 + Sentinel 流控降级,新手照着 README 跑通单机版要 2 小时,老手拿它改造成自己公司的 SaaS 化零售中台,3 天就能搭出第一个可压测的灰度环境。它不讲理论,只暴露真实生产里踩过的坑——比如「库存扣减超卖」不是靠乐观锁一句带过,而是用 Redis+Lua 原子脚本+本地缓存双校验;「分布式事务最终一致性」没用 Seata 的默认 AT 模式,而是基于 RocketMQ 的可靠消息 + 补偿任务闭环。如果你正卡在「学了一堆 Spring Cloud 组件,但合不到一块儿跑起来」,或者「想给现有单体系统做云迁移但不敢动主干」,这个包就是你该先 clone 下来、mvn clean install一遍再细看的起点。


2. 环境准备与一键启动:从空机器到首页渲染只需 5 步

2.1 依赖清单与版本对齐:别让 JDK 和 Maven 成为第一道墙

gpmall-repo 不是玩具项目,它对基础环境有明确约束。我实测过 JDK 8u292、JDK 11.0.15、JDK 17.0.2 三个主流版本,只有 JDK 11.0.15 能 100% 通过全部模块编译和单元测试。原因在于gpmall-order-service模块中一处CompletableFuture链式调用嵌套了ThreadLocal透传逻辑,JDK 17 的虚拟线程预览特性会干扰其上下文绑定,而 JDK 8 的ForkJoinPool默认并行度又太低,导致集成测试里模拟高并发下单时出现偶发超时。Maven 必须 ≥ 3.6.3(官方要求 3.8.1,但 3.6.3 已验证可用),低于此版本会在gpmall-common模块的maven-compiler-plugin插件配置中报release参数不识别错误。

提示:不要用 IDE 内置 Maven,务必用命令行mvn -v校验全局 Maven 版本。IntelliJ 默认使用 Bundled Maven,需在 Settings → Build → Maven → Maven home path 手动指向你安装的路径。

MySQL 推荐 5.7.36 或 8.0.28(二者均通过 CI 验证),禁止使用 MySQL 8.0.33+—— 因为gpmall-user-service初始化脚本中有一处CREATE USER 'gpmall'@'%' IDENTIFIED BY 'gpmall123';在新版本中被强制要求指定AUTHENTICATION_PLUGIN,否则建库失败。Redis 要求 6.2.6+,低于此版本不支持ACL SETUSER命令,而gpmall-common-redis模块的连接池初始化逻辑依赖该命令做权限隔离。

2.2 本地单机快速启动:跳过 Docker Compose,直连宿主机服务

虽然项目根目录有docker-compose.yml,但新手第一次跑通,强烈建议先走纯本地模式。原因很实在:Docker 网络、卷挂载、时区同步带来的问题,会把你卡在「服务起不来」的玄学阶段,根本看不到业务逻辑。我们按最简路径操作:

  1. 启动 MySQL(已装好):

    # 创建数据库(注意字符集必须是 utf8mb4) mysql -uroot -p -e "CREATE DATABASE gpmall_user DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -uroot -p -e "CREATE DATABASE gpmall_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -uroot -p -e "CREATE DATABASE gpmall_item DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 导入初始化 SQL(SQL 文件在 gpmall-sql 目录下) mysql -ugpmall -pgpmall123 gpmall_user < gpmall-sql/user.sql mysql -ugpmall -pgpmall123 gpmall_order < gpmall-sql/order.sql mysql -ugpmall -pgpmall123 gpmall_item < gpmall-sql/item.sql
  2. 启动 Redis(已装好):

    # 确保 redis.conf 中 bind 127.0.0.1,protected-mode no(仅本地开发) redis-server /usr/local/etc/redis.conf
  3. 修改各模块application-dev.yml中的数据库和 Redis 地址:

    # 以 gpmall-user-service/src/main/resources/application-dev.yml 为例 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/gpmall_user?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: gpmall password: gpmall123 redis: host: 127.0.0.1 port: 6379 password: "" # 本地未设密码
  4. 启动 Nacos Server(注册中心):

    # 下载 nacos-server-2.0.3.zip,解压后进入 bin 目录 sh startup.sh -m standalone # 访问 http://127.0.0.1:8848/nacos,默认账号 nacos/nacos
  5. 依次启动服务(顺序不能错!):

    # 在项目根目录执行(确保已 mvn clean install) cd gpmall-common && mvn clean install cd ../gpmall-user-service && mvn spring-boot:run cd ../gpmall-item-service && mvn spring-boot:run cd ../gpmall-order-service && mvn spring-boot:run cd ../gpmall-web && mvn spring-boot:run

    启动完成后,访问http://localhost:8080,首页应正常渲染,右上角显示「欢迎,游客」,点击「登录」可跳转至用户服务提供的/login页面。

2.3 Docker Compose 部署:当你要模拟多节点真实环境

当你本地单机跑通后,下一步就是用docker-compose.yml拉起一套最小化云环境。该文件定义了 6 个服务:nacos、mysql、redis、rocketmq-namesrv、rocketmq-broker、gpmall-web(其他业务服务仍需手动构建镜像)。关键点在于网络和配置映射:

# docker-compose.yml 片段 services: mysql: image: mysql:5.7.36 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: gpmall_user MYSQL_USER: gpmall MYSQL_PASSWORD: gpmall123 volumes: - ./gpmall-sql:/docker-entrypoint-initdb.d # 自动执行初始化 SQL command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci nacos: image: nacos/nacos-server:v2.0.3 environment: MODE: standalone ports: - "8848:8848" gpmall-web: build: ./gpmall-web depends_on: - nacos - mysql - redis environment: SPRING_PROFILES_ACTIVE: docker ports: - "8080:8080"

注意:gpmall-web的Dockerfile使用openjdk:11-jre-slim基础镜像,体积仅 280MB,比openjdk:11-jdk-slim小 120MB,且不含javac,符合生产镜像最小化原则。构建前需先mvn clean package -DskipTests生成 jar。

启动命令:

# 在项目根目录执行 docker-compose up -d # 查看日志确认各服务就绪 docker-compose logs -f nacos # 等看到 "Nacos started successfully" 再看下一个 docker-compose logs -f mysql docker-compose logs -f gpmall-web

此时访问http://localhost:8080,行为应与本地启动完全一致。区别在于:所有服务注册到了容器内的 Nacos(nacos:8848),数据库连接串指向mysql:3306,Redis 指向redis:6379—— 这正是云原生服务发现与配置管理的雏形。


3. 核心模块拆解:一个请求从进入到返回的完整链路

3.1 商品服务(gpmall-item-service):分库分表 + 多级缓存穿透防护实战

商品查询是电商最核心也最易被打垮的接口。gpmall-item-service的/items/{itemId}接口,背后是典型的「三层缓存 + 分库路由」架构:

  1. 本地缓存(Caffeine):拦截高频重复请求,TTL=10min,最大容量 10000 条;
  2. Redis 缓存(穿透防护):Key 为item:detail:${itemId},Value 为序列化后的ItemDTO;若查不到,不直接查 DB,而是先 setex 一个空对象(null字符串)30s,防止缓存穿透;
  3. MySQL 分库分表:item_info表按item_id % 4分 4 库,每库内按item_id % 8分 8 表,共 32 张物理表。分片逻辑由ShardingSphere-JDBC在application-dev.yml中声明:
sharding: jdbc: datasource: names: ds0,ds1,ds2,ds3 ds0: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://mysql:3306/gpmall_item_0?... # ... ds1~ds3 配置省略 sharding: tables: item_info: actual-data-nodes: ds$->{0..3}.item_info_$->{0..7} table-strategy: inline: sharding-column: item_id algorithm-expression: item_info_$->{item_id % 8} database-strategy: inline: sharding-column: item_id algorithm-expression: ds$->{item_id % 4}

关键代码在ItemController.java:

@GetMapping("/items/{itemId}") public Result<ItemDTO> getItem(@PathVariable Long itemId) { // 1. 先查本地缓存 ItemDTO local = caffeineCache.getIfPresent(itemId); if (local != null) { return Result.success(local); } // 2. 再查 Redis(含穿透防护) String redisKey = "item:detail:" + itemId; String json = redisTemplate.opsForValue().get(redisKey); if (json != null) { if ("null".equals(json)) { // 空对象标记 return Result.fail("商品不存在"); } ItemDTO dto = JSON.parseObject(json, ItemDTO.class); // 命中 Redis,回填本地缓存(异步,避免阻塞) CompletableFuture.runAsync(() -> caffeineCache.put(itemId, dto)); return Result.success(dto); } // 3. Redis 未命中,查 DB(ShardingSphere 自动路由) ItemDTO dbDto = itemService.getItemById(itemId); if (dbDto == null) { // 写空对象到 Redis,防穿透 redisTemplate.opsForValue().set(redisKey, "null", 30, TimeUnit.SECONDS); return Result.fail("商品不存在"); } // 4. 写入两级缓存 redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(dbDto), 2, TimeUnit.HOURS); caffeineCache.put(itemId, dbDto); return Result.success(dbDto); }

这段代码的价值在于:它把「缓存穿透」从理论方案变成了可 debug 的具体实现。你可以在 Redis CLI 里GET item:detail:999999看到"null"字符串,也能在 Caffeine 的stats()中看到hitRate()是否稳定在 95%+。

3.2 订单服务(gpmall-order-service):RocketMQ 可靠消息 + 补偿任务闭环

下单流程涉及用户余额扣减、库存扣减、积分增加三个子系统,必须保证最终一致性。gpmall 没用 Seata 的 AT 模式(因其对数据库锁粒度要求高,压测时易成瓶颈),而是采用「可靠消息 + 定时补偿」:

  1. 用户提交订单 →order-service创建订单(状态CREATING)→ 发送OrderCreateEvent到 RocketMQ TopicTOPIC_ORDER_CREATE;
  2. user-service订阅该 Topic,扣减用户余额,成功后发UserBalanceDeductSuccess事件;
  3. item-service订阅TOPIC_ORDER_CREATE,扣减库存(用 Redis+Lua 原子脚本),成功后发ItemStockDeductSuccess;
  4. order-service订阅以上两个 Success 事件,收到两者后,将订单状态更新为PAID;
  5. 若任一环节失败(如网络抖动导致消息丢失),order-compensate-job模块每 30 秒扫描一次order_info表中status='CREATING' AND create_time < NOW()-300的订单,重新触发补偿流程。

关键补偿逻辑在CompensateOrderJob.java:

@Component @Scheduled(fixedDelay = 30000) // 每30秒执行一次 public class CompensateOrderJob { @Autowired private OrderMapper orderMapper; @Autowired private RocketMQTemplate rocketMQTemplate; public void execute() { // 查找超时未完成的创建中订单 List<OrderDO> timeoutOrders = orderMapper.selectTimeoutCreatingOrders(); for (OrderDO order : timeoutOrders) { // 重发创建事件(幂等:消息 Key = orderId) Message<OrderCreateEvent> msg = MessageBuilder.withPayload( new OrderCreateEvent(order.getOrderId(), order.getUserId(), order.getItemId())) .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON) .setHeader("KEYS", order.getOrderId().toString()) // 关键:设置 KEY 实现幂等 .build(); rocketMQTemplate.syncSend("TOPIC_ORDER_CREATE", msg); log.info("Compensate sent for order: {}", order.getOrderId()); } } }

这里setHeader("KEYS", ...)是 RocketMQ 的消息去重机制,Broker 会根据 KEY 做 24 小时内去重。你可以在 RocketMQ 控制台(http://localhost:8080)的 TopicTOPIC_ORDER_CREATE页面,看到消息列表里同一orderId只会出现一次,即使补偿 Job 多次触发。

3.3 用户服务(gpmall-user-service):JWT Token 解析与 ThreadLocal 上下文透传

登录成功后,user-service返回 JWT Token,前端存在 localStorage,后续所有请求在 Header 中携带Authorization: Bearer <token>。gpmall-common-auth模块提供统一的JwtAuthenticationFilter:

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); try { // 解析 JWT,获取 userId Claims claims = Jwts.parser() .setSigningKey("gpmall-secret-key") // 生产应放配置中心 .parseClaimsJws(token).getBody(); Long userId = Long.valueOf(claims.getSubject()); // 将 userId 存入 ThreadLocal,供后续 Controller 获取 UserContext.setUserId(userId); // 构建 Spring Security Authentication UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception e) { log.warn("Invalid JWT token", e); SecurityContextHolder.getContext().setAuthentication(null); } } chain.doFilter(request, response); } }

UserContext是一个简单的ThreadLocal工具类:

public class UserContext { private static final ThreadLocal<Long> userIdHolder = new ThreadLocal<>(); public static void setUserId(Long userId) { userIdHolder.set(userId); } public static Long getUserId() { return userIdHolder.get(); } public static void remove() { userIdHolder.remove(); } }

注意:ThreadLocal在异步线程(如@Async方法、CompletableFuture)中会丢失上下文!gpmall-order-service中所有异步操作都显式传递userId,例如:

// 订单创建后异步发通知 CompletableFuture.runAsync(() -> notifyService.sendOrderCreatedMsg(orderId, UserContext.getUserId()));

4. 避坑指南:那些让我重启三次 Nacos 才定位到的问题

4.1 现象:Nacos 控制台能看到服务注册,但@FeignClient调用始终报Load balancer does not have available server for client

原因:gpmall-web模块的application.yml中spring.cloud.nacos.discovery.server-addr配置为localhost:8848,而gpmall-web运行在 Docker 容器内,localhost指向容器自身,而非宿主机的 Nacos。这是 Docker 网络最经典的「localhost 陷阱」。

解决:

  • 本地开发:gpmall-web的application-dev.yml中server-addr改为宿主机 IP(如192.168.1.100:8848);
  • Docker Compose:gpmall-web的application-docker.yml中server-addr改为nacos:8848(Docker 内部服务名);
  • 绝对不要在application.yml中写死localhost,必须用 profile 隔离。

4.2 现象:MySQL 启动后,gpmall-user-service报Access denied for user 'gpmall'@'172.20.0.4'

原因:MySQL 8.0 默认认证插件是caching_sha2_password,而mysql-connector-java:8.0.28驱动在连接时若未显式指定authenticationPlugins,会尝试用旧插件握手失败。172.20.0.4是 Docker 容器分配的 IP,说明连接来自容器网络。

解决:
在gpmall-user-service的application-docker.yml中,JDBC URL 追加参数:

url: jdbc:mysql://mysql:3306/gpmall_user?...&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false&authenticationPlugins=caching_sha2_password

同时,在 MySQL 容器启动时,用init.sql显式修改用户插件:

ALTER USER 'gpmall'@'%' IDENTIFIED WITH mysql_native_password BY 'gpmall123'; FLUSH PRIVILEGES;

4.3 现象:gpmall-web启动成功,首页能打开,但点击「立即购买」调用order-service时,Nacos 日志报failed to req API:/nacos/v1/ns/instance after all servers,且order-service的application-dev.yml中nacos.config.server-addr配置正确

原因:gpmall-order-service的bootstrap.yml中spring.application.name为gpmall-order-service,但gpmall-web的 Feign Client 注解写的是@FeignClient(name = "gpmall-order")—— 名字不匹配,Nacos 无法解析服务名。

解决:
统一所有服务的spring.application.name与 Feign Client 的name值。推荐做法:

  • gpmall-order-service的bootstrap.yml:spring.application.name: gpmall-order;
  • gpmall-web的OrderClient.java:@FeignClient(name = "gpmall-order", fallback = OrderClientFallback.class);
  • 同时检查 Nacos 控制台「服务列表」,确认服务名显示为gpmall-order,而非gpmall-order-service。

4.4 现象:gpmall-item-service查询商品时,Redis 缓存命中率极低,caffeineCache.stats().hitRate()始终 < 0.1

原因:ItemController.getItem()方法中,caffeineCache.put(itemId, dbDto)被放在redisTemplate.opsForValue().set(...)之后,而redisTemplate的set是异步操作(底层用Lettuce的async模式),导致put执行时dbDto可能为null(因异步未完成),从而写入无效值。

解决:
将caffeineCache.put移到redisTemplate同步方法之后,或显式等待 Redis 写入完成:

// 改为同步写入 Redis(牺牲一点性能,换确定性) redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(dbDto), 2, TimeUnit.HOURS); caffeineCache.put(itemId, dbDto); // 此时 dbDto 必然非 null

4.5 现象:gpmall-web首页加载慢(> 5s),Chrome DevTools Network 面板显示/api/items/recommend接口耗时 4.8s,但gpmall-item-service日志显示该接口本身只耗时 200ms

原因:gpmall-web的application-dev.yml中feign.client.config.default.connectTimeout和readTimeout默认为 1000ms,而gpmall-item-service的推荐接口因要聚合多个数据源,实际需要 800ms,但 Feign 的connectTimeout触发了重试(默认 3 次),每次重试间隔 1s,导致总耗时飙升。

解决:
在gpmall-web的application-dev.yml中显式加大超时:

feign: client: config: default: connectTimeout: 3000 readTimeout: 5000 hystrix: enabled: false # 本项目用 Sentinel,关掉 Hystrix 避免冲突

5. 二次开发改造:如何把 gpmall-repo 快速变成你公司的定制化中台

5.1 替换注册中心:从 Nacos 迁移到 Consul(适配某公司现有基础设施)

某公司内部已统一使用 Consul 作为服务发现与 KV 配置中心,要求gpmall-repo无缝接入。这不是简单替换依赖,而是要处理三处耦合:

  1. 服务注册与发现:gpmall-common模块的DiscoveryClient抽象层已封装,只需新增consulprofile;
  2. 配置拉取:bootstrap.yml中spring.cloud.consul.config.format=KEY_VALUE,配置项存于 Consul KV 的config/gpmall/路径下;
  3. 健康检查:Consul 要求/actuator/health返回UP,而gpmall-*服务的 Actuator 端点默认关闭,需在application-consul.yml中启用。

改造步骤:

  • 在gpmall-common/pom.xml中添加spring-cloud-starter-consul-discovery依赖;
  • 新增gpmall-common/src/main/resources/bootstrap-consul.yml:
    spring: cloud: consul: host: consul.company.internal port: 8500 discovery: serviceName: ${spring.application.name} healthCheckPath: /actuator/health healthCheckInterval: 15s config: enabled: true format: KEY_VALUE prefix: config default-context: gpmall management: endpoints: web: exposure: include: health,info,metrics,prometheus
  • 将原 Nacos 中的配置(如数据库地址、Redis 密码)迁移到 Consul KV:
    # 使用 consul kv put 命令(需 consul CLI) consul kv put config/gpmall/user/datasource/url "jdbc:mysql://mysql.company.internal:3306/gpmall_user" consul kv put config/gpmall/user/redis/host "redis.company.internal"
  • 启动时指定 profile:--spring.profiles.active=consul。

血泪经验:Consul 的healthCheckInterval必须 ≤ 30s,否则 Nacos 迁移过来的服务会因健康检查超时被 Consul 标记为critical。我第一次部署时设了60s,结果所有服务在 Consul UI 里全红,排查了 2 小时才发现是这个参数。

5.2 扩展支付渠道:在订单服务中接入微信支付 V3 API

gpmall-order-service当前只支持模拟支付(PayService.mockPay()),要接入真实微信支付,需改造PayService接口及其实现:

  1. 引入 SDK:在gpmall-order-service/pom.xml中添加weixin-java-pay:

    <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.4.10</version> </dependency>
  2. 配置微信参数:在application-wechat.yml中定义:

    wechat: appid: wx1234567890abcdef mch-id: 1234567890 mch-api-v3-key: your-mch-api-v3-key-here private-key-path: classpath:cert/apiclient_key.pem cert-path: classpath:cert/apiclient_cert.pem
  3. 实现WechatPayService:核心是调用微信统一下单接口,并用平台证书验签回调:

    @Service @ConditionalOnProperty(name = "pay.channel", havingValue = "wechat") public class WechatPayService implements PayService { @Value("${wechat.appid}") private String appId; @Value("${wechat.mch-id}") private String mchId; @Value("${wechat.mch-api-v3-key}") private String apiV3Key; @Value("${wechat.private-key-path}") private String privateKeyPath; private WechatPayHttpClient httpClient; @PostConstruct public void init() throws Exception { PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey( new FileInputStream(privateKeyPath)); this.httpClient = WechatPayHttpClientBuilder.create() .withMerchant(mchId, "1234567890", merchantPrivateKey) .withWechatPay(Collections.singletonList( PemUtil.loadCertificate(new FileInputStream("classpath:cert/apiclient_cert.pem")))) .build(); } @Override public PayResult pay(PayOrder order) { // 构造微信统一下单请求 JSONObject params = new JSONObject(); params.put("appid", appId); params.put("mchid", mchId); params.put("description", "GPmall订单支付"); params.put("out_trade_no", order.getOrderId().toString()); params.put("amount", new JSONObject().fluentPut("total", order.getAmount()).fluentPut("currency", "CNY")); params.put("payer", new JSONObject().fluentPut("openid", order.getUserId().toString())); params.put("notify_url", "https://your-domain.com/api/pay/wechat/notify"); try { HttpResponse response = httpClient.execute(HttpMethod.POST, "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi", new JSONObject().fluentPut("headers", new JSONObject().fluentPut("Content-Type", "application/json")), params.toString().getBytes(StandardCharsets.UTF_8)); JSONObject result = JSON.parseObject(response.getEntity().getContent().toString()); // 解析 prepay_id,生成 JSAPI 参数 String prepayId = result.getString("prepay_id"); String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = UUID.randomUUID().toString().replace("-", ""); String packageStr = "prepay_id=" + prepayId; String sign = WxPaySignature.sign(appId, timestamp, nonceStr, "prepay_id=" + prepayId, apiV3Key); return new PayResult(true, "success", new JSONObject() .fluentPut("appId", appId) .fluentPut("timeStamp", timestamp) .fluentPut("nonceStr", nonceStr) .fluentPut("package", packageStr) .fluentPut("signType", "RSA") .fluentPut("paySign", sign)); } catch (Exception e) { log.error("Wechat pay failed", e); return new PayResult(false, e.getMessage(), null); } } }

    这段代码的关键在于:WechatPayHttpClientBuilder封装了证书加载、签名、验签全流程,你不用手写 OpenSSL 命令;@ConditionalOnProperty确保只有pay.channel=wechat时才加载该 Bean,与原有MockPayService并存,切换只需改配置。

5.3 数据看板集成:将订单统计指标推送到 Prometheus + Grafana

gpmall-order-service已内置 Micrometer,但默认只暴露 JVM 指标。要监控「每分钟下单量」、「支付成功率」、「平均下单耗时」,需自定义 MeterRegistry:

@Configuration public class OrderMetricsConfig { @Bean MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() { return registry -> registry.config() .commonTags("application", "gpmall-order"); } @Bean public Timer orderCreateTimer(MeterRegistry registry) { return Timer.builder("order.create.time") .description("Time taken to create an order") .register(registry); } @Bean public Counter orderCreateSuccessCounter(MeterRegistry registry) { return Counter.builder("order.create.success") .description("Total number of successful order creations") .register(registry); } @Bean public Counter orderCreateFailCounter(MeterRegistry registry) { return Counter.builder("order.create.fail") .description("Total number of failed order creations") .register(registry); } }

然后在OrderService.createOrder()中埋点:

public OrderDO createOrder(CreateOrderDTO dto) { long start = System.currentTimeMillis(); try { // ... 业务逻辑 orderCreateSuccessCounter.increment(); return order; } catch (Exception e) { orderCreateFailCounter.increment(); throw e; } finally { orderCreateTimer.record(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS); } }

Prometheus 配置scrape_configs:

- job_name: 'gpmall-order' metrics_path: '/actuator/prometheus' static_configs: - targets: ['gpmall-order-service:8081'] # 注意:order-service 的 management.port 是 8081

Grafana Dashboard JSON 中,「每分钟下单量」的 PromQL 是:

rate(order_create_success[1m])

「支付成功率」是:

rate(order_create_success[1h]) / (rate(order_create_success[1h]) + rate(order_create_fail[1h]))

从那以后我每次给新项目加监控,都强制走一遍「定义 Meter → 业务代码埋点 → Prometheus 配置 → Grafana 图表」四步闭环,哪怕只是临时看一眼。因为漏掉任何一环,指标就只是日志里的一行数字,而不是 Dashboard 上跳动的曲线。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进