268 lines
7.1 KiB
PHP
268 lines
7.1 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace app\utils;
|
|||
|
|
|
|||
|
|
use Predis\Client;
|
|||
|
|
use app\utils\LoggerHelper;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Redis 工具类
|
|||
|
|
*
|
|||
|
|
* 职责:
|
|||
|
|
* - 封装 Redis 连接和基础操作
|
|||
|
|
* - 提供分布式锁功能
|
|||
|
|
*/
|
|||
|
|
class RedisHelper
|
|||
|
|
{
|
|||
|
|
private static ?Client $client = null;
|
|||
|
|
private static array $config = [];
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取 Redis 客户端(单例模式)
|
|||
|
|
*
|
|||
|
|
* @return Client Redis 客户端
|
|||
|
|
*/
|
|||
|
|
public static function getClient(): Client
|
|||
|
|
{
|
|||
|
|
if (self::$client !== null) {
|
|||
|
|
return self::$client;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从 session 配置中读取 Redis 配置(临时方案,后续可创建独立的 cache.php)
|
|||
|
|
$sessionConfig = config('session.config.redis', []);
|
|||
|
|
|
|||
|
|
self::$config = [
|
|||
|
|
'host' => $sessionConfig['host'] ?? getenv('REDIS_HOST') ?: '127.0.0.1',
|
|||
|
|
'port' => (int)($sessionConfig['port'] ?? getenv('REDIS_PORT') ?: 6379),
|
|||
|
|
'password' => $sessionConfig['auth'] ?? getenv('REDIS_PASSWORD') ?: null,
|
|||
|
|
'database' => (int)($sessionConfig['database'] ?? getenv('REDIS_DATABASE') ?: 0),
|
|||
|
|
'timeout' => $sessionConfig['timeout'] ?? 2.0,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$parameters = [
|
|||
|
|
'host' => self::$config['host'],
|
|||
|
|
'port' => self::$config['port'],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
if (!empty(self::$config['password'])) {
|
|||
|
|
$parameters['password'] = self::$config['password'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (self::$config['database'] > 0) {
|
|||
|
|
$parameters['database'] = self::$config['database'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$options = [
|
|||
|
|
'timeout' => self::$config['timeout'],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
self::$client = new Client($parameters, $options);
|
|||
|
|
|
|||
|
|
// 测试连接
|
|||
|
|
try {
|
|||
|
|
self::$client->ping();
|
|||
|
|
LoggerHelper::logBusiness('redis_connected', [
|
|||
|
|
'host' => self::$config['host'],
|
|||
|
|
'port' => self::$config['port'],
|
|||
|
|
]);
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
LoggerHelper::logError($e, [
|
|||
|
|
'component' => 'RedisHelper',
|
|||
|
|
'action' => 'getClient',
|
|||
|
|
]);
|
|||
|
|
throw $e;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return self::$client;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取分布式锁
|
|||
|
|
*
|
|||
|
|
* @param string $key 锁的键
|
|||
|
|
* @param int $ttl 锁的过期时间(秒)
|
|||
|
|
* @param int $retryTimes 重试次数
|
|||
|
|
* @param int $retryDelay 重试延迟(毫秒)
|
|||
|
|
* @return bool 是否获取成功
|
|||
|
|
*/
|
|||
|
|
public static function acquireLock(string $key, int $ttl = 300, int $retryTimes = 3, int $retryDelay = 1000): bool
|
|||
|
|
{
|
|||
|
|
$client = self::getClient();
|
|||
|
|
$lockKey = "lock:{$key}";
|
|||
|
|
$lockValue = uniqid(gethostname() . '_', true); // 唯一值,用于安全释放锁
|
|||
|
|
|
|||
|
|
for ($i = 0; $i <= $retryTimes; $i++) {
|
|||
|
|
// 尝试获取锁(SET key value NX EX ttl)
|
|||
|
|
$result = $client->set($lockKey, $lockValue, 'EX', $ttl, 'NX');
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
LoggerHelper::logBusiness('redis_lock_acquired', [
|
|||
|
|
'key' => $key,
|
|||
|
|
'ttl' => $ttl,
|
|||
|
|
]);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果还有重试机会,等待后重试
|
|||
|
|
if ($i < $retryTimes) {
|
|||
|
|
usleep($retryDelay * 1000); // 转换为微秒
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
LoggerHelper::logBusiness('redis_lock_failed', [
|
|||
|
|
'key' => $key,
|
|||
|
|
'retry_times' => $retryTimes,
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 释放分布式锁
|
|||
|
|
*
|
|||
|
|
* @param string $key 锁的键
|
|||
|
|
* @return bool 是否释放成功
|
|||
|
|
*/
|
|||
|
|
public static function releaseLock(string $key): bool
|
|||
|
|
{
|
|||
|
|
$client = self::getClient();
|
|||
|
|
$lockKey = "lock:{$key}";
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$result = $client->del([$lockKey]);
|
|||
|
|
|
|||
|
|
if ($result > 0) {
|
|||
|
|
LoggerHelper::logBusiness('redis_lock_released', [
|
|||
|
|
'key' => $key,
|
|||
|
|
]);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
LoggerHelper::logError($e, [
|
|||
|
|
'component' => 'RedisHelper',
|
|||
|
|
'action' => 'releaseLock',
|
|||
|
|
'key' => $key,
|
|||
|
|
]);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置键值对
|
|||
|
|
*
|
|||
|
|
* @param string $key 键
|
|||
|
|
* @param mixed $value 值
|
|||
|
|
* @param int|null $ttl 过期时间(秒),null 表示不过期
|
|||
|
|
* @return bool 是否设置成功
|
|||
|
|
*/
|
|||
|
|
public static function set(string $key, $value, ?int $ttl = null): bool
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$client = self::getClient();
|
|||
|
|
$serialized = is_string($value) ? $value : json_encode($value, JSON_UNESCAPED_UNICODE);
|
|||
|
|
|
|||
|
|
if ($ttl !== null) {
|
|||
|
|
$client->setex($key, $ttl, $serialized);
|
|||
|
|
} else {
|
|||
|
|
$client->set($key, $serialized);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
LoggerHelper::logError($e, [
|
|||
|
|
'component' => 'RedisHelper',
|
|||
|
|
'action' => 'set',
|
|||
|
|
'key' => $key,
|
|||
|
|
]);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取键值
|
|||
|
|
*
|
|||
|
|
* @param string $key 键
|
|||
|
|
* @return mixed 值,不存在返回 null
|
|||
|
|
*/
|
|||
|
|
public static function get(string $key)
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$client = self::getClient();
|
|||
|
|
$value = $client->get($key);
|
|||
|
|
|
|||
|
|
if ($value === null) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 尝试 JSON 解码
|
|||
|
|
$decoded = json_decode($value, true);
|
|||
|
|
return $decoded !== null ? $decoded : $value;
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
LoggerHelper::logError($e, [
|
|||
|
|
'component' => 'RedisHelper',
|
|||
|
|
'action' => 'get',
|
|||
|
|
'key' => $key,
|
|||
|
|
]);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除键
|
|||
|
|
*
|
|||
|
|
* @param string $key 键
|
|||
|
|
* @return bool 是否删除成功
|
|||
|
|
*/
|
|||
|
|
public static function delete(string $key): bool
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$client = self::getClient();
|
|||
|
|
$result = $client->del([$key]);
|
|||
|
|
return $result > 0;
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
LoggerHelper::logError($e, [
|
|||
|
|
'component' => 'RedisHelper',
|
|||
|
|
'action' => 'delete',
|
|||
|
|
'key' => $key,
|
|||
|
|
]);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查键是否存在
|
|||
|
|
*
|
|||
|
|
* @param string $key 键
|
|||
|
|
* @return bool 是否存在
|
|||
|
|
*/
|
|||
|
|
public static function exists(string $key): bool
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$client = self::getClient();
|
|||
|
|
$result = $client->exists($key);
|
|||
|
|
return $result > 0;
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
LoggerHelper::logError($e, [
|
|||
|
|
'component' => 'RedisHelper',
|
|||
|
|
'action' => 'exists',
|
|||
|
|
'key' => $key,
|
|||
|
|
]);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除键(别名,兼容del方法)
|
|||
|
|
*
|
|||
|
|
* @param string $key 键
|
|||
|
|
* @return bool 是否删除成功
|
|||
|
|
*/
|
|||
|
|
public static function del(string $key): bool
|
|||
|
|
{
|
|||
|
|
return self::delete($key);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|