1.增加互斥锁:保证缓存和数据库数据一致性

2.增加消费队列,保证mysql数据的正常保存
This commit is contained in:
2026-04-20 14:13:48 +08:00
parent 614fb00ec4
commit 1eed3cf0f7
23 changed files with 836 additions and 255 deletions

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use support\Redis;
use Throwable;
/**
* 热点实体互斥锁Redis SET NX EX按资源串行化写入避免多管理员/多进程同时改同一缓存键对应的数据。
*/
final class GameHotDataLock
{
public const TYPE_USER = 'user';
public const TYPE_GAME_CONFIG = 'gc';
public const TYPE_GAME_RECORD = 'gr';
private const KEY_PREFIX = 'dfw:v1:lock:mut:';
/**
* @return array{acquired: bool, token: ?string, redis_lock: bool}
*/
public static function tryAcquire(string $type, string $resourceKey): array
{
if ($resourceKey === '') {
return ['acquired' => 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;
}
}