细说 php 高性能缓存 APCu

Faria 2023-11-29 PM 1605℃ 4条

1. APCu 的前世今生

APCu 的前身是 APC,全名 Alternative PHP Cache,即“替代PHP缓存”

它主要扮演着两个角色:

  1. 将 PHP 代码编译生成的字节码暂存在共享内存中,提供 Opcode Cache,从而加速应用的运行效率
  2. 提供用户数据缓存功能

然而,随着 PHP 5.5 版本的推出,Zend Optimizer Plus(后更名为 Opcache)成为内置的 Opcode Cache 实现,导致 APC 的主要功能逐渐失去意义。官方也在后续宣布停止对APC的维护。

APCu 的诞生则是基于对 APC 的一次演进。它专注于处理用户数据的缓存,摒弃了操作码缓存的部分,成为 APC 的轻量级替代品。APCu 在性能和资源利用上做了一些优化,使其更适合特定的应用场景。


2. APCu 的说明

官方对于 APCu 的介绍:

  1. APCu 是 PHP 版的内存键值存储。 键是 string 类型且值可以为 PHP 任何变量。 APCu 仅支持用户空间(userland)级别的变量缓存。
  2. APCu 缓存在 Windows 上是按进程的,所以当使用基于进程(而不是基于线程)的 SAPI 时,它不会在不同的进程之间共享。
  3. APCu 是去除了操作码缓存的 APC。

关于上述第 2 点展开讲讲:

APCu 缓存在 Windows 上是按进程而不是线程进行管理的。因此,在使用基于进程的 SAPI(例如,Apache 的mod_php或 PHP-FPM 中的进程池模式)时,不同进程之间的 APCu 缓存是相互隔离的,并不共享数据。

在 Linux 上是不同进程池之间的 APCu 缓存是相互隔离的,同一个进程池内的不同进程是共享的,怎么理解?

PHP-FPM 在创建进程池进程的时候会先起一个 master 主进程,这个时候如果配置了 APCu,会开辟一块内存空间给 APCu,然后再创建 worker 子进程,创建的这些 worker 子进程都共享这块内存空间,所以在主进程下的 worker 子进程之间是共享的。

和 redis、Memcached 这些能多设备集群部署的相比较,apcu 就是一个进程隔离级别的单机缓存,

但是论性能比 redis、Memcached 这种走网络请求的那是强上很多倍的


3. 安装以及使用

最简单的安装方式pecl安装

pecl install apcu

默认下载的是最新的稳定版本,如果想下载其他版本可以从 http://pecl.php.net/package/APCu 去选择

安装完之后需要在 php.ini 中加入配置

之后使用php -m | grep apcu查看是否安装成功

如果想让项目中生效需要重启php-fpm

4. php.ini 配置

官方说明:https://www.php.net/manual/zh/apcu.configuration.php

1.jpg

下面介绍几个常用的:

  • apc.enabled:启用或禁用 APCu。设置为1表示启用,设置为0表示禁用。默认值为1
  • apc.shm_segments:为编译器缓存分配的共享内存段数量。默认为1
  • apc.shm_size:共享内存大小。默认值为32M
  • apc.ttl:缓存对象的生命周期(以秒为单位)。默认值为0,表示永不过期
  • apc.enable_cli:是否启用 APCu 的 CLI 模式。1表示启用,0表示禁用。默认值为0
注意:两个独立的 cli 进程不共享,cli 下命令结束则该进程结束,缓存清除

5. 实操演练

5.1 首先下载扩展

执行pecl install apcu之后就干等着。。。

由于网速太差,直接下载失败,/尴尬

然后又学到了一种安装方式:

在官网 https://pecl.php.net 找到 apcu 扩展,找个稳定版下载到本地,然后命令安装

pecl install /tmp/apcu-5.1.23.tgz

会自动源码编译安装

5.2 安装成功之后设置 php.ini

[APCu]
extension="apcu.so"
apc.enabled=1
apc.shm_size=512M
apc.shm_segments=1
apc.ttl=0
apc.enable_cli=1

5.3 代码测试

<?php

  if(!extension_loaded('apcu')) {
      throw new Exception("Apcu 扩展未加载!", 1);
  }

  if(!apcu_enabled()) {
      throw new Exception("Apcu 未开启!", 1);
  }

  /*
      apcu_add,apcu_store 都是添加 k=>v 数据,支持数组方式存储,都支持设置过期时间(秒级)
      唯一的不同点是:对于已存在的 key:
      - apcu_add 返回 false,不做更新、
      - apcu_store 做覆盖更新
    */
  echo "\n\n------- apcu_add()|apcu_store() -------\n";
  var_dump(apcu_add("test_key", "val")); # true
  var_dump(apcu_add("test_key", "val2")); # false
  // apcu_fetch 获取 key 的值,支持数组的方式获取
  var_dump(apcu_fetch("test_key")); # val
  
  var_dump(apcu_store("test_key", "val3")); # true
  var_dump(apcu_fetch("test_key")); # val3
  
  // 数组的方式
  $arr = ['test_arr_key1' => "v1", 'test_arr_key2' => "v2"];
  var_dump(apcu_add($arr)); # []
  print_r(apcu_fetch(['test_arr_key1', 'test_arr_key2'])); # ['test_arr_key1' => "v1", 'test_arr_key2' => "v2"]
  
  // 设置过期时间
  var_dump(apcu_add("test_ttl_key1", "val", 3)); # true
  var_dump(apcu_fetch("test_ttl_key1")); # val
  sleep(4);
  var_dump(apcu_fetch("test_ttl_key1")); # false
  
  var_dump(apcu_store("test_ttl_key2", "val", 3)); # true
  var_dump(apcu_fetch("test_ttl_key2")); # val
  sleep(4);
  var_dump(apcu_fetch("test_ttl_key2")); # false
  
  // apcu_cas 用新值更新旧值,结果为布尔值,成功为 true
  // 注意:新值和旧值 必须都为 in 类型,否则会报错
  echo "\n\n------- apcu_cas() -------\n";
  apcu_store('test_cas', 2);
  var_dump(apcu_cas("test_cas", 2, 23)); # true
  var_dump(apcu_fetch("test_cas")); # 23
  
  
  // apcu_inc 自增,支持传递增加的数值
  // apcu_dec 自减,支持传递减少的数值
  echo "\n\n------- apcu_inc()|apcu_dec() -------\n";
  apcu_store("dec_inc", 10);
  
  var_dump(apcu_inc("dec_inc")); # 11
  var_dump(apcu_inc("dec_inc", 2)); # 13
  
  var_dump(apcu_dec("dec_inc")); # 12
  var_dump(apcu_dec("dec_inc", 4)); # 8
  var_dump(apcu_dec("dec_inc", 10)); # -2
  
  // 对于不存在的 key,默认初始值为 0
  var_dump(apcu_inc("dec_inc_new1")); # 1
  var_dump(apcu_dec("dec_inc_new2")); # -1
  
  
  // apcu_key_info 获取 key 的详细信息
  echo "\n\n------- apcu_key_info() -------\n";
  echo json_encode(apcu_key_info("dec_inc"));
  /* 返回
  {
      "hits": 0,
      "access_time": 1700495080,
      "mtime": 1700495080,
      "creation_time": 1700495080,
      "deletion_time": 0,
      "ttl": 0,
      "refs": 0
  }
  */
  
  // apcu_sma_info 检索 APCU 共享内存分配信息
  echo "\n\n------- apcu_sma_info() -------\n";
  echo json_encode(apcu_sma_info());
  /* 返回
  {
      "num_seg": 1,
      "seg_size": 536870768, # 共享内存大小
      "avail_mem": 536836736, # 还有多少可用
      "block_lists": [
          [
              {
                  "size": 536836704,
                  "offset": 34144
              }
          ]
      ]
  }
  */
  
  
  // 删除 key
  echo "\n\n------- apcu_delete() -------\n";
  apcu_store("test_delete1", "val");
  apcu_store("test_delete2", "val");
  var_dump(apcu_delete("test_delete1"));     # true
  // 支持数组的格式,返回删除失败的 key
  print_r(apcu_delete(["test_delete1", "test_delete2"])); # ["test_delete1"] 
  
  
  // apcu_exists 检查 key 是否存在
  echo "\n\n------- apcu_exists() -------\n";
  apcu_store("test_exists", "val");
  var_dump(apcu_exists("test_exists")); # true
  // 支持数组传参,返回的数组结果里只有存在记录的 key
  print_r(apcu_exists(["test_exists", "test_existssssss"])); # ["test_exists" => 1]
  /*
  有个小疑问:操作过录入一个 key 然后删除,也操作过设置一个有效期的 key 等过期,
  apcu_exists() 都不会显示出来,所以不太明白结果集为什么要有 test_exists 等于 1,
  直接返回 ['test_exists'] 不是更好吗
  */
  
  // apcu_entry 原子的获取记录,确保在并发情况下对缓存操作的原子性和一致性。
  echo "\n\n------- apcu_entry() -------\n";
  $entryData = apcu_entry("test_entry", function () {
      return ["name" => "test"];
  });
  print_r($entryData);
  
  
  // 清除 apcu 缓存
  echo "\n\n------- apcu_clear_cache() -------\n";
  apcu_clear_cache();
  print_r(apcu_fetch(["test_key", "test_key2"])); # []

5.4 场景案例

<?php
  // 开启APCu扩展
  if (!extension_loaded('apcu')) {
      throw new Exception("Apcu 扩展未加载!", 1);
  }
  
  // 尝试从APCu缓存中获取数据
  $data = apcu_fetch('my_data');
  
  if (!$data) {
      // 如果缓存中没有数据,则从数据库中获取数据
      $data = get_data_from_database();
  
      // 将数据转换为JSON格式,并将JSON数据存入APCu缓存中,设置过期时间为1天
      $json_data = json_encode($data);
      apcu_store('my_json_data', $json_data, 86400);
  }
  
  // 使用数据进行处理
  $data = json_decode(apcu_fetch('my_json_data'), true);
  process_data($data);
  
  // 定义一个处理数据的函数
  function process_data($data) {
      // 处理数据的代码
  }
  
  // 定义一个从数据库中获取数据的函数
  function get_data_from_database() {
      // 从数据库中获取数据的代码
  }

5.5 操作过程中的报错

错误一

执行pecl install apcu

Package "apcu" Version "5.1.23" does not have REST xml available 
install failed

原因:

网络的问题,连接 pecl 库失败导致的错误

解决方法:

直接去 http://pecl.php.net/package/APCu 官网下载到本地安装

错误二:

执行pecl install /tmp/apcu-5.1.23.tgz本地安装的时候报错

/opt/homebrew/Cellar/php@7.4/7.4.33_4/include/php/ext/pcre/php_pcre.h:25:10: fatal error: 'pcre2.h' file not found

解决方案:

首先确定是否有文件:ll /opt/homebrew/Cellar/php@7.4/7.4.28/include/php/ext/pcre/php_pcre.h

如果有则执行ln -s /opt/homebrew/include/pcre2.h /opt/homebrew/Cellar/php@7.4/7.4.28/include/php/ext/pcre/php_pcre.h

/opt/homebrew/Cellar... 这是我的 php 安装目录,不要盲目 copy

错误三

配置完 php.ini 之后报错

PHP Warning:  PHP Startup: apc.shm_segments setting ignored in MMAP mode in Unknown on line 0

原因:

php.ini 配置apcu.shm_segments=2导致的

php 官方原话:

When APCu is compiled with mmap support (Memory Mapping), it will use only one memory segment, unlike when APCu is built with SHM (SysV Shared Memory) support that uses multiple memory segments

翻译过来:

当APCu使用mmap支持(内存映射)编译时,它将只使用一个内存段,这与APCu使用使用多个内存段的SHM(SysV共享内存)支持构建时不同

也就是说在 mmap 模式下,apcu.shm_segments只能为1

什么时候是 mmap 模式呢?默认就是 mmap

解决方法:

设置 apcu.shm_segments=1


6. GUI图形界面

官方原文:

Once the server is running, the apc.php script that is bundled with the extension should be copied somewhere into the docroot and viewed with a browser as it provides a detailed analysis of the internal workings of APCu. If GD is enabled in PHP, it will even display some interesting graphs.

意思是把apc.php拷贝到项目中就能查看

apc.php获取方式:

https://github.com/krakjoe/apcu 找到apc.php并下载

图形效果:

2.png


7. 扩展阅读

对于读操作(例如apcu_fetch()),多个并发的读取操作可以同时进行,因为这些操作不会修改缓存,所以不需要互斥锁,这是保证了并发下读取的高性能

但对于写操作(例如apcu_store()apcu_delete()),会涉及到修改缓存的情况,这时会使用互斥锁来保证写操作的原子性和一致性,避免并发写导致的问题

那么问题来了:

当有一个写入操作正在更新某个键的值时(使用apcu_store()),同时有另一个请求正在尝试读取相同的键(使用apcu_fetch()),这时读取操作可能会获取到未完成的、不一致的数据

对于这种情况 APCu 给出的解决方案是**apcu_entry()**

它允许你通过传递一个回调函数来执行缓存操作,并在获取锁后才执行该操作,确保操作的原子性和一致性。这种方式允许你在读取之前获取锁,执行你的操作(比如检查、计算或更新缓存),然后在释放锁之前返回结果。这样可以确保在并发情况下,只有一个操作能够获取锁并执行,避免了并发情况下的数据不一致性问题。

对于某个热门的缓存键(key),如果该键频繁地被并发访问、修改或更新,使用 apcu_entry() 来保证操作的原子性可能会导致锁竞争,增加系统开销和CPU负载,所以要慎用,根据实际的业务场景来考量
标签: php扩展, APCu

非特殊说明,本博所有文章均为博主原创。

评论啦~



已有 4 条评论


  1. rzqhsnarbr
    rzqhsnarbr

    博主真是太厉害了!!!

    回复 2024-09-22 18:18
  2. etkxkkjkkl
    etkxkkjkkl

    叼茂SEO.bfbikes.com

    回复 2024-09-22 23:55
  3. ictlymamih
    ictlymamih

    看的我热血沸腾啊

    回复 2024-09-23 09:04
  4. nbzaaaazss
    nbzaaaazss

    怎么收藏这篇文章?

    回复 2024-09-27 12:32