PHP APCu缓存使用与避坑

APCu

  • 极简概括: PHP 的开源内存缓存扩展,类比Redis,但是一般都用Redis,所以APCu用的很少。
  • 官方文档:www.php.net/manual/zh/a...
  • 解决问题:类比Redis做缓存组件,提升性能,同步数据使用。
  • 适用场景:轻量级的缓存,适合写少读多的场景。缺少原子性、缺少多条指令无间隙执行,不建议高并发时写多读多,写多读少的场景下使用。
  • 优点:
    • 比Redis快一百多倍。
    • 运维成本低:利用PHP扩展的方式实现,无需与缓存组件进行网络通信。
    • 简单易用:APCu 提供了简单而有效的接口,容易上手。
    • 跨文件跨进程:A文件set值,B文件get值,是可以获取到值的,若做不到和变量没区别。
  • 缺点:
    • 不支持远程独立部署。
    • 类型没有Redis的多,适用场景仅限于缓存。
    • 数据无法做到常驻内存,重启或出故障,数据丢了就没了,没有像Redis的RDB或AOF持久化机制。
    • 无法保证多个操作的原子性。
    • 可能存在超卖的问题。
    • 获取确定已经存在的值可能会遇到false,获取结果不稳定。
  • 注意:从PHP 8.0.0开始,不再支持apcu bc。

是否能像Redis+Lua一样保证多个操作原子性?

不能。 Redis是单线程的,意味着单位时间内只执行一个任务,Redis让并发过来的任务强制串行执行。有Lua的加持,保证多条指令无间隙执行。 APCu没有这个机制,让操作要么都成功,要么都失败,要实现这一步需要手动写逻辑。

高并发下会有超卖的一致性问题吗?

还好APCu有乐观锁机制,可以防止超卖问题。

但APCu 没有互斥的锁机制,互斥意味着并发过来的请求,通过独占该资源,让任务串行执行。

由于PHP+Nginx默认是多进程机制,(也可以调整为多线程,用得少) 假设一个场景: 获取到值,自增。进程P0获取到A的值为5的时候,想要自增到6,可能其它进程已经自增到8了。此时两步操作存在间隙,又没有机制对此数据加锁防止被其它进程更改,所以可能P0执行自增时会加到9,这是个概率问题,因此乐观锁的机制就显得非常重要。

安装

sh 复制代码
前提是安装好了PHP,默认在/usr/local/php下,并配置有/usr/local/php/bin目录的环境变量
cd /test
wget https://pecl.php.net/get/apcu-5.1.23.tgz
tar zxf apcu-5.1.23.tgz
phpize
./configure
make
make install

相关配置(php.ini)

配置名 值类型 默认值 说明
apc.enabled int 1 设置为0以禁用APC。这在APC被静态编译到PHP中时非常有用,因为没有其他方法可以禁用它。
apc.shm_segments int 1 为编译器缓存分配的共享内存段的数量。如果APC共享内存不足,但apc.shm_size设置为系统允许的最高值,提高该值可能会防止APC耗尽其内存。
apc.shm_size int 32M 每个共享内存段的大小
apc.entries_hint int 4096 是用于设置APCu缓存的条目预期数量
apc.ttl int 0 指定缓存中的条目在过期之前可以存在多长时间,单位为秒。默认为0,表示永不过期
apc.gc_ttl int 3600 指定过期缓存条目被清理的时间间隔,单位为秒
apc.mmap_file_mask string null 是用于配置在使用共享内存映射(MMAP)方式时的文件名模板。这个选项在某些情况下可以用于解决操作系统限制或者提高性能。默认情况下,这个选项为空,APCu会使用系统默认的文件名模板。设置apc.mmap_file_mask时,你可以使用一些特殊的占位符来指定文件名的格式,例如%s代表共享内存标识符的十六进制表示,%p代表当前进程的PID(进程标识符)。这样可以确保每个进程使用不同的文件名,避免冲突。一般情况下,你不需要手动设置这个选项,除非你遇到共享内存映射方面的特定问题或者有特殊需求。在大多数情况下,使用默认设置即可满足需求
apc.slam_defense int 1 防止缓存雪崩,多进程下,每个进程都试图同时缓存同一个文件。此选项设置跳过尝试缓存未缓存文件的进程的百分比。或者将其视为单个进程跳过缓存的概率。例如,设置为75意味着该进程有75%的概率不会缓存未缓存的文件。因此,设置越高,对缓存雪崩的防御就越强。将此项设置为0禁用此功能
apc.enable_cli int 0 是否在cli模式下启用apc,实测不生效
apc.use_request_time int 0 置控制是否APC应该使用请求时间来为文件加上时间戳。当启用时,它可以确保在请求时间变化时刷新缓存文件,这在某些情况下会很有用,比如在开发或调试代码时
apc.serializer string php 用于配置APC序列化方式。
apc.coredump_unmap int 0 启用APC处理信号,如SIGSEGV,该信号在收到信号时写入内核文件。当收到这些信号时,APC将尝试取消共享内存段的映射,以便将其从核心文件中排除。当接收到致命信号并且配置了大型APC共享内存段时,此设置可以提高系统稳定性
apc.preload_path string null 用于指定要预加载的PHP文件或目录的路径。预加载可以提高应用程序的性能,因为它可以在应用程序启动时将指定的文件或目录加载到内存中,从而减少了每次请求时的文件读取和解析时间。

使用

php 复制代码
设置值,注意,缓存有值的情况下无法设置值,类比Redis的setnx,类型支持标量、数组、与对象,这一点非常好。
bool  apcu_add(key, val, ttl);

获取缓存,获取不到返回false,并发情况下容易返回false
mixed apcu_fetch(key);

乐观锁机制,在旧值的基础上添加新的值
bool apcu_cas(key, int_old, int_new):

清除所有缓存
bool apcu_clear_cache()

递减,参数2支持负数
int apcu_dec(key, 递减值, 函数返回结果赋值给变量, ttl秒)

从缓存中删除某个元素
bool|array apcu_delete(array|string key)

判断当前环境能否使用apcu
bool apcu_enabled()

若key不存在,则调用callback,并带有一个默认参数,即key的值
null apcu_entry(key, callback, ttl)

判断多个key或者单个key是否存在。当参数为array时,函数返回只存在的key组成的数组
bool|array apcu_exists(string|array key)

获取某个key的值,若参数1是数组,那么结果也是个数组,只会返回存在的key的值,若key有值参数2为true,否则反之。
bool|array apcu_fetch(array|string key, $var);

递增,参数2支持负数
int apcu_inc(key, 递增值, 函数返回结果赋值给变量, ttl秒)

将key的值存储缓存,类比Redis的set,若已存在,可直接替换,参数1也可以传输数组。
bool apcu_store(array|string key, val, ttl)

压测,对比连接Redis性能

方式 轮次 APCu耗时(秒) Redis耗时(秒)
只读 10000 0.011 1.162
只写 10000 0.012 1.062
读写,一次new Redis 10000 0.011 2.117
读写,多次new Redis 10000 0.011 3.646
php 复制代码
只读(APCu):
<?php

$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
    $key = 'apcu'. $i;
    apcu_fetch($key);
}

echo microtime(true) - $start;


只读(Redis):
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
    $key = 'redis' . $i;
    $redis->get($key);
}

echo microtime(true) - $start;


只写(APCu):
<?php

$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
    $key = 'apcu'. $i;
    apcu_add($key, $i);
}

echo microtime(true) - $start;


只写(Redis):
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
    $key = 'redis' . $i;
    $redis->set($key, $i);
}

echo microtime(true) - $start;


读写,一次new Redis(APCu):
<?php

$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
    $key = 'apcu'. $i;
    apcu_add($key, $i);
    apcu_fetch($key);
}

echo microtime(true) - $start;


读写,一次new Redis(Redis):
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
    $key = 'redis' . $i;
    $redis->set($key, $i);
    $redis->get($key);
}

echo microtime(true) - $start;


读写,多次new Redis(APCu):
<?php

$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
    $key = 'apcu'. $i;
    apcu_add($key, $i);
    apcu_fetch($key);
}

echo microtime(true) - $start;


读写,多次new Redis(Redis):
<?php
$start = microtime(true);
for($i = 0; $i < 10000; $i++) {
	$redis = new Redis();
	$redis->connect('127.0.0.1', 6379);
    $key = 'redis' . $i;
    $redis->set($key, $i);
    $redis->get($key);
}

高并发下对APCu原子性测试

压测工具用ApiPOST,我认为比ab工具好用。 压测前,为了保证ApiPOST压测参数(压测轮次 * 并发数 结果积)的准确性,特地用Redis做了多次测试,发现参数是对的,并发数大了就不对(150以上),这意味着压测工具应该没问题,只是设备线程数不够。

php 复制代码
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->decr('test_key');

多条APCu语句执行能才能测试更能充分原子性,并发100,测试10轮次,也就是1000次请求,但是多次压测下来,结果不对。apcu_fetch获取值动不动就是false,导致结果重新赋值为10000(在不存在的情况下赋初始值),有并发问题,但不是因为并发引起的,而是因为apcu_fetch函数的问题,获取不到值返回false。

php 复制代码
<?php
$key = 'test_key';

$res = apcu_fetch($key);
if($res === false) {
    apcu_add($key, 10000);
} else {
    apcu_delete($key);
    apcu_add($key, $res - 1);
}

echo apcu_fetch($key);
相关推荐
BingoGo1 小时前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack1 小时前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
用户30745969820720 小时前
PHP 扩展——从入门到理解
php
鹏仔先生2 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php
云水一下2 天前
从零开始学 PHP 系列(一):PHP 的前世今生与开发环境搭建
开发语言·php
xingpanvip2 天前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
酉鬼女又兒2 天前
零基础入门计算机网络运输层:端到端通信核心作用、端口号分类规则、复用分用工作机制及UDP与TCP协议全方位对比详解
网络·网络协议·tcp/ip·计算机网络·考研·udp·php
dog2502 天前
不要再继续优化 TCP
网络协议·tcp/ip·php
Channing Lewis2 天前
PHP 解析 Excel 的那些坑:一次“行号错位”引发的数据丢失
开发语言·php·excel
Cheng小攸2 天前
渗透行为分析与检测
开发语言·php