From 71ed87371bad848cff1444ba5026d9b226b1a2d4 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Wed, 3 Jun 2026 22:22:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor(deploy):=20=E5=8D=87=E7=BA=A7=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=BC=98=E9=9B=85=E5=90=AF=E5=81=9C=E3=80=81=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E6=A3=80=E6=9F=A5=E4=B8=8E=E8=87=AA=E5=8A=A8=E5=9B=9E?= =?UTF-8?q?=E6=BB=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 升级部署脚本为运维级实现,新增以下核心特性: - 优雅启停 (SIGTERM → 等待 → SIGKILL 兜底) - 健康检查 + 自动回滚 - 版本备份,保留最近 5 个版本 - 并发锁防止重复执行 - 前置检查 (磁盘/内存/端口/编译产物) - 结构化日志 同时启用 Spring Boot 优雅关闭功能,配置 30 秒超时以等待请求和后台任务完成。 --- script/deploy/deploy-all.sh | 536 ++++++++++++++---- .../src/main/resources/application-local.yaml | 7 + 2 files changed, 448 insertions(+), 95 deletions(-) diff --git a/script/deploy/deploy-all.sh b/script/deploy/deploy-all.sh index 5581f1af15..a2906c1c6a 100644 --- a/script/deploy/deploy-all.sh +++ b/script/deploy/deploy-all.sh @@ -1,127 +1,473 @@ #!/bin/bash # ============================================ -# 芋道 (Yudao) 全量部署脚本 - 生产增强版 +# 芋道 (Yudao) 生产部署脚本 - 运维级 # ============================================ -set -e +# 特性: +# - 优雅启停 (SIGTERM → 等待 → SIGKILL 兜底) +# - 健康检查 + 自动回滚 +# - 版本备份 + 保留最近 5 个版本 +# - 并发锁,防止同时执行 +# - 前置检查 (磁盘/内存/端口/编译产物) +# - 结构化日志 +# ============================================ +set -o pipefail -# ==================== 1. 核心路径配置 ==================== -PROJECT_DIR="/www/wwwroot/sionrui" # 源码目录 -BACKEND_DIR="/www/wwwroot/yudao-server" # 后端运行目录 -FRONTEND_DIR="/www/wwwroot/muyetools.cn" # 用户端前端 (web-gold) -ADMIN_DIR="/www/wwwroot/8.155.172.147" # 管理后台前端 (yudao-ui-admin-vue3) +# ==================== 配置 ==================== +readonly PROJECT_DIR="/www/wwwroot/sionrui" +readonly BACKEND_DIR="/www/wwwroot/yudao-server" +readonly FRONTEND_DIR="/www/wwwroot/muyetools.cn" +readonly ADMIN_DIR="/www/wwwroot/8.155.172.147" -BACKEND_JAR_NAME="sion-rui.jar" -BUILD_LOG="/tmp/gitea_build_$(date +%Y%m%d).log" +readonly BACKEND_JAR="sion-rui.jar" +readonly BACKEND_PORT=9900 +readonly BACKUP_KEEP=5 # 保留最近 N 个版本 +readonly SHUTDOWN_GRACE_SEC=30 # 优雅关闭等待上限 +readonly HEALTH_CHECK_RETRIES=30 # 健康检查重试次数 (30 × 2s = 60s) +readonly HEALTH_CHECK_INTERVAL=2 # 健康检查间隔(秒) -# ==================== 2. 环境强制指定 ==================== -# 强制指定 JDK 17 (根据你的截图) -export JAVA_HOME="/www/server/java/jdk-17.0.8" -export PATH=$JAVA_HOME/bin:$PATH +readonly DEPLOY_LOCK="/tmp/yudao_deploy.lock" +readonly BUILD_LOG="/tmp/yudao_build_$(date +%Y%m%d_%H%M%S).log" +readonly DEPLOY_LOG="/tmp/yudao_deploy.log" +readonly BACKUP_DIR="$BACKEND_DIR/backups" -# 自动寻找 Maven 路径 -MVN_EXEC=$(which mvn || find /www/server -name "mvn" | head -n 1 || echo "mvn") - -# 限制内存防止 OOM,设置 Maven 多线程加速编译 +# Java 环境 +export JAVA_HOME="/www/server/java/jdk-17.0.8" +export PATH="$JAVA_HOME/bin:$PATH" export MAVEN_OPTS="-Xms512m -Xmx1024m" export NODE_OPTIONS="--max-old-space-size=2048" -# 指定本地仓库(解决 www 用户权限问题) +# Maven +MVN_EXEC=$(which mvn 2>/dev/null || find /www/server -name "mvn" -type f 2>/dev/null | head -1 || echo "mvn") MAVEN_REPO="$PROJECT_DIR/.m2_repo" -mkdir -p $MAVEN_REPO && chmod -R 777 $MAVEN_REPO -# ==================== 3. 工具函数 ==================== -log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $BUILD_LOG -} +# ==================== 工具函数 ==================== +log() { echo "[$(date '+%H:%M:%S')] $1" | tee -a "$DEPLOY_LOG"; } +success() { echo "[$(date '+%H:%M:%S')] ✅ $1" | tee -a "$DEPLOY_LOG"; } +warn() { echo "[$(date '+%H:%M:%S')] ⚠️ $1" | tee -a "$DEPLOY_LOG"; } +error() { echo "[$(date '+%H:%M:%S')] ❌ $1" | tee -a "$DEPLOY_LOG"; } +die() { error "$1"; release_lock; exit 1; } -# ==================== 4. 后端部署逻辑 ==================== -deploy_backend() { - log "🚀 [后端] 开始部署流程..." - - cd $PROJECT_DIR - log "1.1 同步最新代码..." - git fetch origin - git reset --hard origin/main - - log "1.2 开始多线程构建 (4核加速)..." - # -T 1C 表示每个 CPU 核心一个线程,显著缩短 99% CPU 占用的时长 - $MVN_EXEC clean package -DskipTests -pl yudao-server -am -T 1C -Dmaven.repo.local=$MAVEN_REPO >> $BUILD_LOG 2>&1 - - log "1.3 清理旧进程与文件锁..." - # 查找并强杀旧 JAR 进程 - OLD_PID=$(ps -ef | grep "$BACKEND_JAR_NAME" | grep -v "grep" | awk '{print $2}') - [ -z "$OLD_PID" ] || kill -9 $OLD_PID - sleep 2 - - log "1.4 复制 JAR 包 (安全覆盖)..." - SOURCE_JAR="$PROJECT_DIR/yudao-server/target/$BACKEND_JAR_NAME" - if [ ! -f "$SOURCE_JAR" ]; then - log "❌ 编译失败:未在 $SOURCE_JAR 找到文件!请检查 $BUILD_LOG" - exit 1 +# ==================== 并发锁 ==================== +acquire_lock() { + if [ -f "$DEPLOY_LOCK" ]; then + local pid + pid=$(cat "$DEPLOY_LOCK" 2>/dev/null) + if ps -p "$pid" > /dev/null 2>&1; then + die "部署脚本已在运行中 (PID=$pid),请等待完成或手动删除 $DEPLOY_LOCK" + fi + warn "发现残留锁文件(进程 $pid 已不存在),清理后继续" + rm -f "$DEPLOY_LOCK" fi - - mkdir -p $BACKEND_DIR/logs - rm -f "$BACKEND_DIR/$BACKEND_JAR_NAME" - cp -f "$SOURCE_JAR" "$BACKEND_DIR/" - - log "1.5 后台启动服务..." - cd $BACKEND_DIR - # 完全脱离终端重定向,防止脚本挂起 - # 注意:JVM参数(-Xms/-Xmx)必须在 -jar 之前 - nohup $JAVA_HOME/bin/java -Xms512m -Xmx1024m -jar $BACKEND_JAR_NAME --server.port=9900 > ./logs/console.log 2>&1 & - - log "✅ 后端启动成功 (PID: $!)" + echo $$ > "$DEPLOY_LOCK" } -# ==================== 5. 用户端前端部署逻辑 ==================== +release_lock() { + rm -f "$DEPLOY_LOCK" +} + +# 确保退出时释放锁 +trap release_lock EXIT INT TERM + +# ==================== 前置检查 ==================== +pre_flight() { + log "=== 前置检查 ===" + + # 磁盘空间 (至少 2GB) + local avail + avail=$(df -m "$BACKEND_DIR" | awk 'NR==2 {print $4}') + if [ "${avail:-0}" -lt 2048 ]; then + die "磁盘空间不足:${BACKEND_DIR} 可用 ${avail}MB,需要至少 2048MB" + fi + log " 磁盘可用: ${avail}MB ✓" + + # 内存 (至少 512MB 空闲) + local mem_free + mem_free=$(free -m | awk '/Mem:/ {print $7}') + if [ "${mem_free:-0}" -lt 512 ]; then + warn "可用内存偏低:${mem_free}MB,构建可能失败" + else + log " 可用内存: ${mem_free}MB ✓" + fi + + # JDK + if [ ! -x "$JAVA_HOME/bin/java" ]; then + die "JDK 未找到:$JAVA_HOME/bin/java" + fi + log " JDK: $($JAVA_HOME/bin/java -version 2>&1 | head -1) ✓" + + # 项目目录 + if [ ! -d "$PROJECT_DIR/.git" ]; then + die "项目目录不存在或不是 Git 仓库:$PROJECT_DIR" + fi + log " 项目目录: $PROJECT_DIR ✓" + + # 端口 (是否被非本应用的进程占用) + local port_pid + port_pid=$(ss -tlnp 2>/dev/null | grep ":$BACKEND_PORT " | grep -oP 'pid=\K\d+' || true) + if [ -n "$port_pid" ]; then + local port_cmd + port_cmd=$(ps -p "$port_pid" -o comm= 2>/dev/null || echo "unknown") + if ! echo "$port_cmd" | grep -q "java"; then + die "端口 $BACKEND_PORT 被非 Java 进程占用 (PID=$port_pid, cmd=$port_cmd)" + fi + log " 端口 $BACKEND_PORT: 被当前后端占用 (PID=$port_pid),将在停止阶段处理" + else + log " 端口 $BACKEND_PORT: 空闲 ✓" + fi + + success "前置检查通过" +} + +# ==================== 停止旧服务 ==================== +stop_service() { + log "=== 停止旧服务 ===" + + local old_pid + old_pid=$(ps -ef | grep "$BACKEND_JAR" | grep -v grep | awk '{print $2}') + + if [ -z "$old_pid" ]; then + log " 未发现运行中的旧进程" + return 0 + fi + + # 记录旧进程信息用于回滚 + local old_start + old_start=$(ps -p "$old_pid" -o lstart= 2>/dev/null || echo "unknown") + log " 旧进程: PID=$old_pid, 启动时间=$old_start" + + # Step 1: SIGTERM 优雅关闭 + log " 发送 SIGTERM ..." + kill -15 "$old_pid" 2>/dev/null || true + + local waited=0 + while [ $waited -lt $SHUTDOWN_GRACE_SEC ]; do + if ! ps -p "$old_pid" > /dev/null 2>&1; then + success "旧进程优雅退出 (PID=$old_pid, 耗时 ${waited}s)" + break + fi + sleep 1 + waited=$((waited + 1)) + # 每 10 秒汇报一次状态 + if [ $((waited % 10)) -eq 0 ] && [ $waited -gt 0 ]; then + log " 等待中... (${waited}s/${SHUTDOWN_GRACE_SEC}s)" + fi + done + + # Step 2: 仍未退出 → SIGKILL + if ps -p "$old_pid" > /dev/null 2>&1; then + warn "优雅关闭超时 (${SHUTDOWN_GRACE_SEC}s),执行 kill -9" + kill -9 "$old_pid" 2>/dev/null || true + sleep 3 + if ps -p "$old_pid" > /dev/null 2>&1; then + die "无法杀死进程 PID=$old_pid,请手动处理" + fi + log " 进程已强制终止" + fi + + # Step 3: 确认端口释放 + local retries=10 + while [ $retries -gt 0 ]; do + if ! ss -tlnp 2>/dev/null | grep -q ":$BACKEND_PORT "; then + log " 端口 $BACKEND_PORT 已释放 ✓" + return 0 + fi + sleep 2 + retries=$((retries - 1)) + done + + # 最后手段:fuser 释放端口 + warn "端口 $BACKEND_PORT 未自动释放,使用 fuser 强制释放" + fuser -k "${BACKEND_PORT}/tcp" 2>/dev/null || true + sleep 2 +} + +# ==================== 版本备份 ==================== +backup_current() { + if [ -f "$BACKEND_DIR/$BACKEND_JAR" ]; then + mkdir -p "$BACKUP_DIR" + local backup_name="${BACKEND_JAR%.jar}_$(date +%Y%m%d_%H%M%S).jar" + cp "$BACKEND_DIR/$BACKEND_JAR" "$BACKUP_DIR/$backpack_name" + log " 已备份: $backup_name" + + # 清理旧备份,只保留最近 N 个 + local count + count=$(ls -1t "$BACKUP_DIR"/*.jar 2>/dev/null | wc -l) + if [ "$count" -gt "$BACKUP_KEEP" ]; then + ls -1t "$BACKUP_DIR"/*.jar | tail -n +$((BACKUP_KEEP + 1)) | xargs rm -f + log " 清理旧备份,保留最近 $BACKUP_KEEP 个" + fi + fi +} + +# ==================== 回滚 ==================== +rollback() { + error "=== 健康检查失败,开始回滚 ===" + + # 停止新进程 + local new_pid="$1" + if [ -n "$new_pid" ] && ps -p "$new_pid" > /dev/null 2>&1; then + log " 停止新进程 PID=$new_pid" + kill -9 "$new_pid" 2>/dev/null || true + sleep 2 + fi + + # 恢复最近的备份 + local latest_backup + latest_backup=$(ls -1t "$BACKUP_DIR"/*.jar 2>/dev/null | head -1) + if [ -n "$latest_backup" ]; then + log " 恢复备份: $latest_backup" + cp -f "$latest_backup" "$BACKEND_DIR/$BACKEND_JAR" + else + error "没有可用的备份文件!" + return 1 + fi + + # 启动旧版本 + log " 启动旧版本..." + cd "$BACKEND_DIR" + nohup "$JAVA_HOME/bin/java" -Xms512m -Xmx1024m \ + -jar "$BACKEND_JAR" --server.port="$BACKEND_PORT" \ + > ./logs/console.log 2>&1 & + + local rollback_pid=$! + log " 回滚进程 PID=$rollback_pid" + + # 等待旧版本启动 + for i in $(seq 1 $HEALTH_CHECK_RETRIES); do + if curl -s -o /dev/null -w "%{http_code}" \ + "http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then + success "回滚成功 (PID=$rollback_pid)" + return 0 + fi + if ! ps -p "$rollback_pid" > /dev/null 2>&1; then + break + fi + sleep $HEALTH_CHECK_INTERVAL + done + + error "回滚失败!请手动检查!" + return 1 +} + +# ==================== 启动新服务 ==================== +start_service() { + log "=== 启动新服务 ===" + + cd "$BACKEND_DIR" + + # 确保日志目录 + mkdir -p "$BACKEND_DIR/logs" + + nohup "$JAVA_HOME/bin/java" \ + -Xms512m -Xmx1024m \ + -jar "$BACKEND_JAR" --server.port="$BACKEND_PORT" \ + > ./logs/console.log 2>&1 & + + local new_pid=$! + log " 新进程 PID=$new_pid" + + # 健康检查 + log " 等待健康检查..." + local started=false + for i in $(seq 1 $HEALTH_CHECK_RETRIES); do + if ! ps -p "$new_pid" > /dev/null 2>&1; then + error "新进程已退出!最近日志:" + tail -30 "$BACKEND_DIR/logs/console.log" | while read -r line; do + error " $line" + done + rollback "$new_pid" + die "启动失败,已回滚到上一版本" + fi + + if curl -s -o /dev/null -w "%{http_code}" \ + "http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then + success "后端启动成功 (PID=$new_pid, 耗时 $((i * HEALTH_CHECK_INTERVAL))s)" + started=true + break + fi + + # 每 5 次汇报进度 + if [ $((i % 5)) -eq 0 ]; then + log " 等待中... ($((i * HEALTH_CHECK_INTERVAL))s / $((HEALTH_CHECK_RETRIES * HEALTH_CHECK_INTERVAL))s)" + fi + sleep $HEALTH_CHECK_INTERVAL + done + + if [ "$started" != "true" ]; then + error "健康检查超时" + rollback "$new_pid" + die "启动超时,已回滚到上一版本" + fi +} + +# ==================== 后端部署 ==================== +deploy_backend() { + log "" + log "╔══════════════════════════════════════════╗" + log "║ 后端部署 ║" + log "╚══════════════════════════════════════════╝" + + # 0. 前置检查 + pre_flight + + # 1. 拉取代码 & 构建 + log "=== 构建 ===" + cd "$PROJECT_DIR" + + log " 拉取最新代码..." + git fetch origin 2>&1 | tee -a "$BUILD_LOG" + local old_commit new_commit + old_commit=$(git rev-parse --short HEAD) + git reset --hard origin/main 2>&1 | tee -a "$BUILD_LOG" + new_commit=$(git rev-parse --short HEAD) + + if [ "$old_commit" != "$new_commit" ]; then + log " 代码更新: ${old_commit} → ${new_commit}" + else + log " 代码无变化 (${old_commit})" + fi + + log " Maven 构建..." + mkdir -p "$MAVEN_REPO" && chmod -R 777 "$MAVEN_REPO" 2>/dev/null || true + if ! $MVN_EXEC clean package -DskipTests -pl yudao-server -am -T 1C \ + -Dmaven.repo.local="$MAVEN_REPO" >> "$BUILD_LOG" 2>&1; then + error "Maven 构建失败!查看:tail -100 $BUILD_LOG" + die "构建失败" + fi + success "Maven 构建完成" + + # 2. 检查产物 + local source_jar="$PROJECT_DIR/yudao-server/target/$BACKEND_JAR" + if [ ! -f "$source_jar" ]; then + die "编译产物不存在:$source_jar" + fi + log " 产物大小: $(du -h "$source_jar" | cut -f1)" + + # 3. 备份当前版本 + backup_current + + # 4. 停止旧服务 + stop_service + + # 5. 部署新 JAR + log "=== 部署 ===" + rm -f "$BACKEND_DIR/$BACKEND_JAR" + cp -f "$source_jar" "$BACKEND_DIR/" + log " JAR 已复制到 $BACKEND_DIR/" + + # 6. 启动 + start_service + + log "" + success "后端部署完成 ($old_commit → $new_commit)" +} + +# ==================== 前端部署 ==================== deploy_frontend() { - log "🚀 [用户端前端] 开始部署流程..." - cd $PROJECT_DIR/frontend + log "" + log "╔══════════════════════════════════════════╗" + log "║ 用户端前端部署 ║" + log "╚══════════════════════════════════════════╝" - log "2.1 安装依赖 (pnpm)..." - pnpm install >> $BUILD_LOG 2>&1 + log "=== 构建 ===" + cd "$PROJECT_DIR/frontend" - log "2.2 构建生产文件..." - pnpm build:gold >> $BUILD_LOG 2>&1 + log " pnpm install..." + pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败" - log "2.3 刷新 Web 目录..." - rm -rf $FRONTEND_DIR/* - cp -r $PROJECT_DIR/frontend/app/web-gold/dist/* $FRONTEND_DIR/ + log " pnpm build:gold..." + pnpm build:gold >> "$BUILD_LOG" 2>&1 || die "pnpm build:gold 失败" - log "✅ 用户端前端部署完成" + log "=== 部署 ===" + rm -rf "${FRONTEND_DIR:?}"/* + cp -r "$PROJECT_DIR/frontend/app/web-gold/dist/"* "$FRONTEND_DIR/" + success "用户端前端部署完成" } -# ==================== 5.1 管理后台前端部署逻辑 ==================== deploy_admin() { - log "🚀 [管理后台] 开始部署流程..." - cd $PROJECT_DIR/yudao-ui-admin-vue3 + log "" + log "╔══════════════════════════════════════════╗" + log "║ 管理后台前端部署 ║" + log "╚══════════════════════════════════════════╝" - log "3.1 安装依赖 (pnpm)..." - pnpm install >> $BUILD_LOG 2>&1 + log "=== 构建 ===" + cd "$PROJECT_DIR/yudao-ui-admin-vue3" - log "3.2 构建生产文件..." - pnpm build:prod >> $BUILD_LOG 2>&1 + log " pnpm install..." + pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败" - log "3.3 刷新管理后台目录..." - rm -rf $ADMIN_DIR/* - cp -r $PROJECT_DIR/yudao-ui-admin-vue3/dist-prod/* $ADMIN_DIR/ + log " pnpm build:prod..." + pnpm build:prod >> "$BUILD_LOG" 2>&1 || die "pnpm build:prod 失败" - log "✅ 管理后台部署完成" + log "=== 部署 ===" + rm -rf "${ADMIN_DIR:?}"/* + cp -r "$PROJECT_DIR/yudao-ui-admin-vue3/dist-prod/"* "$ADMIN_DIR/" + success "管理后台部署完成" } -# ==================== 6. 执行主流程 ==================== -# 清理可能残留的构建进程 -pkill -f "maven" || true +# ==================== 部署后摘要 ==================== +print_summary() { + log "" + log "╔══════════════════════════════════════════╗" + log "║ 部署摘要 ║" + log "╚══════════════════════════════════════════╝" + log " 构建日志: $BUILD_LOG" + log " 部署日志: $DEPLOY_LOG" + log " 版本备份: $BACKUP_DIR/ ($(ls -1 "$BACKUP_DIR"/*.jar 2>/dev/null | wc -l) 个)" + log " 控制台日志: $BACKEND_DIR/logs/console.log" -case "$1" in - backend-only) deploy_backend ;; - frontend-only) deploy_frontend ;; - admin-only) deploy_admin ;; - *) - deploy_backend - deploy_frontend - deploy_admin - ;; -esac + # 快速验证 + if curl -s -o /dev/null -w "%{http_code}" \ + "http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then + success "健康检查: UP" + else + warn "健康检查: DOWN (可能仍在启动中)" + fi -log "🏁 [$(date '+%H:%M:%S')] 部署流水线全部执行完毕!" \ No newline at end of file + # 磁盘 + local disk_use + disk_use=$(df -h "$BACKEND_DIR" | awk 'NR==2 {print $5}') + log " 磁盘使用: ${disk_use}" + + log "" +} + +# ==================== 主流程 ==================== +main() { + acquire_lock + + # 清理残留 Maven 进程 + pkill -f "maven" 2>/dev/null || true + + echo "" >> "$DEPLOY_LOG" + log "═══════════════════════════════════════════" + log " 部署开始 (模式: ${1:-full})" + log "═══════════════════════════════════════════" + + case "${1:-full}" in + backend-only) + deploy_backend + ;; + frontend-only) + deploy_frontend + ;; + admin-only) + deploy_admin + ;; + backend|full|"") + deploy_backend + deploy_frontend + deploy_admin + ;; + *) + echo "用法: $0 {backend-only|frontend-only|admin-only|full}" + release_lock + exit 1 + ;; + esac + + print_summary + log "🏁 部署流水线全部执行完毕 ($(date '+%H:%M:%S'))" +} + +main "$@" diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 6f9726aaf5..8f5a55bc54 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -1,5 +1,12 @@ server: port: 9900 + # 优雅关闭:等待 HTTP 请求和后台任务完成后再退出 + shutdown: graceful + +spring: + lifecycle: + # 每个阶段最多等待 30 秒,给 CompletableFuture 和线程池排空的机会 + timeout-per-shutdown-phase: 30s --- #################### 数据库相关配置 #################### spring: