缓存驱动

Viswoole 缓存采用驱动(Driver)架构:所有存储后端实现统一的 Viswoole\Cache\Contract\CacheDriverInterface 契约,由 CacheManager 注册为「商店(Store)」并提供统一调用入口。本篇说明内置 File / Redis 驱动的配置参数与扩展方式。

驱动体系

text
CacheDriverInterface(契约,定义全部缓存操作)

Driver(抽象基类:键前缀、标签、序列化、过期换算等通用能力)
   ├── Driver\File   文件驱动
   └── Driver\Redis  Redis 驱动(协程连接池)

抽象基类 Viswoole\Cache\Driver 已封装标签(tag() / getTags())、键前缀(getCacheKey())、序列化定制(setSerialize())等横切逻辑,具体驱动只需实现存储差异部分。自定义驱动实现接口或继承基类均可

配置驱动商店

驱动在 config/cache.phpstores 中注册,default 指定默认商店。每个商店支持三种定义形式:

php
use Viswoole\Cache\Driver\Redis;
use Viswoole\Cache\Facade\Cache;

return [
  'default' => env('cache.store', 'file'),
  'stores' => [
    // 形式一:驱动类名(无参构造)
    'file' => Cache::FILE_DRIVER,

    // 形式二:配置数组,options 为驱动构造参数(键名与构造参数一致)
    'redis' => [
      'driver' => Cache::REDIS_DRIVER,
      'options' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
      ],
    ],

    // 形式三:驱动实例
    'memory' => new Redis(host: '127.0.0.1'),
  ],
];
配置键类型默认值说明
defaultstring首个商店名默认商店名称,不区分大小写
storesarray[]商店列表,键为商店名,值为上述三种形式之一

商店名内部统一按小写索引;驱动类不存在、options 格式错误或未实现 CacheDriverInterface 时抛出 CacheErrorException

File 驱动

Viswoole\Cache\Driver\File 将缓存以序列化形式写入本地文件,无需外部依赖。TTL 通过文件头部嵌入的 expire(时间戳) 标记实现,读取时惰性检测过期并删除文件;竞争锁基于 flock 文件锁实现。

构造参数

参数类型默认值说明
storagestringBASE_PATH.'/runtime/cache'缓存文件存储根目录
prefixstring''缓存键前缀,用于命名空间隔离
tag_prefixstringtag:标签键前缀
tag_storestringTAG_STORE标签仓库键名
expireint0默认过期时间(秒),0 表示永不过期

配置示例:

php
'stores' => [
  'file' => [
    'driver' => Cache::FILE_DRIVER,
    'options' => [
      'storage' => BASE_PATH . '/runtime/cache',
      'prefix' => 'app:',
      'expire' => 3600,
    ],
  ],
],

存储结构

缓存键即文件名,键中可用 / 建立子目录层级:

text
runtime/cache/
├── user:1                      # 普通缓存文件
├── sub/
│   └── config:site             # 键含 / 时自动建立子目录
├── lock/                       # 竞争锁目录
│   └── lock_order:create       # 锁文件名为 lock_{scene}
└── tag:user:1                  # 标签集合文件

缓存键限制

File 驱动禁止缓存键中出现 .. 与反斜杠 \(路径穿越防护),命中时抛出 InvalidArgumentException

适用场景

  • 开发与测试环境快速验证
  • 单机部署的小型应用
  • 无 Redis 等外部依赖的轻量场景

Redis 驱动

Viswoole\Cache\Driver\Redis 基于 Redis 原生命令实现 TTL、原子自增自减与分布式锁(SET NX EX + Lua 比对删除),并自带协程连接池。

构造参数

参数类型默认值说明
hoststring127.0.0.1Redis 服务器地址
portint6379Redis 服务器端口
passwordstring''认证密码,空字符串表示无密码
db_indexint0数据库索引(0-15)
timeoutfloat0连接超时时间(秒),0 表示不限制
retry_intervalint1000重连间隔(毫秒)
read_timeoutfloat0读取超时时间(秒)
prefixstring''缓存键前缀
tag_prefixstringtag:标签键前缀
expireint0默认过期时间(秒),0 表示永不过期
tag_storestringTAG_STORE标签仓库键名
pool_max_sizeint10连接池最大连接数
pool_fill_sizeint0连接池预填充连接数

配置示例:

php
'redis' => [
  'driver' => Cache::REDIS_DRIVER,
  'options' => [
    'host' => env('REDIS_HOST', '127.0.0.1'),
    'port' => 6379,
    'password' => env('REDIS_PASSWORD', ''),
    'db_index' => 0,
    'prefix' => 'app:',
    'pool_max_size' => 64,
    'pool_fill_size' => 10,
  ],
],

连接池与协程隔离

驱动底层通过 Viswoole\Cache\RedisPool(继承通用连接池 ConnectionPool)管理连接,容量由 pool_max_size / pool_fill_size 控制:

  • 连接按协程借出:同一协程内的多次缓存操作复用同一条连接,避免协议串包
  • 协程结束(含异常退出)时通过 defer 自动归还连接,杜绝连接滞留
  • 锁的持有记录同样按协程隔离,并发协程不会误释放彼此的锁

Master/Manager 进程短连接

连接池归 Worker 进程所有:连接只在 Worker/TaskWorker 触发 workerStart 时独立建立与计数,Master/Manager 进程不持有池连接。若在服务启动阶段或主进程回调(onStart / onManagerStart 等)中使用 Cache,每次操作走一次性短连接(用完即毁),总连接数不会额外增加。命令行脚本中的 Redis 连接复用为进程级,复用前自动检测存活,断线后自动重建(含密码认证与库选择的会话状态恢复)。高频缓存操作场景请通过 Task::emit 投递到工作进程执行。

连接参数(主机、密码、超时等)由 Viswoole\Cache\RedisConfig 值对象承载,可脱离门面单独使用 new RedisPool(new RedisConfig(...)) 构建连接池。

数据类型处理

php
Cache::set('counter', 42);     // 整数跳过序列化,由 Redis 原生存储
Cache::get('counter');         // int(42)

Cache::set('price', 99.99);    // 浮点数走标准序列化
Cache::get('price');           // float(99.99)

Cache::set('user', ['n' => '张三']); // 复杂结构走标准序列化

数字字符串会被还原为数值

Redis 以字符串承载所有值,驱动读取时会将数字字符串自动转为 int / float。因此写入纯数字字符串(如 '123')读回将得到 int(123),此类数据请改用数组或加前缀存储。

适用场景

  • 生产环境高性能缓存
  • 多实例分布式部署共享缓存
  • 计数器、集合等需要原子操作的模型
  • 高并发分布式锁

驱动对比

特性FileRedis
读写性能低(磁盘 IO)高(内存操作)
外部依赖Redis 服务 + phpredis 扩展
分布式共享不支持原生支持
集合操作原子性模拟(读改写)原生 Set 命令
连接管理协程连接池
推荐环境开发 / 测试生产

自定义驱动

当内置驱动无法满足需求(如 Memcached、本地内存缓存)时可自行扩展,有两种方式:

方式一:继承 Driver 基类(推荐)

基类已处理标签、前缀、序列化等通用逻辑,只需实现存储相关的抽象方法:getsetincdecpullhasdeleteclearttllockunlockcloseconnectsAddArraygetArraysRemoveArray

php
namespace App\Cache;

use DateTime;
use Viswoole\Cache\Driver;

class MemoryDriver extends Driver
{
  /** @var array<string, mixed> 进程内存储(仅示例) */
  private array $items = [];

  public function get(string $key, mixed $default = null): mixed
  {
    $realKey = $this->getCacheKey($key); // 基类提供:拼接键前缀
    return isset($this->items[$realKey]) ? $this->items[$realKey] : $default;
  }

  public function set(
    string            $key,
    mixed             $value,
    DateTime|int|null $expire = null,
    bool              $NX = false
  ): bool {
    // 按 $NX 与过期时间实现写入逻辑
    return true;
  }

  // 其余抽象方法按接口契约逐一实现...
}

方式二:直接实现 CacheDriverInterface

不继承基类、完全自行实现 CacheDriverInterface 的全部方法(含标签与锁)。适合需要彻底自定义键规则或标签语义的场景。

注册与使用

php
// 方式一:写入 config/cache.php(推荐)
'stores' => [
  'memory' => [
    'driver' => \App\Cache\MemoryDriver::class,
    'options' => ['prefix' => 'app:'],
  ],
],

// 方式二:运行时注册
Cache::addStore('memory', new \App\Cache\MemoryDriver());
php
Cache::store('memory')->set('key', 'value', 60);

序列化定制

默认使用 PHP serialize / unserialize。出于对象注入(CWE-502)防护或跨语言读取的考虑,可通过 setSerialize() 切换为 JSON 等格式:

php
Cache::store('memory')->setSerialize(
  fn(mixed $data): string => json_encode($data, JSON_UNESCAPED_UNICODE),
  fn(string $data): mixed => json_decode($data, true)
);

存储介质安全

缓存介质(runtime 目录、Redis)一旦被同权限进程篡改,基于 unserialize 的默认方案可能构成反序列化攻击面。请确保存储介质的访问控制,安全敏感场景建议切换为 JSON 序列化。

下一步