text-media / shm-cache
Requires
- php: >=7.0
Requires (Dev)
- ext-memcached: *
- jamm/memory: ^1.0
- phpunit/phpunit: ^7.1
- squizlabs/php_codesniffer: ^3.1
This package is auto-updated.
Last update: 2024-09-10 17:56:09 UTC
README
基于共享内存的 "键 -> 值" 类型缓存。
适用于同时运行大量脚本,且这些脚本需要处理大量静态不变数据的情况。
- 一个脚本按照计划或某个事件来预热缓存,即从某处读取数据,将其转换为所需格式并保存到内存中;
- 其他许多脚本从内存中读取数据,即不使用处理器资源进行读取和格式转换,也不需要内存来存储相同的数据。
配置
为了配置缓存,需要创建抽象类 \TextMedia\ShmCache\Behavior 的子类。在此类中,必须 强制 重写以下常量
| 常量 | 描述 |
|---|---|
PROJECT_ID | 项目标识符。它应该是一个字符(见 https://php.ac.cn/manual/ru/function.ftok.php)。 |
CACHE_SIZE | 共享内存块的大小。可以是字节数或字符串类型:1m、100k 等。必须至少为 1 KB。 |
VALUE_SIZE | 定义存储包装数据所需多少字节。可以在 1 到 4 之间变化。 |
VALUE_SIZE 应根据存储值的长度来选择。如果只是数字或短文本(不超过 255 个 ASCII 字符)- 只需指定 1 个字节。
CACHE_SIZE 应根据以下值的总和来选择
- 缓存中的记录数乘以 1 - 存储键长所需的字节数;
- 所有键的字符串表示的总长度;
- 记录数乘以
VALUE_SIZE- 存储值长度的字节数; - 所有值的字符串表示的总长度。
值的字符串表示不应以 var_export 等方式呈现,因为这些数据包含冗余信息,例如键名。
例如,在保存类似 ['x' => 数值1, 'y' => 数值2] 的数组时,只需保存二进制形式的数值,而在读取内存时将其转换为所需类型并形成所需格式的数组。为此,需要在类中重写以下方法
| 方法 | 描述 |
|---|---|
string packData(string $key, $data) | 将数据元素打包为字符串以进行记录。 |
mixed unpackData(string $key, string $packed) | 将读取自内存的数据解包回原始结构。 |
将 $key 传递给两个方法,因为它可能影响打包/解包算法。
例如,如果存储的数据具有以下字段
user_id: 数值,写入它不需要超过 2 个字节;name: 长度不超过 255 个字符的字符串;desc: 字符串。
use TextMedia\ShmCache\Behavior;
class MyCacheBehavior extends Behavior
{
public function packData(string $key, $data): string
{
return (self::packNumber($data['user_id'], 2)
. self::packNumber(strlen($data['name'], 1))
. $data['name']
. $data['desc']);
}
public function unpackData(string $key, string $packed)
{
$nameLength = self::unpackNumber(substr($packed, 2, 1));
return [
'user_id' => self::unpackNumber(substr($packed, 0, 2)),
'name' => substr($packed, 3, $nameLength),
'desc' => substr($packed, 3 + $nameLength),
];
}
}
如果 packData() 方法应简单地转换为字符串,则最好使用 parent::packData(),因为它检查值是否可以转换为字符串,并在无法转换时抛出异常。这将避免致命错误。
unpackData() 方法在解包数据时可以检查其有效性、符合某些模板,并在出错时抛出类型为 \TextMedia\ShmCache\Exception 的异常(见 "错误" 部分),该异常将被主要对象捕获,从而执行以下操作
- 将缓存状态设置为 "损坏";
- 调用
onCorrupt方法 - 默认情况下它不执行任何操作,但可以重写它; - 异常将被抛出到上层,即脚本将终止。
同样,这个类必须定义一个用于“预热”缓存所需的 getData() 方法。此方法必须返回一个 ArrayObject,以便将其作为“键-值”保存到缓存中。例如
use ArrayObject;
use TextMedia\ShmCache\Behavior;
class MyCacheBehavior extends Behavior
{
public function getData(): ArrayObject
{
return new ArrayObject($this->database->getQueryResult('SELECT user_id, name, desc FROM users', 'user_id'));
}
}
类 \TextMedia\ShmCache\Behavior 包含以下静态方法,可用于包装/解包数据
| 方法 | 描述 |
|---|---|
string packNumber(int $number, int $size) | 将数字打包成字符序列($size - 字节数)。 |
int unpackNumber(string $string) | 将字符序列解包成数字。 |
事件处理器
\TextMedia\ShmCache\Behavior 的子类可以重定义某些事件发生时的行为(所有三个方法都不是必需重定义的,默认情况下什么都不做)
| 方法 | 事件 |
|---|---|
onCorrupt | 在处理从缓存中读取的数据的过程中发生错误。 |
onEmpty | 在尝试从缓存中读取数据时发现缓存为空。 |
onWarmed | 缓存“预热”完成。 |
onIndexed | 索引表(偏移量)构建完成。 |
每个方法的第一个参数是发生事件的 \TextMedia\ShmCache\Cache 类的缓存对象。
onCorrupt 处理器的附加参数包括
| 参数 | 类型 | 描述 |
|---|---|---|
$key | string | 未能处理的数据的键。 |
$value | string | 从内存中读取的数据。 |
onWarmed 和 onIndexed 处理器额外有两个参数 $onStart 和 $onReady;两者都是具有以下字段的普通对象
| 字段 | 类型 | 描述 |
|---|---|---|
time | float | 启动/完成进程的时间(microtime(true))。 |
memory | integer | 启动/完成时的使用内存(memory_get_peak_usage(true))。 |
records | integer | 已记录/读取的记录数(在启动时 = 0)。 |
size | integer | 已记录/读取的字节数(在启动时 = 0);不包括缓存头的大小。 |
onEmpty 处理器没有额外的参数,默认情况下调用 onCorrupt 处理器(如果没有其他重定义),因为显然两种情况的行为应该是相同的 - 修复缓存内容,重新“预热”。
使用方法
要使用缓存,需要创建一个 \TextMedia\ShmCache\Cache 类的实例,并在其构造函数中传递一个 \TextMedia\ShmCache\Behavior 的子对象。
使用 warmup() 方法“预热”缓存,该方法有一个可选参数,指示是否首先完全清空内存中的所有数据,即用0填充。默认情况下,此参数的值为 TRUE。
以下方法可用于从共享内存中读取数据
| 方法 | 描述 |
|---|---|
mixed getItem(string $key) | 按单个键读取。 |
array getItems(array $keys) | 按多个键读取。输出数组为“键 --> 值”。 |
在 getItems() 方法的第二个参数中可以传递 bool $ignoreMissing(默认 = TRUE),该参数指示是否忽略索引表中缺失的键或抛出异常。如果此参数等于 TRUE 并且抛出了代码为 FAILED_SEARCH_KEY(参见“错误”部分)的异常,则值不会进入结果数组;在其他情况下,将抛出异常。
错误
所有上述类在发生错误时都会抛出类型为 \TextMedia\ShmCache\Exception 的异常。每个错误类型都分配了自己的代码 - 该类的常量
| 常量 | 值 | 错误描述 |
|---|---|---|
INVALID_PROJECT_ID | 1 | 项目标识符的值不正确。 |
FAILED_GET_SHM_ID | 2 | 无法确定共享内存块的标识符。 |
INVALID_CACHE_SIZE | 3 | 共享内存块的大小值不正确。 |
INVALID_VALUE_SIZE | 4 | 数据字符串表示的长度值不正确。 |
FAILED_OPEN_SHMOP | 5 | 无法打开共享内存块。 |
FAILED_DELETE_SHMOP | 6 | 无法删除共享内存块。 |
INVALID_STATUS_VALUE | 7 | 状态值无效。 |
FAILED_READ_SHMOP | 8 | 从共享内存读取错误。 |
FAILED_WRITE_SHMOP | 9 | 写入共享内存错误。 |
FAILED_SEARCH_KEY | 10 | 指定的键在表中不存在。 |
CACHE_NOT_READY | 11 | 缓存未准备好读取。 |
FAILED_PACK_VALUE | 12 | 无法将值转换为字符串。 |
FAILED_UNPACK_VALUE | 13 | 无法从字符串解包值。 |
调试
要启用调试模式,需要调用Cache::setDebugMode()方法并传递参数TRUE;要禁用,传递参数FALSE。
调试模式包括保存操作执行时间和使用的内存数据。以下操作将被跟踪:
| 名称 | 描述 |
|---|---|
CACHE CLEAN | 清理缓存。 |
CACHE WARMUP | 缓存预热。 |
TABLE CREATION | 创建索引表。 |
OFFSET SEARCH | 索引表搜索。 |
调试数据可以通过Cache::getDebugData()方法获得,该方法返回一个数组,其列表为包含以下字段的数组
| 字段 | 描述 |
|---|---|
action | 执行的操作(参见上表)。 |
time | 花费时间(秒,最多8位小数)。 |
memory | 最终使用的最大内存(字节)。 |
测试
cd /path/to/package
vendor/bin/phpunit
除了检查写入/读取共享内存外,测试还包括与Memcached的性能比较。
- 运行一个单独的脚本,该脚本
- 清除共享内存和
memcached; - 启动300个进程(每种类型的缓存平均分配一半);启动的进程“挂起”在内存中,等待缓存预热;
- 执行预热 - 所有类型的缓存都填充了200个相同的元素;
- 清除共享内存和
- 启动的脚本对缓存进行10000次随机元素查询;
- 所有脚本工作完成后输出
- 每个进程的平均运行时间;
- 最大值;
- 最小值;
- 缓存预热时间。
性能测试默认启用,但可以通过以下方式禁用
export PHP_UNIT='--no-performance' && vendor/bin/phpunit
如果性能测试未禁用并且“挂起”或中断,则在启动下一个测试之前,需要“杀死”先前的进程。
pkill -f "TestPerformance.php"