在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. 异步写入:不阻塞主流程

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

相关推荐
少许极端19 小时前
Redis入门指南(五):从零到分布式缓存-其他类型及Java客户端操作redis
java·redis·分布式·缓存
以太浮标19 小时前
华为eNSP模拟器综合实验之-BGP路由协议的配置解析
服务器·开发语言·php
工具罗某人20 小时前
docker快速部署redis
redis·docker·容器
C_心欲无痕20 小时前
网络相关 - 强缓存与协商缓存讲解
前端·网络·网络协议·缓存
三两肉20 小时前
HTTP 缓存详解
网络协议·http·缓存
此生只爱蛋21 小时前
【Redis】RESP协议和库的安装
数据库·redis·缓存
亓才孓1 天前
封装类对象的缓存对象
java·jvm·缓存
kong79069281 天前
使用XXL-JOB任务定时更新缓存
缓存·xxl-job
Clarence Liu1 天前
redis学习(3) - 布隆过滤器
redis·学习·哈希算法
uup1 天前
SpringBoot 集成 Redis 实现分布式 WebSocket:跨实例消息推送实战
java·redis·websocket