fix: MySQL DDL+DML同批EXPLAIN预检bug + Redis集群MOVED重定向 + 清晰错误提示
MySQL修复: - 批次含DDL时跳过整个EXPLAIN预检(避免CREATE TABLE未执行时INSERT的EXPLAIN失败) - 纯DML批次仍正常做EXPLAIN预检 Redis集群修复: - 单节点客户端遇到MOVED时自动映射外部IP+端口重试 - 重试失败给出清晰提示(Docker端口未暴露) - 新增 isClusterInstance/parseMovedTarget 辅助函数 测试验证通过: - MySQL 5条DDL+DML ✅ (建表+INSERT+UPDATE+加索引) - MySQL 纯DML+EXPLAIN预检 ✅ - MySQL dba(logan)执行 ✅ - Redis单机 SET写入 ✅ - Redis集群 ⚠️ Docker内部端口未暴露(基础设施限制)
This commit is contained in:
1 parent
2984f11181
commit
8ceb249274
1 file changed
+65
-12
@@ -14,6 +14,7 @@ import (
|
|||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SqlExecItem records the execution result of a single SQL statement
|
// SqlExecItem records the execution result of a single SQL statement
|
||||||
@@ -571,6 +572,33 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
execStart := time.Now()
|
execStart := time.Now()
|
||||||
result, err := client.Do(ctx, args...).Result()
|
result, err := client.Do(ctx, args...).Result()
|
||||||
|
// Cluster MOVED redirect: retry on the correct node with external IP mapping
|
||||||
|
if err != nil && isClusterInstance(&instance) {
|
||||||
|
errStr := err.Error()
|
||||||
|
if moved := parseMovedTarget(errStr); moved != "" {
|
||||||
|
// Map internal IP to external host: replace host part, keep port
|
||||||
|
if idx := strings.LastIndex(moved, ":"); idx >= 0 {
|
||||||
|
port := moved[idx+1:]
|
||||||
|
externalAddr := instance.Host + ":" + port
|
||||||
|
password := instance.Password
|
||||||
|
if strings.HasPrefix(password, "enc:") {
|
||||||
|
if dec, e := decryptPassword(strings.TrimPrefix(password, "enc:")); e == nil {
|
||||||
|
password = dec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetClient := redis.NewClient(&redis.Options{
|
||||||
|
Addr: externalAddr,
|
||||||
|
Username: instance.Username,
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
defer targetClient.Close()
|
||||||
|
result, err = targetClient.Do(ctx, args...).Result()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("集群节点重定向失败(MOVED→%s): %w — 集群节点端口(%s)可能未暴露给外部,需检查Docker端口映射", moved, err, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
stmtMs := time.Since(execStart).Milliseconds()
|
stmtMs := time.Since(execStart).Milliseconds()
|
||||||
execRes := SqlExecResult{
|
execRes := SqlExecResult{
|
||||||
Status: "success",
|
Status: "success",
|
||||||
@@ -659,10 +687,26 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
execTimeout := getSettingInt("executor_timeout", 300)
|
execTimeout := getSettingInt("executor_timeout", 300)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
// Dry-run: EXPLAIN pre-check (skip DDL statements)
|
// Dry-run: EXPLAIN pre-check
|
||||||
if getSetting("executor_dry_run", "true") == "true" {
|
if getSetting("executor_dry_run", "true") == "true" {
|
||||||
ddlPrefixes := []string{"CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"}
|
ddlPrefixes := []string{"CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"}
|
||||||
statements := splitSQL(ticket.SQLContent)
|
statements := splitSQL(ticket.SQLContent)
|
||||||
|
// If batch contains ANY DDL, skip EXPLAIN precheck entirely —
|
||||||
|
// DDL changes the schema so subsequent DML can't be EXPLAINed beforehand
|
||||||
|
hasDDL := false
|
||||||
|
for _, stmt := range statements {
|
||||||
|
upper := strings.ToUpper(strings.TrimSpace(stmt))
|
||||||
|
for _, prefix := range ddlPrefixes {
|
||||||
|
if strings.HasPrefix(upper, prefix) {
|
||||||
|
hasDDL = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasDDL {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasDDL {
|
||||||
for _, stmt := range statements {
|
for _, stmt := range statements {
|
||||||
stmt = strings.TrimSpace(stmt)
|
stmt = strings.TrimSpace(stmt)
|
||||||
if stmt == "" {
|
if stmt == "" {
|
||||||
@@ -672,17 +716,6 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
if strings.HasPrefix(upperStmt, "EXPLAIN") {
|
if strings.HasPrefix(upperStmt, "EXPLAIN") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Skip DDL — EXPLAIN does not work on DDL statements
|
|
||||||
isDDL := false
|
|
||||||
for _, prefix := range ddlPrefixes {
|
|
||||||
if strings.HasPrefix(upperStmt, prefix) {
|
|
||||||
isDDL = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isDDL {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rows, err := db.QueryContext(ctx, "EXPLAIN "+stmt)
|
rows, err := db.QueryContext(ctx, "EXPLAIN "+stmt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
execErr = fmt.Sprintf("预检查失败(EXPLAIN): %v", err)
|
execErr = fmt.Sprintf("预检查失败(EXPLAIN): %v", err)
|
||||||
@@ -690,6 +723,7 @@ func ExecuteTicket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if execErr != "" {
|
if execErr != "" {
|
||||||
failStatus := "failed"
|
failStatus := "failed"
|
||||||
ticket.Status = &failStatus
|
ticket.Status = &failStatus
|
||||||
@@ -1026,3 +1060,22 @@ func getApplicantName(userID int) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isClusterInstance checks if a Redis instance is configured as cluster mode
|
||||||
|
func isClusterInstance(instance *model.Instance) bool {
|
||||||
|
return instance.ClusterMode != nil && *instance.ClusterMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMovedTarget extracts the target address from a MOVED response
|
||||||
|
// Input: "MOVED 8480 172.19.0.4:9002" → Output: "172.19.0.4:9002"
|
||||||
|
func parseMovedTarget(errMsg string) string {
|
||||||
|
// MOVED <slot> <host>:<port>
|
||||||
|
if !strings.HasPrefix(errMsg, "MOVED ") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(errMsg, " ", 3)
|
||||||
|
if len(parts) == 3 {
|
||||||
|
return parts[2]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in new issue
Block a user