inopx / noslamcache
PHP缓存库,具有进程同步功能,以防止缓存攻击和性能下降。
Requires
- php: >=5.6
This package is not auto-updated.
Last update: 2024-09-19 17:14:11 UTC
README
缓存攻击是许多开发者不知道的问题,但它使得大多数缓存系统相当无效,无论缓存存储方法:文件、memcached、数据库等。
问题在于缺乏进程同步,而不是存储方法。
以下是一个线程竞态和缓存攻击的示例
假设我们需要缓存非常消耗资源的操作,整体需要几秒钟来完成,这在繁忙的互联网系统中是很长的时间。
在这种情况下,每秒有少量或更多的HTTP请求需要从缓存中获取此类资源,以下是资源未缓存或已过期时会发生什么:
-
第一个进程/线程无法从缓存中读取资源,然后开始创建资源,这可能需要几秒钟和大量的服务器资源:处理器/内存/IO。
-
与此同时,当第一个进程正在创建资源时,其他进程/线程正在尝试读取缓存,失败,并执行与进程/线程1相同的工作,因为没有在大多数PHP可用的缓存系统中内置同步。
-
性能下降,一切变慢,并且由于并发线程的数量和工作负载,它会变得更加严重。这意味着您的网站用户体验会下降。网络上有关页面加载时间有许多测量测试和意见,但许多研究表明,当页面加载时间超过200毫秒时,它开始让访客烦恼。页面加载时间超过十几秒对于您的网站上的普通访客来说是不可以接受的。
-
这会持续到最后一个任务完成。当这个时间高于缓存项的过期时间时,您就处于严重麻烦之中。
这被称为缓存攻击,这是错误的!
在给定键创建资源时,应该只有一个进程,而其他进程应该等待,直到第一个进程完成工作。
之后,睡眠的进程应该被唤醒,并从缓存中读取新创建的资源。
您可能直到您的网站流量低时才看不到问题,但一旦您开始取得成功,您的网站知名度和流量增加,那么网站流量和需要缓存资源的并发进程/线程的数量可能会引起缓存攻击和性能下降等问题。
要求
为了使用默认的No Slam Cache同步方法,您需要安装PECL Sync扩展:https://pecl.php.net/package/sync
相反,您可以使用PostgreSQL行级锁定机制进行同步,请参阅下面的“使用PostgreSQL进行同步”。
解决缓存攻击和基本No Slam Cache使用方法
No Slam Cache包为缓存攻击问题提供了解决方案,它使用PECL Sync包和SyncReaderWriter类提供进程同步:https://php.ac.cn/manual/en/class.syncreaderwriter.php,或通过PostgreSQL行级锁定。
默认情况下,它使用PECL Sync。
这是一个多读者、单写者的同步模型。
使用No Slam Cache需要与通常的方法不同来创建资源。
典型的缓存系统方法
$item = $cache->getItem($key);
if (!$item) {
//////////////////////////
// 创建项目,可能需要很长时间,
// 并且它可能由许多线程并发执行
将项目分配给 $item = $db->executeSQL();
将项目放入缓存 $cache->put($item);
}
现在使用 php-no-slam-cache 方法
$recipeForCreateItem = function() use($sql, $db) {
// 创建项目,每次只由一个线程执行,其他线程将让步并等待
返回 $db->executeSQL( $sql );
};
$cacheMethod->get($group, $key, $lifetimeInSeconds, $recipeForCreateItem);
在您的代码中不检查资源是否存在于缓存中。
使用匿名函数(https://php.ac.cn/manual/en/functions.anonymous.php)可能一开始看起来困难或复杂,但实际上它比常规方法简单得多、优雅。
如果资源存在于缓存中,则不会执行闭包,而是使用 READ LOCK 返回缓存的资源。
如果资源不存在于缓存中,则回调函数在同步块中使用 WRITE BLOCK 执行,并且一次只执行一个给定组和键的回调函数。
回调函数不带任何参数调用,并应返回指定用于存储的值。如果它返回 NULL,则不会存储任何内容,并且缓存方法对象也将返回 NULL。
$group 参数是缓存组 - 想象成 SQL 表的名称,而 $key 是缓存键,想象成表中的行唯一 ID。
$group 和 $key 的配对必须是唯一的,但 $key 值可以在不同的组中重复。
$lifetimeInSeconds - 解释自明
$createCallback - 它是无参数的回调函数,当资源过期或不存在于缓存中时将创建资源。
读取/写入缓存整个过程是同步的,也就是说
对于给定的组和键,只有一个进程会写入缓存,其他进程将等待并获取最近创建的资源,而不是猛击缓存.
当资源存在于缓存中且未过期时,它可以由多个 PHP 进程同时读取。
使用回调方法和缓存方法文件的示例
$group = 'products';
$key = 150;
$lifetimeInSeconds = 30;
$name = 'anonymous';
$randomNumber = rand(1, 10000);
$createCallback = function() use($name, $randomNumber) { return 'Hi '.$name.', I was created at '.date('Y-m-d H:i:s').' the random number is: '.randomNumber; }
$dir = __DIR__.'/inopx_cache';
if (!file_exists($dir)) {
mkdir($dir, 0775);
}
$cache = new \inopx\cache\CacheMethodFile($dir);
echo 'Cached value = '.$cache->get($group, $key, $lifetimeInSeconds, $createCallback);
启动 / 引导
将 classloader.php 包含到您的引导文件/程序中,以加载包类。
类加载器是可选的,因为类名和目录位置与 PSR-4 兼容。
您还可以使用 composer 安装:https://packagist.org.cn/packages/inopx/noslamcache
$ composer require inopx/noslamcache
缓存方法通用
每个缓存方法都实现了接口 \inopx\cache\InterfaceCacheMethod,并在构造函数中包含 $syncTimeoutSeconds 变量,默认值为 30。
接口 \inopx\cache\InterfaceCacheMethod 包含前面描述的主要获取方法以及其他一些方法,请参阅 API 文档(压缩在 doc-api.zip 中)以获取更多详细信息。
$syncTimeoutSeconds 是锁超时值,即如果进程等待时间超过 $syncTimeoutSeconds 秒,则无法获取锁,然后,而不是抛出错误,它将使用回调创建资源并将其写入缓存。
因此,当创建资源的工作可能需要更长的时间来完成时,非常重要地覆盖此值。
缓存方法 Memcached
类 \inopx\cache\CacheMethodMemcached 是一个 Memcached 存储方法类,具有构造函数
__construct( $memcachedHost = '127.0.0.1', $memcachedPort = 11211, $syncTimeoutSeconds = 30, $inputOutputTransformer = null, $synchroCallback = null )
构造函数参数基本自解释。请查看API文档中的$synchroCallback参数以及本README中的“使用PostgreSQL进行同步”部分。
缓存方法PDO
类\inopx\cache\CacheMethodPDO是一个数据库存储方法类,具有构造函数
__construct( PDO $PDOConnection, integer $sqlDialect = null, integer $syncTimeoutSeconds = 30, $inputOutputTransformer = null, $synchroCallback = null )
其中$PDOConnection是数据库(PDO类)的已建立连接,而$sqlDialect是此类支持的两种方言之一: \inopx\cache\CacheMethodPDO::SQL_DIALECT_MYSQL或\inopx\cache\CacheMethodPDO::SQL_DIALECT_POSTGRESQL。
在使用此缓存方法之前,您必须通过执行方法createSQLTable来创建数据库表。
表名和列名可以通过修改类变量如:$SQLTableName、$SQLColumnGroupName、$SQLColumnKeyName等进行配置 - 更多信息请查看API文档。
请查看API文档中的$synchroCallback参数以及本README中的“使用PostgreSQL进行同步”部分。
缓存方法文件
类\inopx\cache\CacheMethodFile是一个文件存储方法类,具有构造函数
__construct( string $baseDir = 'inopx_cache', integer $syncTimeoutSeconds = 30, $inputOutputTransformer = null, $synchroCallback = null )
请查看API文档中的$synchroCallback参数以及本README中的“使用PostgreSQL进行同步”部分。
其中$baseDir是缓存文件的基准目录,不带尾部分隔符。基准目录必须存在并且可写。
对于每个组,基准目录中都将有一个名为该组的单独子目录,但首先会进行清理以正确地作为文件系统目录名。
如果键不是数字,则通过crc32函数将其转换为数字,基于该数字,如果数字超过100,则创建特殊的子目录结构。
这是为了确保每个组目录的每个子目录中不超过100个文件和10个子目录。
请查看类\inopx\io\IOTool和其方法getClusteredDir
在某些文件系统中,一个目录中文件和/或子目录数量过多可能导致磁盘搜索时间过长,从而减慢I/O速度。“目录聚类”可以防止这种情况发生。
但仍然存在潜在问题,即基准目录中有大量组和子目录。
使用此缓存方法时,请注意组名和键名中的特殊字符,因为组和键将分别作为子目录名和包含缓存值的文件名。这些值首先会进行清理,这可能导致只有特殊字符不同的情况下出现两个相似的键冲突。
虚拟缓存
类\inopx\cache\CacheMethodDummy用于模拟缓存,当您不想使用缓存但方便提供缓存方法对象时。
键名前缀
每个缓存方法都通过方法setCacheKeyPrefix提供设置键前缀的方式,并通过方法getCacheKeyPrefix获取。当您在多个使用相同memcached服务器或数据库存储缓存项的应用程序中使用无碰撞缓存时,提供前缀是避免键名冲突的一种方法。
死锁问题
在使用任何类型的进程同步时,可能会出现死锁问题。
这种情况发生在
- 进程编号1获得锁A
- 然后进程编号2获得锁B
- 然后进程编号1试图获得锁B,而进程编号2试图获得锁A
这导致永远不会结束或锁超时错误的情况,称为死锁。
如果您需要更详细的解释,请上网搜索,例如:https://en.wikipedia.org/wiki/Deadlock
避免死锁的最佳方案是永远不要使用嵌套锁,也就是说:在获取第一个锁后,不要获取任何其他锁,直到解锁第一个锁。这是锁定的智能使用,可以保证没有死锁。
关于无锁缓存,这意味着你绝不应该在创建资源的回调函数内部进行任何同步,特别是使用缓存。
这样的回调是错误的
$createCallback = function() use($myVar)
{
$value = 'My Value';
$cache = new \inopx\cache\CacheMethodFile('inopx_cache');
$value2 = $cache->get('mygroup',123,30, function() { return uniqid(''); });
return $value . ' ' . $value2;
}
...因为 $value2 是在回调中使用缓存创建的,它是嵌套锁,如果其他进程以相反的顺序锁定,使用相同的组和键,有可能发生死锁。
使用 PostgreSQL 进行同步
如果您想使用 PostgreSQL 进行同步,您需要做以下事情
- 在 SynchroPostgresql 类文件中重新填充 $dbUser 和 $dbPass 变量
- 创建用于锁定的专用 PostgreSQL 数据库
- 使用 SynchroPostgresql 类中的 createSyncTable() 方法创建用于锁定的 PostgreSQL 表
- 创建将返回 PostgreSQL 同步的回调,并在构建缓存方法时使用它
示例
$synchro = function ($lockKey, $lockTimeoutMilliseconds) {
return new \inopx\cache\SynchroPostgresql($lockKey, $lockTimeoutMilliseconds);
};
$cache = \inopx\cache\CacheMethodFile('inopx-cache', 30, null, $synchro);
请注意,使用 PostgreSQL 进行同步比使用 PECL Sync 慢得多,因为它基于 SQL 事务。
测试脚本
您可以在 No Slam Cache 包的主目录中找到 cli-test-cache.php 脚本。
它旨在在 CLI 模式下运行,用于测试缓存方法的一般目的,以及测试并发性。
打开命令行窗口/ Linux 终端,并输入
php cli-test-cache.php
将显示有关可用命令的帮助信息。
您应该打开几个命令行窗口,在每个窗口中放入所需的命令,并测试并发性。
测试脚本最初就是为此配置的,因为它在回调函数中睡眠 10 秒,以便您有足够的时间在其余打开的窗口中执行相同的脚本并观察结果。