false, 'token' => null, 'redis_lock' => false]; } $lockKey = self::lockKey($type, $resourceKey); $ttl = self::lockTtl(); $token = bin2hex(random_bytes(16)); try { $client = Redis::connection()->client(); if (!is_object($client) || !method_exists($client, 'set')) { return ['acquired' => true, 'token' => null, 'redis_lock' => false]; } $ok = $client->set($lockKey, $token, ['nx', 'ex' => $ttl]); if ($ok === true) { return ['acquired' => true, 'token' => $token, 'redis_lock' => true]; } } catch (Throwable) { return ['acquired' => true, 'token' => null, 'redis_lock' => false]; } return ['acquired' => false, 'token' => null, 'redis_lock' => true]; } /** * 带短等待的重试(毫秒),用于对局写入与 ensureAiLocked 等可能交叉的路径。 * * @return array{acquired: bool, token: ?string, redis_lock: bool} */ public static function tryAcquireWithWait(string $type, string $resourceKey, int $maxWaitMs = 800): array { $deadline = (int) (microtime(true) * 1000) + max(0, $maxWaitMs); while (true) { $r = self::tryAcquire($type, $resourceKey); if ($r['acquired']) { return $r; } $now = (int) (microtime(true) * 1000); if ($now >= $deadline) { return $r; } usleep(25_000); } } public static function release(string $type, string $resourceKey, ?string $token, bool $redisLock): void { if ($resourceKey === '' || !$redisLock || $token === null || $token === '') { return; } $key = self::lockKey($type, $resourceKey); $script = <<<'LUA' if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) end return 0 LUA; try { $client = Redis::connection()->client(); if (is_object($client) && method_exists($client, 'eval')) { $client->eval($script, [$key, $token], 1); } } catch (Throwable) { } } /** * @template T * @param callable(): T $fn * @return T|null */ public static function runExclusive(string $type, string $resourceKey, callable $fn): mixed { $lock = self::tryAcquire($type, $resourceKey); if (!$lock['acquired']) { return null; } try { return $fn(); } finally { self::release($type, $resourceKey, $lock['token'], $lock['redis_lock']); } } public static function safeResourceKeyForConfig(string $configKey): string { return rtrim(strtr(base64_encode($configKey), '+/', '-_'), '='); } public static function lockKeyFromConfigKey(string $configKey): string { return self::lockKey(self::TYPE_GAME_CONFIG, self::safeResourceKeyForConfig($configKey)); } private static function lockKey(string $type, string $resourceKey): string { return self::KEY_PREFIX . $type . ':' . $resourceKey; } private static function lockTtl(): int { $v = config('game_hot_cache.admin_user_mutation_lock_ttl', 30); $n = filter_var($v, FILTER_VALIDATE_INT); return ($n === false || $n < 5) ? 30 : $n; } }