shabbyrobe / cachet
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);
许多“假值”是有效的缓存值,例如 `null
和
false
`. 查看值是否实际找到
<?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`
。