refactor(mysql): 适配PXC严格模式,替换全局锁方案

重构MySQL迁移逻辑,新增acquireMigrationLock适配Percona XtraDB Cluster的pxc_strict_mode限制,将原有的GET_LOCK迁移锁替换为更兼容的方案,同时为各迁移流程添加快速检查路径减少锁竞争
This commit is contained in:
Austin
2026-09-20 14:52:01 +08:00
parent 753a04ed12
commit b2e8b64c0b
4 changed files with 105 additions and 29 deletions
+36 -17
View File
@@ -5,8 +5,6 @@ package mysql
import (
"context"
"database/sql"
"errors"
"github.com/jmoiron/sqlx"
)
@@ -26,22 +24,40 @@ func ensureUserPrivacySchema(db *sqlx.DB) error {
}
defer conn.Close()
var locked sql.NullInt64
if err = conn.QueryRowContext(ctx, "SELECT GET_LOCK("+userPrivacyMigrationLock+", 60)").Scan(&locked); err != nil {
// Fast path: check for missing columns without taking the migration lock.
// Databases created by createIMUserProductTables already have every column,
// so this is the common case and avoids GET_LOCK entirely (prohibited on
// Percona XtraDB Cluster with pxc_strict_mode=ENFORCING).
added, err := ensureUserPrivacyColumns(ctx, conn)
if err != nil || added {
return err
}
if !locked.Valid || locked.Int64 != 1 {
return errors.New("mysql: timed out waiting for user privacy migration lock")
}
defer conn.ExecContext(ctx, "SELECT RELEASE_LOCK("+userPrivacyMigrationLock+")")
_, unlock, err := acquireMigrationLock(ctx, conn, userPrivacyMigrationLock)
if err != nil {
return err
}
if unlock != nil {
defer unlock()
}
// Double-check under the lock: another instance may have migrated while we
// were waiting. Without a lock (PXC fallback) this simply runs idempotently.
_, err = ensureUserPrivacyColumns(ctx, conn)
return err
}
// ensureUserPrivacyColumns adds any columns of im_user_privacy_settings that
// are missing and reports whether nothing was missing. A missing table is
// skipped: it is created by the fresh-install path and there is nothing to
// upgrade yet. Column definitions mirror createIMUserProductTables; the SELECT
// in userPrivacyByUID references every one of them.
func ensureUserPrivacyColumns(ctx context.Context, conn *sqlx.Conn) (bool, error) {
tableExists, err := identityTableExists(ctx, conn, "im_user_privacy_settings")
if err != nil || !tableExists {
return err
return true, err
}
// Column definitions mirror createIMUserProductTables; the SELECT in
// userPrivacyByUID references every one of them.
columns := []struct{ name, ddl string }{
{"profile_visibility", "VARCHAR(32) NOT NULL DEFAULT 'friends'"},
{"last_seen_visibility", "VARCHAR(32) NOT NULL DEFAULT 'friends'"},
@@ -49,16 +65,19 @@ func ensureUserPrivacySchema(db *sqlx.DB) error {
{"friend_request_verification", "TINYINT NOT NULL DEFAULT 1"},
{"group_invite_verification", "TINYINT NOT NULL DEFAULT 0"},
}
missing := false
for _, col := range columns {
exists, err := identityColumnExists(ctx, conn, "im_user_privacy_settings", col.name)
if err != nil {
return err
return false, err
}
if !exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_privacy_settings ADD COLUMN "+col.name+" "+col.ddl); err != nil {
return err
}
if exists {
continue
}
missing = true
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_privacy_settings ADD COLUMN "+col.name+" "+col.ddl); err != nil {
return false, err
}
}
return nil
return !missing, nil
}