null:表示已确认不存在(负缓存) * - key => array:表示已加载行数据 * * @var array|null> */ private static array $gcLocal = []; public static function enabled(): bool { return config('game_hot_cache.enabled', true) === true; } /** * @return array|null */ public static function gameConfigRow(string $configKey): ?array { if ($configKey === '') { return null; } // game_config 为全局配置,多进程/多 worker 间必须强一致; // 因此不使用进程内本地缓存(gcLocal),避免某个进程读到旧值导致前端回弹/行为冲突。 if (self::enabled()) { $cached = self::redisGet(self::KEY_GC . $configKey); if ($cached !== null && $cached !== '') { $decoded = json_decode($cached, true); if (is_array($decoded)) { return $decoded; } } } $row = Db::name('game_config')->where('config_key', $configKey)->find(); if (!$row) { return null; } if (self::enabled()) { $ttl = self::intConfig('ttl_game_config', 86400); self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } return $row; } public static function gameConfigForget(string $configKey): void { if ($configKey === '') { return; } self::redisDel(self::KEY_GC . $configKey); unset(self::$gcLocal[$configKey]); } /** * 按库中当前行覆盖 game_config 缓存(无行则删缓存) */ public static function gameConfigReplaceFromDb(string $configKey): void { if ($configKey === '') { return; } // 无论是否启用 Redis 热点缓存,都要刷新进程内缓存,避免同一 worker 读到旧值 $row = Db::name('game_config')->where('config_key', $configKey)->find(); if (!$row) { self::$gcLocal[$configKey] = null; if (self::enabled()) { self::redisDel(self::KEY_GC . $configKey); } return; } self::$gcLocal[$configKey] = $row; if (self::enabled()) { $ttl = self::intConfig('ttl_game_config', 86400); self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } } /** * 对局写入后:刷新指定 id 的行缓存,并删除「活跃局 / 最新局」聚合键以免脏读 * * @param int|null $id 可为 null(仅清聚合键) */ public static function gameRecordSyncCachesAfterDbWrite(?int $id): void { if (!self::enabled()) { return; } if ($id !== null && $id > 0) { $row = Db::name('game_record')->where('id', $id)->find(); if ($row) { $ttl = self::intConfig('ttl_game_record', 60); self::redisSetEx(self::KEY_GR_ID . $id, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } else { self::redisDel(self::KEY_GR_ID . $id); } } self::redisDel(self::KEY_GR_ACTIVE, self::KEY_GR_LATEST); } /** * @return array|null */ public static function gameRecordById(int $id): ?array { if ($id <= 0) { return null; } $key = self::KEY_GR_ID . $id; if (self::enabled()) { $cached = self::redisGet($key); if ($cached !== null && $cached !== '') { $decoded = json_decode($cached, true); if (is_array($decoded)) { return $decoded; } } } $row = Db::name('game_record')->where('id', $id)->find(); if (!$row) { return null; } if (self::enabled()) { $ttl = self::intConfig('ttl_game_record', 60); self::redisSetEx($key, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } return $row; } /** * 当前未完全结束的对局(status ∈ 0,1,2,3 中 id 最大的一条) * * @return array|null */ public static function gameRecordActive(): ?array { if (self::enabled()) { $cached = self::redisGet(self::KEY_GR_ACTIVE); if ($cached !== null && $cached !== '') { $decoded = json_decode($cached, true); if (is_array($decoded)) { return $decoded; } } } $row = Db::name('game_record')->whereIn('status', [0, 1, 2, 3])->order('id', 'desc')->find(); if (!$row) { return null; } if (self::enabled()) { $ttl = self::intConfig('ttl_game_record', 60); self::redisSetEx(self::KEY_GR_ACTIVE, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } return $row; } /** * 最新一条对局(含已结束),与 GameRecord::order('id','desc')->find() 一致 * * @return array|null */ public static function gameRecordLatest(): ?array { if (self::enabled()) { $cached = self::redisGet(self::KEY_GR_LATEST); if ($cached !== null && $cached !== '') { $decoded = json_decode($cached, true); if (is_array($decoded)) { return $decoded; } } } $row = Db::name('game_record')->order('id', 'desc')->find(); if (!$row) { return null; } if (self::enabled()) { $ttl = self::intConfig('ttl_game_record', 60); self::redisSetEx(self::KEY_GR_LATEST, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } return $row; } /** * 任意 game_record 变更后调用:删除活跃局、最新局及指定 id 缓存 */ public static function gameRecordForget(?int $id = null): void { $keys = [self::KEY_GR_ACTIVE, self::KEY_GR_LATEST]; if ($id !== null && $id > 0) { $keys[] = self::KEY_GR_ID . $id; } self::redisDel(...$keys); } /** * @return array|null */ public static function userRow(int $userId): ?array { if ($userId <= 0) { return null; } $key = self::KEY_USER . $userId; if (self::enabled()) { $cached = self::redisGet($key); if ($cached !== null && $cached !== '') { $decoded = json_decode($cached, true); if (is_array($decoded)) { return $decoded; } } } $row = Db::name('user')->where('id', $userId)->find(); if (!$row) { return null; } if (self::enabled()) { $ttl = self::intConfig('ttl_user', 90); self::redisSetEx($key, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } return $row; } public static function userRemember(User $user): void { if (!self::enabled()) { return; } $id = $user->getAttr('id'); $uid = filter_var($id, FILTER_VALIDATE_INT); if ($uid === false || $uid <= 0) { return; } $ttl = self::intConfig('ttl_user', 90); self::redisSetEx(self::KEY_USER . $uid, $ttl, json_encode($user->toArray(), JSON_UNESCAPED_UNICODE)); } public static function userForget(int $userId): void { if ($userId <= 0) { return; } self::redisDel(self::KEY_USER . $userId); } /** * 从数据库读取最新 user 行并覆盖写入 Redis(与 DB 同事务后调用,保持缓存与库一致) */ public static function userReplaceCacheFromDb(int $userId): void { if ($userId <= 0 || !self::enabled()) { return; } $row = Db::name('user')->where('id', $userId)->find(); if (!$row) { self::userForget($userId); return; } $ttl = self::intConfig('ttl_user', 90); self::redisSetEx(self::KEY_USER . $userId, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE)); } /** * 尝试获取「后台修改该用户」互斥锁(Redis SET NX EX)。 * 与热点缓存开关无关:只要 Redis 可用即加锁;连接失败时降级为仅依赖数据库乐观锁(WHERE coin=)。 * * @return array{acquired: bool, token: ?string, redis_lock: bool} */ public static function userAdminMutationLockTry(int $userId): array { if ($userId <= 0) { return ['acquired' => false, 'token' => null, 'redis_lock' => false]; } return GameHotDataLock::tryAcquire(GameHotDataLock::TYPE_USER, (string) $userId); } /** * 释放 userAdminMutationLockTry 取得的锁(仅当 redis_lock 且 token 非空) */ public static function userAdminMutationLockRelease(int $userId, ?string $token, bool $redisLock): void { if ($userId <= 0) { return; } GameHotDataLock::release(GameHotDataLock::TYPE_USER, (string) $userId, $token, $redisLock); } /** * 用缓存行构造已存在库的 User(供 Auth 等高频读) */ public static function userModelFromCacheOrDb(int $userId): ?User { $row = self::userRow($userId); if ($row === null) { return null; } return (new User())->newInstance($row); } private static function intConfig(string $name, int $default): int { $v = config('game_hot_cache.' . $name, $default); if (is_int($v)) { return $v > 0 ? $v : $default; } if (is_numeric($v)) { $n = filter_var($v, FILTER_VALIDATE_INT); if ($n === false) { return $default; } return $n > 0 ? $n : $default; } return $default; } private static function redisGet(string $key): ?string { try { $v = Redis::get($key); if ($v === false || $v === null) { return null; } if (!is_string($v)) { return null; } return $v; } catch (Throwable) { return null; } } private static function redisSetEx(string $key, int $ttl, string $value): void { try { Redis::setEx($key, $ttl, $value); } catch (Throwable) { } } private static function redisDel(string ...$keys): void { if ($keys === []) { return; } try { Redis::del(...$keys); } catch (Throwable) { } } }