From 585ff094437749e36b036784246acf0bb1dac6dd Mon Sep 17 00:00:00 2001 From: ghostg00 <28946120+ghostg00@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:46:46 +0800 Subject: [PATCH] compat(redis): replicate effects for Lua scripts using TIME on Redis 3.2-4.x Call redis.replicate_commands() at the top of every Lua script that reads redis.call('TIME'), so the resulting writes (ZADD, ZREMRANGEBYSCORE, SET, DEL, EXPIRE) replicate correctly on Redis 3.2-4.x. This is a no-op on Redis 5.0+, where effects replication is the default. The function remains a public API on Redis 7.0+ (deprecated but supported) and is still the documented way to opt into effects replication on older versions, so calling it unconditionally is safe and forward-compatible. Affected scripts (8 in total, all already using redis.call('TIME') by design to avoid client-side clock skew across multiple app instances): - concurrency_cache.go: acquireScript, getCountScript, cleanupExpiredSlotsScript - session_limit_cache.go: registerSessionScript, refreshSessionScript, getActiveSessionCountScript, isSessionActiveScript - user_msg_queue_cache.go: releaseLockScript Scripts that do not invoke non-deterministic commands are intentionally left untouched. --- backend/internal/repository/concurrency_cache.go | 9 +++++++++ backend/internal/repository/session_limit_cache.go | 12 ++++++++++++ backend/internal/repository/user_msg_queue_cache.go | 3 +++ 3 files changed, 24 insertions(+) diff --git a/backend/internal/repository/concurrency_cache.go b/backend/internal/repository/concurrency_cache.go index 8732b2cea1..5e6f10062f 100644 --- a/backend/internal/repository/concurrency_cache.go +++ b/backend/internal/repository/concurrency_cache.go @@ -44,6 +44,9 @@ var ( // ARGV[2] = TTL(秒) // ARGV[3] = requestID acquireScript = redis.NewScript(` + -- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') + -- replicates correctly. No-op on Redis 5.0+ (effects replication is default). + redis.replicate_commands() local key = KEYS[1] local maxConcurrency = tonumber(ARGV[1]) local ttl = tonumber(ARGV[2]) @@ -81,6 +84,9 @@ var ( // KEYS[1] = 有序集合键 // ARGV[1] = TTL(秒) getCountScript = redis.NewScript(` + -- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') + -- replicates correctly. No-op on Redis 5.0+ (effects replication is default). + redis.replicate_commands() local key = KEYS[1] local ttl = tonumber(ARGV[1]) @@ -151,6 +157,9 @@ var ( // KEYS[1] = 有序集合键 // ARGV[1] = TTL(秒) cleanupExpiredSlotsScript = redis.NewScript(` + -- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') + -- replicates correctly. No-op on Redis 5.0+ (effects replication is default). + redis.replicate_commands() local key = KEYS[1] local ttl = tonumber(ARGV[1]) local timeResult = redis.call('TIME') diff --git a/backend/internal/repository/session_limit_cache.go b/backend/internal/repository/session_limit_cache.go index 3d57b15201..1711256937 100644 --- a/backend/internal/repository/session_limit_cache.go +++ b/backend/internal/repository/session_limit_cache.go @@ -42,6 +42,9 @@ var ( // ARGV[3] = sessionUUID // 返回: 1 = 允许, 0 = 拒绝 registerSessionScript = redis.NewScript(` + -- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') + -- replicates correctly. No-op on Redis 5.0+ (effects replication is default). + redis.replicate_commands() local key = KEYS[1] local maxSessions = tonumber(ARGV[1]) local idleTimeout = tonumber(ARGV[2]) @@ -82,6 +85,9 @@ var ( // ARGV[1] = idleTimeout(秒) // ARGV[2] = sessionUUID refreshSessionScript = redis.NewScript(` + -- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') + -- replicates correctly. No-op on Redis 5.0+ (effects replication is default). + redis.replicate_commands() local key = KEYS[1] local idleTimeout = tonumber(ARGV[1]) local sessionUUID = ARGV[2] @@ -102,6 +108,9 @@ var ( // KEYS[1] = session_limit:account:{accountID} // ARGV[1] = idleTimeout(秒) getActiveSessionCountScript = redis.NewScript(` + -- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') + -- replicates correctly. No-op on Redis 5.0+ (effects replication is default). + redis.replicate_commands() local key = KEYS[1] local idleTimeout = tonumber(ARGV[1]) @@ -120,6 +129,9 @@ var ( // ARGV[1] = idleTimeout(秒) // ARGV[2] = sessionUUID isSessionActiveScript = redis.NewScript(` + -- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') + -- replicates correctly. No-op on Redis 5.0+ (effects replication is default). + redis.replicate_commands() local key = KEYS[1] local idleTimeout = tonumber(ARGV[1]) local sessionUUID = ARGV[2] diff --git a/backend/internal/repository/user_msg_queue_cache.go b/backend/internal/repository/user_msg_queue_cache.go index bb3ee698bf..2b5b96bdc3 100644 --- a/backend/internal/repository/user_msg_queue_cache.go +++ b/backend/internal/repository/user_msg_queue_cache.go @@ -34,6 +34,9 @@ return 1 // Lua 脚本:原子释放锁 + 记录完成时间(使用 Redis TIME 避免时钟偏差) var releaseLockScript = redis.NewScript(` +-- Redis 3.2-4.x compat: opt into effects replication so redis.call('TIME') +-- replicates correctly. No-op on Redis 5.0+ (effects replication is default). +redis.replicate_commands() local cur = redis.call('GET', KEYS[1]) if cur == ARGV[1] then redis.call('DEL', KEYS[1])