1.增加互斥锁:保证缓存和数据库数据一致性
2.增加消费队列,保证mysql数据的正常保存
This commit is contained in:
132
app/common/service/GameHotDataLock.php
Normal file
132
app/common/service/GameHotDataLock.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user