shabbyrobe/cachet

此包已被废弃且不再维护。作者建议使用 symfony/cache 包。

适用于 PHP 5.6 或更高版本的插件式缓存

维护者

详细信息

git.sr.ht/~shabbyrobe/cachet

主页

v4.0.0 2021-06-14 09:48 UTC

This package is auto-updated.

Last update: 2024-09-07 09:48:55 UTC


README

警告:我已经超过十年没有在严肃的场合使用它了。世界已经远离了当时这些方法还是好的方法的时代。即使我再次开始使用PHP,我也不会恢复它。如果你使用它,我 强烈 建议你尽快放弃。

cachet |ˈkaʃeɪ| 名词,一种扁平的胶囊,封装着味道不好吃的药物。

特性

  • 支持PHP 5.6及以上版本(5.6的支持将在2019年停止)
  • 可交换的后端
  • 支持Redis、MySQL、APCu、Memcached、SQLite等
  • 复合后端,用于级联和分片
  • 对后端(尽可能)进行内存高效的迭代
  • 通过依赖项进行动态项目过期
  • 针对冲击保护的锁定策略
  • 原子计数器
  • 会话处理
  • 可选的通过适配器支持psr16

.. 内容:

:depth: 3

安装

Cachet 可以使用 Composer <https://getcomposer.org.cn> 安装

composer require shabbyrobe/cachet:3.0.*

您也可以直接从 GitHub Releases <https://github.com/shabbyrobe/cachet/releases> 页面下载 Cachet

用法

实例化后端和缓存

<?php
$backend = new Cachet\Backend\APCU();
$cache = new Cachet\Cache('mycache', $backend);

基本操作(`set, get, delete, has, flush`)

<?php
$cache->set('foo', 'bar');
$value = $cache->get('foo');
$cache->delete('foo');
$cache->flush();
$exists = $cache->has('foo');

// Store anything as long as it's serializable
$cache->set('foo', array(1, 2, 3));
$cache->set('foo', (object) array('foo'=>'bar'));
$cache->set('foo', null);

许多“假值”是有效的缓存值,例如 `nullfalse`. 查看值是否实际找到

<?php
$cache->set('hmm', false);
if (!$cache->get('hmm')) {
    // this will also execute if the 'false' value was actually
    // retrieved from the cache
}

$value = $cache->get('hmm', $found);
if (!$found) {
    // this will only execute if no value was found in the cache.
    // it will not execute if values which evaluate to false were
    // retrieved from the cache.
}

通过依赖项动态过期数据

<?php
// Expire in 30 seconds
$cache->set('foo', 'bar', 30);

// Expire when a file modification time is changed
$cache->set('foo', 'bar', new Cachet\Dependency\File('/tmp/test'));
$cache->get('foo') == 'bar';   // true
touch('/tmp/test');
$cache->get('foo') == 'bar';   // false

Cachet 提供了一种方便的方法来使用策略包装获取和设置,并可选择锁定

<?php
$value = $cache->wrap('foo', function() use ($service, $param) {
    return $service->doSlowStuff($param); 
});

$dataRetriever = function() use ($db) {
    return $db->query("SELECT * FROM table")->fetchAll();
}

// With a TTL
$value = $cache->wrap('foo', 300, $dataRetriever);

// With a Dependency
$value = $cache->wrap('foo', new Cachet\Dependency\Permanent(), $dataRetriever);

// Set up a rotating pool of 4 file locks (using flock)
$hasher = function($cache, $key) {
    return $cache->id."/".(abs(crc32($key)) % 4);
};
$cache->locker = new Cachet\Locker\File('/path/to/locks', $hasher);

// Stampede protection - the cache will stop and wait if another concurrent process 
// is running the dataRetriever. This means that the cache ``set`` will only happen once:
$value = $cache->blocking('foo', $dataRetriever);

迭代 - 这很棘手,并充满了警告。请参阅下面的迭代部分,其中详细描述了它们

<?php
$cache = new Cachet\Cache($id, new Cachet\Backend\Memory());
$cache->set('foo', 'bar');

// this dependency is just for demonstration/testing purposes.
// iteration will not return this value as the dependency is invalid 
$cache->set('baz', 'qux', new Cachet\Dependency\Dummy(false));

foreach ($cache->values() as $key=>$value) {
    echo "$key: $value\n";
}
// outputs "foo: bar" only.

原子计数器

<?php
$counter = new Cachet\Counter\APCU();

// returns 1
$value = $counter->increment('foo');

// returns 2
$value = $counter->increment('foo');

// returns 1
$value = $counter->decrement('foo');

// returns 4
$value = $counter->increment('foo', 3);

// force a counter's value
$counter->set('foo', 100);

// inspect a counter's value
$value = $counter->value('foo');

PSR-16 支持

Cachet 支持 PSR-16 <https://www.php-fig.org/psr/psr-16>,这是一个 PHP-FIG 推荐的简单缓存接口。Cachet 是作为一种对早期 PSR-6 <https://www.php-fig.org/psr/psr-6> 建议不合理扩展的直接反应而创建的,因此看到更好的替代品令人欣慰。

PSR-16 是一种最低共同基数尝试,旨在为像 Cachet 这样的不同缓存 API 提供接口,而 Cachet 本身也是一种最低共同基数尝试,旨在为像 Redis、APCU 等不同的缓存后端提供接口,因此当你到达像 `Psr\SimpleCache\Cache` 这样的接口时,你已经失去了许多功能(如迭代器、锁定、区分“null”和“未设置”的能力)。我并不一定建议使用 PSR-16 接口而不是直接使用 Cachet 的 API,但在某些情况下它可能很有用,并且与 PSR-6 不同,它很容易实现,所以如果你认为它有用,这里就是!享受吧!

要使用适配器,创建一个 `Cachet\Cache` 就像平常一样,然后用 `Cachet\Simple\Cache` 包裹它

<?php
$backend = new Cachet\Backend\APCU();
$cache = new Cachet\Cache('mycache', $backend);
$simple = new Cachet\Simple\Cache($cache);

迭代

缓存可以迭代,但支持有限。如果底层后端支持列出键,则迭代通常效率很高。Cachet APCU 后端使用 `APCIterator` 类,并且非常高效。Memcached_ 提供了没有任何方法来迭代键。

如果后端支持迭代,它将实现`Cachet\Backend\Iterator`。实现此接口不是必需的,但所有随Cachet提供的后端都实现了。如果底层后端不支持迭代(例如Memcache),则Cachet提供了对使用支持迭代键的辅助后端的可选支持。这将减慢插入、删除和刷新的速度,但不会影响检索。

后端提供的不同迭代支持类型包括

  • 迭代器:通过一个`\\Iterator`类有效地实现迭代。只有在需要时才会检索和返回键/项。此类型的迭代应该不会有内存问题。

  • 键数组 + 检索器:一次性检索所有键。直接从后端逐个检索项。数百万个键可能会导致内存问题。

  • 所有数据:一次性返回所有内容。这仅适用于内存缓存或会话缓存,在这些情况下没有其他选项。数千个键可能会导致内存问题。

  • 可选键后端:键存储在辅助可迭代的后端中。设置、删除和刷新将变得更慢,因为这些操作需要在后端和键后端上执行。内存问题会从键后端继承,因此您应尽可能使用基于`Iterator`的键后端。

    键后端迭代是可选的。如果没有提供键后端,则迭代将失败。

后端

缓存后端必须实现`Cache\Backend`,尽管某些后端需要付出更多努力才能满足接口。

不同的后端对以下功能的支持程度不同

自动过期

某些后端支持某些依赖类型(dependency_)的自动过期。当后端支持此功能时,它将有一个`useBackendExpirations`属性,默认值为`true`

例如,APCU后端将在传递`Cachet\Dependency\TTL`时检测到并自动将其用于`apcu_store`的第三个参数,该参数接受以秒为单位的TTL。其他后端支持不同依赖类型的展开方法。以下将进行说明。

`useBackendExpirations`设置为`false`并不保证后端在其他情况下不会过期缓存值。

迭代

后端应该(但可能不一定)实现`Cache\Backend\Iterator`。不实现的不能迭代。这将在每个后端的文档中指定。

受这些限制的后端可以扩展`Cachet\Backend\IterationAdapter`,这允许使用第二个后端来存储键。这将减慢设置、删除和刷新,但根本不影响从后端获取项的速度,因此在需要迭代且读取次数远多于写入次数的情况下,这不是一个糟糕的权衡。

这种方法存在一些潜在的风险

  • 如果项从键后端消失,它可能仍然存在于后端本身中。如果后端不支持迭代,则无法检测这些值。请确保您选择的键后端类型在任何情况下都不会自动过期值,并且如果您的后端支持`useBackendExpirations`,请将其设置为`false`

  • 可用于键后端的后端类型相当有限——它本身必须是可迭代的,并且不能是`Cachet\Backend\IterationAdapter`

APCU

仅支持`apcu`扩展,不包含向后兼容函数。

对于需要`apc`支持的旧代码,请使用`Cachet\Backend\APC`,尽管它已过时。您真的应该升级到PHP >=7.0并使用`apcu`

  • 迭代支持:迭代器
  • 后端过期:`Cachet\Expiration\TTL`
<?php
$backend = new Cachet\Backend\APCU();

// Or with optional cache value prefix. Prefix has a forward slash appended:
$backend = new Cachet\Backend\APCU("myprefix");

$backend->useBackendExpirations = true; 

PHPRedis

需要 phpredis <http://github.com/nicolasff/phpredis> 扩展。

  • 迭代支持: 键数组 + 捕获器

  • 后端过期: `Cachet\Expiration\TTL`Cachet\Expiration\Time`Cachet\Expiration\Permanent`

<?php
// pass Redis server name/socket as string. connect-on-demand.
$backend = new Cachet\Backend\PHPRedis('127.0.0.1');

// pass Redis server details as array. connect-on-demand. all keys
// except host optional
$redis = [
    'host'=>'127.0.0.1',
    'port'=>6739,
    'timeout'=>10,
    'database'=>2
];
$backend = new Cachet\Backend\PHPRedis($redis);

// optional cache value prefix. Prefix has a forward slash appended:
$backend = new Cachet\Backend\PHPRedis($redis, "myprefix");

// pass existing Redis instance. no connect-on-demand.
$redis = new Redis();
$redis->connect('127.0.0.1');
$backend = new Cachet\Backend\PHPRedis($redis);

文件

文件系统缓存。仅在 OS X 和 Linux 上进行了测试,但可能在 Windows 上也能工作(可能应该如此 - 如果不工作,请提交错误报告)。

缓存速度并不快。刷新和迭代可能非常、非常慢,但不应出现内存问题。

如果您使用此缓存,请进行一些性能分析,看看它是否比没有缓存要快。

  • 迭代支持:迭代器
  • 后端过期:
<?php
// Inherit permissions, user and group from the environment
$backend = new Cachet\Backend\File('/path/to/cache');

// Passing options
$backend = new Cachet\Backend\File('/path/to/cache', array(
    'user'=>'foo',
    'group'=>'foo',
    'filePerms'=>0666,   // Important: must be octal
    'dirPerms'=>0777,    // Important: must be octal
));

Memcache

需要 `memcached` PHP 扩展。

  • 迭代支持: 可选键后端
  • 后端过期:`Cachet\Expiration\TTL`
<?php
// Connect on demand. Constructor accepts the same argument as Memcached->addServers()
$backend = new Cachet\Backend\Memcached(array(array('127.0.0.1', 11211)));

// Use existing Memcached instance:
$memcached = new Memcached();
$memcached->addServer('127.0.0.1');
$backend = new Cachet\Backend\Memcached($memcached);

$backend->useBackendExpirations = true; 

默认不支持刷新,但在提供键后端的情况下可以正常工作。如果您不想使用键后端,可以激活不安全刷新模式,这将简单地刷新您的整个 memcache 实例,而不管它是针对哪个缓存。

<?php
// using a key backend, no surprises
$backend = new Cachet\Backend\Memcached($servers);
$backend->setKeyBackend($keyBackend);

$cache1 = new Cachet\Cache('cache1', $backend);
$cache2 = new Cachet\Cache('cache2', $backend);
$cache1->set('foo', 'bar');
$cache2->set('baz', 'qux');

$cache1->flush();
var_dump($cache2->has('baz'));  // returns true


// using unsafe flush
$backend = new Cachet\Backend\Memcached($servers);
$backend->unsafeFlush = true;

$cache1 = new Cachet\Cache('cache1', $backend);
$cache2 = new Cachet\Cache('cache2', $backend);
$cache1->set('foo', 'bar');
$cache2->set('baz', 'qux');

$cache1->flush();
var_dump($cache2->has('baz'));  // returns false!

内存

请求或 CLI 运行的内存缓存。

  • 迭代支持: 所有数据
  • 后端过期:
<?php
$backend = new Cachet\Backend\Memory();

PDO

支持 MySQL 和 SQLite。欢迎提供其他数据库支持补丁,前提是它们很简单。

  • 迭代支持: 键数组 + 捕获器(或如果使用 MySQL,可选支持 迭代器
  • 后端过期:
<?php
// Pass connection info array (supports connect on demand)
$backend = new Cachet\Backend\PDO(array(
    'dsn'=>'sqlite:/tmp/pants.sqlite',
));
$backend = new Cachet\Backend\PDO(array(
    'dsn'=>'mysql:host=localhost',
    'user'=>'user',
    'password'=>'password',
));

// Pass connector function (supports connect on demand)
$backend = new Cachet\Backend\PDO(function() {
    return new \PDO('sqlite:/tmp/pants.sqlite');
});

// Use an existing PDO (not recommended - doesn't support disconnection
// or connect-on-demand):
$backend = new Cachet\Backend\PDO(new PDO('sqlite:/tmp/pants.sqlite'));

PDO 后端为每个 `Cachet\Cache` 实例使用一个单独的表。表名是基于缓存 ID 的值,该值以 PDO->cacheTablePrefix` 的值作为前缀,默认为 cachet_`。

<?php
$backend->cacheTablePrefix = "foo_";

表不会自动创建。调用此方法以确保您的缓存存在表

<?php
$cache = new Cachet\Cache('pants', $backend);
$backend->ensureTableExistsForCache($cache);

如果您正在编写 Web 应用程序,则不应在每次请求时都这样做,而应在您的部署或设置过程中完成。

PDO 后端默认使用键数组 + 捕获器进行迭代,这并不免疫于内存耗尽问题。`mysqlUnbufferedIteration` 将解决任何内存问题,使 PDO 后端成为一流的迭代公民。缺点是每次迭代缓存时都会建立到数据库的额外连接。此连接将在 $backend->keys()$backend->items() 返回的迭代器对象的作用域内保持打开状态。

<?php
// Use an unbuffered query for the key iteration (MySQL only):
$backend->mysqlUnbufferedIteration = true;

默认禁用此选项,并且如果底层连接器的引擎不是 MySQL,则忽略此选项。

会话

使用 PHP `$_SESSION` 作为缓存。应小心避免未检查的增长。如果尚未调用 session_start()`,则将自动调用它,因此如果您想自定义会话启动,请先自己调用 session_start()`。

  • 迭代支持: 所有数据
  • 后端过期:
<?php
$session = new Cachet\Backend\Session();

级联

允许按优先级顺序遍历多个后端。如果在较低优先级后端中找到值,则将其插入到列表中每个更高优先级后端。

当最快的后端具有最高优先级(列表中较早的位置)时,这效果最好。

值按逆优先级顺序设置在所有缓存中。

  • 迭代支持:由最低优先级缓存支持的任何内容
  • 后端过期:不适用
<?php
$memory = new Cachet\Backend\Memory();
$apcu = new Cachet\Backend\APCU();
$pdo = new Cachet\Backend\PDO(array('dsn'=>'sqlite:/path/to/db.sqlite'));
$backend = new Cachet\Backend\Cascading(array($memory, $apcu, $pdo));
$cache = new Cachet\Cache('pants', $backend);

// Value is cached into Memory, APCU and PDO
$cache->set('foo', 'bar');

// Prepare a little demonstration
$memory->flush();
$apcu->flush();

// Memory is queried and misses
// APCU is queried and misses
// PDO is queried and hits
// Item is inserted into APCU
// Item is inserted into Memory
$cache->get('foo');

分片

允许缓存为每个键选择几个后端之一。只要后端列表始终相同,就保证为相同的键选择相同的后端。

  • 迭代支持:完全迭代每个后端。
  • 后端过期:不适用
<?php
$memory1 = new Cachet\Backend\Memory();
$memory2 = new Cachet\Backend\Memory();
$memory3 = new Cachet\Backend\Memory();

$backend = new Cachet\Backend\Sharding(array($memory1, $memory2, $memory3));
$cache = new Cachet\Cache('pants', $backend);

$cache->set('qux', '1');
$cache->set('baz', '2');
$cache->set('bar', '3');
$cache->set('foo', '4');

var_dump(count($memory1->data));  // 1
var_dump(count($memory2->data));  // 1
var_dump(count($memory3->data));  // 2

策略

`Cachet\Cache` 提供一系列策略方法。大多数方法都需要为缓存提供锁实现。它们都遵循相同的通用 API

$cache->strategyName(string $key, callable $dataRetriever);
$cache->strategyName(string $key, int $ttl, callable $dataRetriever);
$cache->strategyName(string $key, $dependency, callable $dataRetriever);

某些策略有一些小的例外,下面有说明。

大多数策略都与锁柜_进行交互,有些策略要求如果后端支持 `useBackendExpirations,则必须将其设置为 false`。

包装

  • 需要锁柜_: 不需要
  • 后端过期: 启用或禁用
  • API偏差: 不需要

Cachet 提供的最简单的缓存策略是 `wrap` 策略。它不会做任何事情来防止拥挤,但它不需要锁柜,可以通过减少样板代码使您的代码更加简洁。当使用 `wrap` 时,您可以转换以下代码

<?php
$value = $cache->get('key', $found);
if (!$found) {
    $value = $service->findExpensiveValue($blahBlahBlah);
    if ($value)
        $cache->set('key', $value);
}

用这个

<?php
$value = $cache->wrap('key', function() use ($service, $blahBlahBlah) {
    return $service->findExpensiveValue($blahBlahBlah);
};

阻塞

  • 需要锁柜_: 阻塞
  • 后端过期: 启用或禁用
  • API偏差: 不需要

这需要锁柜_。在缓存未命中时,请求将尝试获取锁,然后再调用数据检索函数。在数据检索后,将释放锁。任何导致缓存未命中的并发请求都将阻塞,直到获取锁的请求释放锁。

如果后端支持它且将 `useBackendExpirations 设置为 true`,则此策略不应受到不利影响,尽管如果您缓存的项目在仅几秒钟后频繁过期,您可能会遇到麻烦。

<?php
$cache->locker = create_my_locker();
echo sprintf("%s %s start\n", microtime(true), uniqid('', true));
$value = $cache->blocking('key', function() {
    sleep(10);
    return get_stuff();
});
echo sprintf("%s %s end\n", microtime(true), uniqid('', true));

以下代码将输出类似的内容(uniqids 将更加复杂):

1381834595 1 start
1381834599 2 start
1381834605 1 end
1381834605 2 end 

安全非阻塞

  • 需要锁柜_: 非阻塞
  • 后端过期: 必须禁用
  • API偏差: 不需要

这需要锁柜_。如果缓存未命中,第一个请求将获取锁并运行数据检索函数。后续请求如果可用,将返回一个过期的值,否则它们将阻塞,直到第一个请求完成,从而保证始终返回一个值。

如果后端具有 `useBackendExpirations 属性并且将其设置为 true`,则此策略将失败。

<?php
$cache->locker = create_my_locker();
$value = $cache->safeNonBlocking('key', function() {
    return get_stuff();
});

不安全非阻塞

  • 需要锁柜_: 非阻塞
  • 后端过期: 必须禁用
  • API偏差:

这需要锁柜_。如果缓存未命中,第一个请求将获取锁并运行数据检索函数。后续请求如果可用,将返回一个过期的值,否则它们将立即返回空值。

由于此策略的 API 与其他策略略有不同,它不能保证返回一个值,因此它提供了一个可选的输出参数 `$found` 来表示方法已返回,但没有检索或设置值。

如果后端具有 `useBackendExpirations 属性并且将其设置为 true`,则此策略将失败。

<?php
$cache->locker = create_my_locker();

$dataRetriever = function() use ($params) {
    return do_slow_stuff($params);
};

$value = $cache->unsafeNonBlocking('key', $dataRetriever);
$value = $cache->unsafeNonBlocking('key', $ttl, $dataRetriever);
$value = $cache->unsafeNonBlocking('key', $dependency, $dataRetriever);

$value = $cache->unsafeNonBlocking('key', $dataRetriever, null, $found);
$value = $cache->unsafeNonBlocking('key', $ttl, $dataRetriever, $found);
$value = $cache->unsafeNonBlocking('key', $dependency, $dataRetriever, $found);

锁柜

锁柜处理在各个缓存策略_之间请求的同步。它们必须能够支持获取时的阻塞,并且应该能够支持非阻塞获取。

当策略_获取锁柜时,将传递缓存和键。如果您想为每个缓存键使用一个锁,则可以使用原始键,但如果您想减少锁的数量,可以将可调用的对象作为 `$keyHasher` 参数传递给锁柜的构造函数。您可以使用它来返回一个更简单的键版本。

<?php
// restrict to 4 locks per cache
$keyHasher = function($cacheId, $key) {
    return $cacheId."/".abs(crc32($key)) % 4;
};

警告:锁柜不支持超时。当前的锁定实现中没有任何一种允许超时,因此您必须依靠仔细调整的 `max_execution_time` 属性来在死锁的情况下获得“安全性”。这可能会在未来改变,但必须等到平台支持改进(可能不会)之后,现有的锁柜实现才能改变。

文件

  • 支持的锁定模式: 阻塞非阻塞

使用 `flock` 来处理锁定。需要存储锁的专用可写目录。

<?php
$locker = new Cachet\Locker\File('/path/to/lockfiles');
$locker = new Cachet\Locker\File('/path/to/lockfiles', $keyHasher);

文件锁柜支持与 `Cachet\Backend\File` 相同的选项数组。

<?php
$locker = new Cachet\Locker\File('/path/to/lockfiles', $keyHasher, [
    'user'=>'foo',
    'group'=>'foo',
    'filePerms'=>0666,   // Important: must be octal
    'dirPerms'=>0777,    // Important: must be octal
]);

如果 `$keyHasher 返回的值包含 / 字符,则将它们转换为路径段(即 mkdir -p`)。

信号量

  • 支持的锁定模式: 阻塞

使用PHP的信号量功能(https://php.ac.cn/manual/en/book.sem.php)提供锁定。PHP必须使用`--enable-sysvsem`编译才能正常工作。

此锁不支持非阻塞获取。

<?php
$locker = new Cachet\Locker\Semaphore($keyHasher);

依赖项

Cachet支持缓存依赖项的概念 - 实现`Cachet\Dependency`的对象会与您的缓存值一起序列化,并在检索时进行检查。任何可序列化的代码都可以用于依赖项,因此这为无效化提供了大量可能性,远超过TTL所能实现的范围。

可以使用`Cachet\Cache->set($key, $value, $dependency)`或使用Cachet\Cache->set($key, $value, $ttl)简写来按项目传递依赖项。简写等价于$cache->set($key, $value, new Cachet\Dependency\TTL($ttl))`。

如果没有依赖项,缓存项将保留在缓存中,直到手动删除或底层后端决定自行删除。

您可以为整个缓存指定一个默认依赖项,如果为某个项未提供依赖项

<?php
$cache = new Cachet\Cache($name, $backend);

// all items that do not have a dependency will expire after 10 minutes
$cache->dependency = new Cachet\Dependency\TTL(600);

// this item will expire after 10 minutes
$cache->set('foo', 'bar');

// this item will expire after 5 minutes
$cache->set('foo', 'bar', new Cachet\Dependency\TTL(300));

警告:即使项已过期,这并不意味着它已被删除。过期项将在检索时被删除,但垃圾回收是一个手动过程,应由单独的过程执行。

TTL

<?php
// cache for 5 minutes
$cache->set('foo', 'bar', new Cachet\Dependency\TTL(300));

永久

即使Cache提供了默认依赖项,Cachet也不会使缓存项过期。这可能会被任何特定环境的后端配置覆盖(例如,apc.ttl配置设置https://php.ac.cn/manual/en/apcu.configuration.php#ini.apcu.ttl

<?php
$cache = new Cachet\Cache($name, $backend);
$cache->dependency = new Cachet\Dependency\TTL(600);

// this item will expire after 10 minutes
$cache->set('foo', 'bar');

// this item will never expire
$cache->set('foo', 'bar', new Cachet\Dependency\Permanent());

时间

该项被认为在固定时间戳无效

<?php
$cache->set('foo', 'bar', new Cachet\Dependency\Time(strtotime('Next week')));

Mtime

支持基于文件修改时间缓存项的无效化。

<?php
$cache->set('foo', 'bar', new Cachet\Dependency\Mtime('/path/to/file');
$cache->get('foo'); // returns 'bar'

touch('/path/to/file');
$cache->get('foo'); // returns null

缓存标签

这与`Mtime`依赖项非常相似,只是它使用二级缓存而不是简单的文件mtimes,并检查标签值是否已更改。

此依赖项的配置稍微复杂一些 - 它需要将二级缓存注册为一级缓存的服务。

<?php
$valueCache = new Cachet\Cache('value', new Cachet\Backend\APCU());
$tagCache = new Cachet\Cache('value', new Cachet\Backend\APCU());

$tagCacheServiceId = 'tagCache';
$valueCache->services[$tagCacheServiceId] = $tagCache;

// the value at key 'tag' in $tagCache is stored alongside 'foo'=>'bar' in the
// $valueCache. It will be checked against whatever is currently in $tagCache
// on retrieval
$valueCache->set('foo', 'bar', new Cachet\Dependency\CachedTag($tagCacheServiceId, 'tag'));
$valueCache->set('baz', 'qux', new Cachet\Dependency\CachedTag($tagCacheServiceId, 'tag'));

// 'tag' has not changed in $tagCache since we set these values in $valueCache
$valueCache->get('foo');  // returns 'bar'
$valueCache->get('baz');  // returns 'qux'

$tagCache->set('tag', 'something else');

// 'tag' has since changed, so the values coming out of $valueCache are invalidated
$valueCache->get('foo');  // returns null
$valueCache->get('baz');  // returns null

默认情况下,通过构造函数传递第三个布尔参数可以启用严格模式比较(默认为宽松模式`==`)。

<?php
$dependency = new Cachet\Dependency\CachedTag($tagCacheServiceId, 'tag', !!'strict');

严格模式使用`===`(对于除对象外的一切),对于对象则使用`==`。这是因为`===`永远不会匹配对象为`true`,因为它仅比较引用;要比较的值已从不同的缓存中检索,因此它们极不可能共享引用。

组合

检查多个依赖项。可以设置为在任何依赖项有效时有效,或当所有依赖项都有效时有效。

所有模式:如果项小于5分钟且文件`/path/to/file`未被触摸,则以下内容将被视为有效。

<?php
$cache->set('foo', 'bar', new Cachet\Dependency\Composite('all', array(
    new Cachet\Dependency\Mtime('/path/to/file'),
    new Cachet\Dependency\TTL(300),
));

任何模式:当项小于5分钟或文件`/path/to/file`未被触摸时,以下内容将被视为有效。

<?php
$cache->set('foo', 'bar', new Cachet\Dependency\Composite('any', array(
    new Cachet\Dependency\Mtime('/path/to/file'),
    new Cachet\Dependency\TTL(300),
));

Session处理器

`Cachet\Cache`可以注册以处理PHP的`$_SESSION`超全局变量。

<?php
$backend = new Cachet\Backend\PDO(['dsn'=>'sqlite:/path/to/sessions.sqlite']);
$cache = new Cachet\Cache('session', $backend);

// this must be called before session_start()
Cachet\SessionHandler::register($cache);

session_start();
$_SESSION['foo'] = 'bar';

默认情况下,当调用`gc`(垃圾回收)方法时,`Cachet\SessionHandler`不会执行任何操作。这是因为缓存迭代不能保证性能 - 这是一个特定于后端的特点,可能差异很大(有关更多详细信息,请参阅迭代_部分),并且开发人员在选择后端时必须意识到这一点。

您可以通过以下方式激活自动垃圾回收:

<?php
Cachet\SessionHandler::register($cache, ['runGc'=>true]);

// or...
Cachet\SessionHandler::register($cache);
Cachet\SessionHandler::$instance->runGc = true;

对于不使用`Iterator`进行迭代的后端,强烈建议您使用单独的过程实现垃圾回收,而不是使用PHP的gc概率机制。

以下后端不应与`SessionHandler`一起使用。

  • `Cachet\Backend\File`:这将引发警告。我认为PHP的默认文件会话机制并不优于这个后端——它们本质上做的是同一件事,只是一个是用C语言实现的,并且经过严重测试,另一个则不是。

  • `Cachet\Backend\Session`:这将引发异常。您不能使用会话来存储会话。

  • `Cachet\Backend\Memory`:这根本无法工作——数据将在请求完成后消失。

计数器

一些后端提供了用于原子地增加或减少整数的函数。Cachet试图为这些功能提供一个一致的接口。

遗憾的是,它并不总是成功。有一些限制(比如总是)

  • 在某些情况下,尽管后端的增加和减少方法工作在原子级别,但它们需要您在可以以非原子方式使用它之前设置值。Cachet计数器接口允许您在没有值已设置的情况下调用增加。

    遗憾的是,这意味着多个并发进程都可以调用`$backend->increment()`并看到那里什么都没有,在其中一个进程有机会调用set初始化计数器之前。表现出这种行为的计数器可以通过传递可选的locker_来缓解这个问题。

  • 所有后端都支持递减到零以下,除了Memcache。

  • 一些后端对最大计数器值有限制,并且当达到这个值时将溢出。还没有进行足够的测试来确定每个计数器后端的最大值,这可能与平台和构建有关。已经提供了一个估计值,但这基于ARM架构。YMMV。

  • 计数器不支持依赖关系,但某些计数器允许为所有计数器指定单个TTL。这由`$backend->counterTTL`属性表示。

  • 确实存在一个著名的计数器类,它是原子的,不会溢出,并支持任何类型的缓存依赖(`Cachet\Counter\SafeCache`)。遗憾的是,它很,并且需要锁。快速、安全、便宜、稳定、好。请选择两个。

为什么计数器不是`Cachet\Cache`的一部分?我最初尝试以这种方式实现,但花了一些时间黑客攻击后,无法摆脱这种感觉,即我在破坏支持它的美好和干净的东西,我意识到这是一个单独的责任,值得拥有自己的层次结构。计数器和后端之间也没有清晰的1对1关系。

计数器实现了`Cachet\Counter`接口,并支持以下API

<?php
// You can increment an uninitialised counter:
// $value == 1
$value = $counter->increment('foo');

// You can also increment by a custom step value:
// $value == 5
$value = $counter->increment('foo', 4);

// $value = 4
$decremented = $counter->decrement('foo');

// $value = 1
$decremented = $counter->decrement('foo', 3);

// $value = 1
$value = $counter->value('foo');

$counter->set('foo', 100);

APCU

仅支持`apcu`扩展,不包含向后兼容函数。

对于需要`apc`支持的旧代码,请使用`Cachet\Counter\APC`,尽管它已弃用。您真的应该升级到PHP >=7.0并使用`apcu`

  • 支持`counterTTL`
  • 原子性:部分。带可选锁器的完全
  • 范围:`-PHP_INT_MAX - 1`PHP_INT_MAX
  • 溢出错误:没有
<?php
$counter = new \Cachet\Counter\APCU();

// Or with optional cache value prefix. Prefix has a forward slash appended.
$counter = new Cachet\Counter\APCU('myprefix');

// TTL
$counter->counterTTL = 86400;

// If you would like set operations to be atomic, pass a locker to the constructor
// or assign to the ``locker`` property
$counter->locker = new \Cachet\Locker\Semaphore();
$counter = new \Cachet\Counter\APCU('myprefix', \Cachet\Locker\Semaphore());

PHPRedis

  • 支持`counterTTL`
  • 原子性:
  • 范围:`-INT64_MAX - 1`INT64_MAX
  • 溢出错误:
<?php
$redis = new \Cachet\Connector\PHPRedis('127.0.0.1');
$counter = new \Cachet\Counter\PHPRedis($redis);

// Or with optional cache value prefix. Prefix has a forward slash appended.
$counter = new \Cachet\Counter\PHPRedis($redis, 'prefix');

Redis本身支持将TTL应用于计数器,但我还没有想出原子地实现它的最佳方法。请将其视为正在进行中的工作。

Memcache

  • 支持`counterTTL`
  • 原子性:部分。带可选锁器的完全
  • 范围:`-PHP_INT_MAX - 1 to PHP_INT_MAX`
  • 溢出错误:没有
<?php
// Construct by passing anything that \Cachet\Connector\Memcache accepts as its first
// constructor argument:
$counter = new \Cachet\Counter\Memcache('127.0.0.1');

// Construct by passing in a connector. This allows you to share a connector instance 
// with a cache backend:
$memcache = new \Cachet\Connector\Memcache('127.0.0.1');
$counter = new \Cachet\Counter\Memcache($memcache);
$backend = new \Cachet\Backend\Memcache($memcache);

// Optional cache value prefix. Prefix has a forward slash appended.
$counter = new \Cachet\Counter\Memcache($memcache, 'prefix');

// TTL
$counter->counterTTL = 86400;

// If you would like set operations to be atomic, pass a locker to the constructor
// or assign to the ``locker`` property
$counter->locker = $locker;
$counter = new \Cachet\Counter\Memcache($memcache, 'myprefix', $locker);

PDOSQLite和PDOMySQL

与PDO缓存后端不同,不同的数据库引擎需要为计数器操作执行非常不同的查询。如果您的PDO引擎是sqlite,请使用`Cachet\Counter\PDOSQLite`。如果您的PDO引擎是MySQL,请使用`Cachet\Counter\PDOMySQL`。虽然`PDOSQLite`可能与其他数据库后端兼容(尽管这尚未经过测试),但`PDOMySQL`使用MySQL特定的查询。

表名默认对所有计数器为 `cachet_counter`。这可以被更改。

  • 支持 `counterTTL`不支持
  • 原子性:可能(我还没有确信我已经证明了这一点)
  • 范围:`-INT64_MAX - 1 到 INT64_MAX`
  • 溢出错误:没有
<?php
// Construct by passing anything that \Cachet\Connector\PDO accepts as its first
// constructor argument:
$counter = new \Cachet\Counter\PDOSQLite('sqlite::memory:');
$counter = new \Cachet\Counter\PDOMySQL([
    'dsn'=>'mysql:host=localhost', 'user'=>'user', 'password'=>'password'
]);

// Construct by passing in a connector. This allows you to share a connector instance 
// with a cache backend:
$connector = new \Cachet\Connector\PDO('sqlite::memory:');
$counter = new \Cachet\Counter\PDOSQLite($connector);

$connector = new \Cachet\Connector\PDO(['dsn'=>'mysql:host=localhost', ...]);
$counter = new \Cachet\Counter\PDOMySQL($connector);

$backend = new \Cachet\Backend\PDO($connector);

// Use a specific table name
$counter->tableName = 'my_custom_table';
$counter = new \Cachet\Counter\PDOSQLite($connector, 'my_custom_table');
$counter = new \Cachet\Counter\PDOMySQL($connector, 'my_custom_table');

为了使用,需要初始化表。不推荐在您的Web应用程序内部执行此操作 - 您应该在部署过程或应用程序设置中执行此操作

<?php
$counter->ensureTableExists();

SafeCache

  • 支持 `counterTTL`:**是**,通过 $counter->cache->dependency
  • 原子性:
  • 范围:无限制

此计数器简单地结合了一个 `Cachet\Cache`、一个锁器以及 `bcmath``gmp`,以克服其他计数器的原子性和范围限制。

它还支持任何类型的依赖。

与使用APCU或Redis后端相比,它的速度要慢得多,但比使用基于PDO的后端要快(除非,当然,您使用的缓存本身就有基于PDO的后端)。

<?php
$cache = new \Cachet\Cache('counter', $backend);
$locker = new \Cachet\Locker\Semaphore();
$counter = new \Cachet\Counter\SafeCache($cache, $locker);

// Simulate counterTTL
$cache->dependency = new \Cachet\Dependency\TTL(3600);

// Or use any dependency you like
$cache->dependency = new \Cachet\Dependency\Permanent();

扩展

后端

自定义后端很容易编写 - 简单地实现 `Cachet\Backend`。请确保您遵循以下指南

  • 后端不应该单独使用 - 它们应该由 `Cachet\Cache` 的一个实例使用

  • 必须能够使用同一个后端实例与多个 `Cachet\Cache` 实例一起使用。

  • `get()` 必须返回一个 `Cachet\Item` 实例。后端必须不检查项目是否有效,因为 `Cachet\Cache` 依赖于总是返回一个项目。

  • 请确保您至少完全实现了 `get()``set()``delete()`。其他任何内容都不是强制性的,尽管可能很有用。

  • `set()` 必须存储足够的信息,以便 `get()` 可以返回一个完全填充的 `Cachet\Item` 实例。这通常意味着如果您的后端无法直接支持PHP对象,您应该直接 `serialize()` `Cachet\Item`

您可以通过使用 `Cachet\Item->compact()`Cachet\Item::uncompact() 来减少放入后端的数据大小。这会从缓存项中删除大量冗余信息。YMMV - 我惊讶地发现使用 `Cachet\Item->compact()` 会使APCU的内存使用量增加。

依赖项

依赖项是通过实现 `Cachet\Dependency` 创建的。依赖项被序列化并存储在缓存中与值一起。当使用依赖项时,它总是传递对当前缓存的引用,并且应该注意永远不要保留对它的引用,或者任何与依赖项数据不直接相关的其他对象,因为它们也将被推入缓存,相信我 - 您不希望这样。

开发

测试

Cachet 已经过彻底测试。由于所有后端和计数器都应满足相同的接口,除了少数(希望)有良好文档记录的例外,这些类的所有功能测试用例都分别扩展自 `Cachet\Test\BackendTestCase``Cachet\Test\CounterTestCase`

在项目的根目录中提供了一个实验性的 docker-compose.yml 文件。您可以像这样针对一些预配置的测试后端运行所有测试:

./run-tests.sh

对于开发,可以通过在项目根目录中不带参数调用 `phpunit` 来运行测试。

一些 Cachet 的方面无法通过简单的单元或功能测试来证明其工作,例如锁器和计数器的原子性。这些是通过一个笨拙但可行的并发测试器进行的测试,该测试器从项目根目录运行。您可以像这样获取有关所有可用选项的帮助:

php test/concurrent.php -h

或者不带参数直接调用,使用默认设置运行所有并发测试。如果所有测试通过,它将以状态 `0` 退出,如果有任何测试失败,则退出状态为 `1`

一些测试设计为失败,但它们的ID中包含 `broken`。您可以如此排除不安全的测试:

php test/concurrent.php -x broken

我保留了这些失败的测试来演示默认行为可能违背预期的情况。我目前正在寻找一种更好的方式在测试器中表示这一点。

并发测试器已被证明在寻找 Cachet 中的海森堡错误方面非常出色。因此,在我们可以决定构建版本可以安全发布之前,应该在多种不同的负载条件下以及不同的架构上多次运行它。

许可证

Cachet 使用MIT许可证授权。有关更多信息,请参阅 `LICENSE`