在ThinkPHP8中实现缓存降级

在ThinkPHP8中实现缓存降级,该方案还在探讨中:

1. 基础配置和依赖

安装熔断器组件(推荐)

bash 复制代码
composer require hyperf/circuit-breaker
# 或者使用更轻量的
composer require workerman/workerman

缓存配置文件 config/cache.php

php 复制代码
return [
    'default' => env('cache.driver', 'redis'),
    
    'stores'  => [
        'redis' => [
            'type'       => 'redis',
            'host'       => env('redis.host', '127.0.0.1'),
            'port'       => env('redis.port', 6379),
            'password'   => env('redis.password', ''),
            'select'     => env('redis.database', 0),
            'timeout'    => 3, // 3秒超时
        ],
        
        'file' => [
            'type'       => 'file',
            'path'       => app()->getRuntimePath() . 'cache',
            'expire'     => 3600,
        ],
    ],
    
    // 降级配置
    'degrade' => [
        'enabled'     => true,
        'switch_key'  => 'cache_degrade_switch', // 动态开关的缓存key
        'timeout'     => 2, // 超时时间(秒)
        'fail_count'  => 3, // 连续失败次数触发降级
    ],
];

2. 创建降级服务类

降级开关服务 app/service/DegradeService.php

php 复制代码
<?php
declare(strict_types=1);

namespace app\service;

use think\facade\Cache;
use think\facade\Log;

class DegradeService
{
    // 降级状态常量
    const STATUS_NORMAL = 0;    // 正常
    const STATUS_DEGRADE = 1;   // 降级中
    const STATUS_RECOVER = 2;   // 恢复中
    
    /**
     * 检查是否应该降级
     */
    public static function shouldDegrade(string $service): bool
    {
        // 1. 检查手动开关
        if (self::isManualDegrade($service)) {
            return true;
        }
        
        // 2. 检查自动降级状态
        if (self::isAutoDegrade($service)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * 手动降级开关
     */
    public static function setManualDegrade(string $service, bool $status): bool
    {
        $key = "degrade:manual:{$service}";
        return Cache::store('file')->set($key, $status ? 1 : 0, 3600);
    }
    
    /**
     * 检查手动降级状态
     */
    private static function isManualDegrade(string $service): bool
    {
        $key = "degrade:manual:{$service}";
        return (bool) Cache::store('file')->get($key, 0);
    }
    
    /**
     * 记录失败次数并判断是否触发自动降级
     */
    public static function recordFailure(string $service): bool
    {
        $key = "degrade:fail_count:{$service}";
        $failCount = Cache::store('file')->increment($key);
        Cache::store('file')->expire($key, 60); // 1分钟窗口
        
        $threshold = config('cache.degrade.fail_count', 3);
        
        if ($failCount >= $threshold) {
            // 触发自动降级
            $degradeKey = "degrade:auto:{$service}";
            Cache::store('file')->set($degradeKey, time(), 300); // 降级5分钟
            Cache::store('file')->delete($key); // 清空计数
            
            Log::warning("服务自动降级触发: {$service}, 失败次数: {$failCount}");
            return true;
        }
        
        return false;
    }
    
    /**
     * 检查自动降级状态
     */
    private static function isAutoDegrade(string $service): bool
    {
        $key = "degrade:auto:{$service}";
        $degradeTime = Cache::store('file')->get($key);
        
        if (!$degradeTime) {
            return false;
        }
        
        // 检查是否还在降级时间窗口内
        if (time() - $degradeTime < 300) { // 5分钟降级窗口
            return true;
        }
        
        // 降级窗口结束,清除状态
        Cache::store('file')->delete($key);
        return false;
    }
    
    /**
     * 记录成功调用(用于恢复)
     */
    public static function recordSuccess(string $service): void
    {
        $key = "degrade:success:{$service}";
        $successCount = Cache::store('file')->increment($key);
        Cache::store('file')->expire($key, 60);
        
        // 连续成功次数达到阈值,可以尝试恢复
        if ($successCount >= 5) {
            $degradeKey = "degrade:auto:{$service}";
            Cache::store('file')->delete($degradeKey);
            Cache::store('file')->delete($key);
            
            Log::info("服务自动恢复: {$service}");
        }
    }
}

3. 缓存降级装饰器

降级缓存装饰器 app/common/CacheWithDegrade.php

php 复制代码
<?php
declare(strict_types=1);

namespace app\common;

use think\facade\Cache;
use think\facade\Log;
use app\service\DegradeService;

class CacheWithDegrade
{
    private $primaryStore = 'redis';   // 主缓存
    private $fallbackStore = 'file';   // 降级缓存
    private $serviceName = 'cache';    // 服务名称
    
    public function __construct(string $serviceName = 'cache')
    {
        $this->serviceName = $serviceName;
    }
    
    /**
     * 带降级的获取缓存
     */
    public function get(string $key, $default = null)
    {
        // 检查降级状态
        if (DegradeService::shouldDegrade($this->serviceName)) {
            Log::info("缓存降级中,使用降级存储: {$key}");
            return $this->getFromFallback($key, $default);
        }
        
        try {
            // 设置超时
            $value = $this->withTimeout(function() use ($key) {
                return Cache::store($this->primaryStore)->get($key);
            });
            
            DegradeService::recordSuccess($this->serviceName);
            return $value ?? $default;
            
        } catch (\Throwable $e) {
            Log::error("主缓存获取失败: {$key}, 错误: " . $e->getMessage());
            DegradeService::recordFailure($this->serviceName);
            
            // 降级到备用缓存
            return $this->getFromFallback($key, $default);
        }
    }
    
    /**
     * 带降级的设置缓存
     */
    public function set(string $key, $value, $ttl = null): bool
    {
        // 即使降级中,也尝试写入主缓存(用于恢复)
        try {
            $this->withTimeout(function() use ($key, $value, $ttl) {
                Cache::store($this->primaryStore)->set($key, $value, $ttl);
            });
            DegradeService::recordSuccess($this->serviceName);
        } catch (\Throwable $e) {
            Log::error("主缓存设置失败: {$key}, 错误: " . $e->getMessage());
            DegradeService::recordFailure($this->serviceName);
        }
        
        // 同时写入降级缓存(保证数据不丢)
        try {
            Cache::store($this->fallbackStore)->set($key, $value, $ttl);
            return true;
        } catch (\Throwable $e) {
            Log::error("降级缓存设置失败: {$key}");
            return false;
        }
    }
    
    /**
     * 从降级存储获取
     */
    private function getFromFallback(string $key, $default = null)
    {
        try {
            return Cache::store($this->fallbackStore)->get($key) ?? $default;
        } catch (\Throwable $e) {
            Log::error("降级缓存也失败,返回默认值: {$key}");
            return $default;
        }
    }
    
    /**
     * 带超时的操作
     */
    private function withTimeout(callable $callback, ?int $timeout = null)
    {
        $timeout = $timeout ?? config('cache.degrade.timeout', 2);
        
        if (function_exists('pcntl_async_signals')) {
            // Linux环境使用pcntl超时
            pcntl_async_signals(true);
            pcntl_signal(SIGALRM, function() {
                throw new \RuntimeException('Cache operation timeout');
            });
            
            pcntl_alarm($timeout);
            try {
                $result = $callback();
                pcntl_alarm(0);
                return $result;
            } finally {
                pcntl_alarm(0);
            }
        } else {
            // Windows环境简单处理
            return $callback();
        }
    }
    
    /**
     * 删除缓存
     */
    public function delete(string $key): bool
    {
        $success = true;
        
        try {
            Cache::store($this->primaryStore)->delete($key);
        } catch (\Throwable $e) {
            $success = false;
        }
        
        try {
            Cache::store($this->fallbackStore)->delete($key);
        } catch (\Throwable $e) {
            $success = false;
        }
        
        return $success;
    }
}

4. 业务层使用示例

用户服务示例 app/service/UserService.php

php 复制代码
<?php
declare(strict_types=1);

namespace app\service;

use app\common\CacheWithDegrade;
use think\facade\Db;

class UserService
{
    private $cache;
    
    public function __construct()
    {
        $this->cache = new CacheWithDegrade('user_service');
    }
    
    /**
     * 获取用户信息(带缓存降级)- 修正版本
     */
    public function getUserInfo(int $userId): array
    {
        $cacheKey = "user:info:{$userId}";
        
        // 1. 先检查是否处于降级状态
        if (DegradeService::shouldDegrade('user_service')) {
            Log::info("用户服务降级中,直接返回默认数据: {$userId}");
            return $this->getDefaultUserInfo($userId);
        }
        
        // 2. 尝试从缓存获取
        $userInfo = $this->cache->get($cacheKey);
        
        if ($userInfo && !empty($userInfo['id'])) {
            return $userInfo;
        }
        
        // 3. 缓存不存在,查询数据库
        try {
            $userInfo = Db::name('user')
                ->where('id', $userId)
                ->findOrEmpty();
                
            if ($userInfo) {
                // 异步写入缓存
                $this->setCacheAsync($cacheKey, $userInfo, 3600);
                return $userInfo;
            } else {
                // 用户确实不存在,返回空或特定错误
                return $this->getUserNotFoundInfo($userId);
            }
            
        } catch (\Throwable $e) {
            // 数据库查询异常,触发降级
            Log::error("数据库查询失败,触发降级: {$userId}, 错误: " . $e->getMessage());
            DegradeService::recordFailure('user_service');
            
            return $this->getDefaultUserInfo($userId);
        }
    }
    
    /**
     * 更完善的降级检查方法
     */
    public function getUserInfoWithMultiLevel(int $userId): array
    {
        $cacheKey = "user:info:{$userId}";
        
        // 第一级:检查全局降级开关
        if (DegradeService::shouldDegrade('user_service')) {
            // 第二级:尝试本地内存缓存(如果支持)
            $localData = $this->getFromLocalCache($userId);
            if ($localData) {
                return $localData;
            }
            
            // 第三级:返回完全降级数据
            return $this->getDefaultUserInfo($userId);
        }
        
        // 正常流程...
        return $this->getNormalUserInfo($userId, $cacheKey);
    }
    
    /**
     * 正常流程获取用户信息
     */
    private function getNormalUserInfo(int $userId, string $cacheKey): array
    {
        // 带超时的缓存获取
        $userInfo = $this->getCacheWithTimeout($cacheKey, 2);
        
        if ($userInfo) {
            return $userInfo;
        }
        
        // 数据库查询(带超时控制)
        return $this->getFromDatabaseWithTimeout($userId, $cacheKey);
    }
    
    /**
     * 带超时的缓存获取
     */
    private function getCacheWithTimeout(string $key, int $timeout): ?array
    {
        try {
            // 使用协程或信号实现超时控制
            if (function_exists('pcntl_alarm')) {
                pcntl_alarm($timeout);
                $result = $this->cache->get($key);
                pcntl_alarm(0);
                return $result;
            }
            
            // 简化版本
            return $this->cache->get($key);
            
        } catch (\Throwable $e) {
            Log::warning("缓存获取超时: {$key}");
            return null;
        }
    }
    
    /**
     * 带超时的数据库查询
     */
    private function getFromDatabaseWithTimeout(int $userId, string $cacheKey): array
    {
        try {
            // 实际生产环境可以使用数据库超时配置
            $userInfo = Db::name('user')
                ->where('id', $userId)
                ->findOrEmpty();
                
            if ($userInfo) {
                $this->setCacheAsync($cacheKey, $userInfo, 3600);
                return $userInfo;
            }
            
            return $this->getUserNotFoundInfo($userId);
            
        } catch (\Throwable $e) {
            Log::error("数据库查询异常: {$userId}");
            DegradeService::recordFailure('user_service');
            
            // 根据异常类型决定是否降级
            if ($this->shouldDegradeOnDbError($e)) {
                return $this->getDefaultUserInfo($userId);
            }
            
            throw $e; // 重新抛出非降级异常
        }
    }
    
    /**
     * 判断数据库异常是否应该触发降级
     */
    private function shouldDegradeOnDbError(\Throwable $e): bool
    {
        $errorMessage = $e->getMessage();
        $degradeErrors = [
            'timeout', 'TIMEOUT', '连接超时', 'Connection timed out',
            'too many connections', 'max_connections',
            'server has gone away'
        ];
        
        foreach ($degradeErrors as $error) {
            if (stripos($errorMessage, $error) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 从本地缓存获取(如APCu、共享内存)
     */
    private function getFromLocalCache(int $userId): ?array
    {
        if (function_exists('apcu_fetch')) {
            $key = "local_user_{$userId}";
            $data = apcu_fetch($key, $success);
            if ($success) {
                return $data;
            }
        }
        
        return null;
    }
    
    /**
     * 用户不存在的处理
     */
    private function getUserNotFoundInfo(int $userId): array
    {
        return [
            'id' => $userId,
            'exists' => false,
            'message' => '用户不存在',
            'code' => 404
        ];
    }
    
    /**
     * 降级时的默认用户信息
     */
    private function getDefaultUserInfo(int $userId): array
    {
        return [
            'id' => $userId,
            'name' => '游客',
            'avatar' => '/static/images/default_avatar.png',
            'level' => 0,
            'is_degraded' => true,
            'degrade_time' => date('Y-m-d H:i:s'),
            'message' => '系统繁忙,正在降级运行'
        ];
    }
    
    /**
     * 异步设置缓存
     */
    private function setCacheAsync(string $key, $value, int $ttl): void
    {
        // 使用协程或队列异步处理
        go(function() use ($key, $value, $ttl) {
            try {
                $this->cache->set($key, $value, $ttl);
            } catch (\Throwable $e) {
                // 异步写入失败可忽略或记录日志
                Log::warning("异步缓存写入失败: {$key}");
            }
        });
    }
}

5. 控制器中使用

控制器示例 app/controller/User.php

php 复制代码
// 在控制器中
public function userInfo(int $id)
{
    try {
        $userService = new UserService();
        
        // 使用修正后的方法
        $userInfo = $userService->getUserInfo($id);
        
        return json([
            'code' => 0,
            'data' => $userInfo,
            'degraded' => $userInfo['is_degraded'] ?? false,
        ]);
        
    } catch (\Throwable $e) {
        // 只有非降级异常才会到达这里
        return json([
            'code' => 500,
            'msg' => '系统错误',
            'degraded' => false
        ]);
    }
}

6. 监控和告警

在应用入口文件添加监控

php 复制代码
// public/index.php 或 app/event.php
app()->event->listen('HttpEnd', function() {
    // 记录缓存命中率、降级次数等指标
    $metrics = [
        'cache_hits' => Cache::get('stats:hits', 0),
        'cache_misses' => Cache::get('stats:misses', 0),
        'degrade_count' => Cache::get('stats:degrade', 0),
    ];
    
    // 发送到监控系统
    // $this->sendToMonitor($metrics);
});

核心要点总结

  1. 多级降级:Redis → 文件缓存 → 默认值
  2. 自动切换:基于失败次数自动触发降级
  3. 手动控制:支持管理界面手动开关
  4. 渐进恢复:成功次数达标后自动恢复
  5. 超时控制:防止长时间阻塞
  6. 异步写入:不阻塞主流程

这样的实现既保证了系统的高可用性,又具备了生产环境所需的各项稳定性保障措施。

相关推荐
曲幽8 小时前
FastAPI分布式系统实战:拆解分布式系统中常见问题及解决方案
redis·python·fastapi·web·httpx·lock·asyncio
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082855 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php