You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP-Memcache存储2万条小字符串内存溢出问题求助

优化Memcache内存溢出与API调用频繁问题的解决方案

先聊聊你的问题:理论上2万条×10KB的缓存数据只占200MB左右,但服务器却出现内存溢出,同时不用缓存又会耗尽API资源——咱们一步步拆解问题,给出可行的优化方案,也聊聊数据库替代的可能性。

一、先排查Memcached服务本身的配置问题

你遇到的内存溢出大概率不是代码的锅,而是Memcached服务的默认内存限制太小。Memcached默认只分配64MB内存,当缓存数据超过这个值时,它会用LRU(最近最少使用)算法淘汰旧数据,但如果你的请求量很大,淘汰+写入的频繁操作可能导致内存波动异常,甚至触发服务器内存告警。

解决方法很简单:修改Memcached的启动参数,调整内存配额。比如设置为256MB(足够容纳你的全部缓存数据):

memcached -m 256 -d

如果是用systemd管理的服务,编辑配置文件(比如/etc/memcached.conf),找到-m参数改成-m 256,然后重启服务。

二、代码层面的优化点

你的代码逻辑没问题,但有几个细节可以优化,既能避免内存溢出,又能减少API的重复调用:

1. 修复连接失败的分支逻辑

现在代码里如果Memcache连接失败,就直接跳过LoadData调用,会导致无数据返回。应该补充else分支,即使连接失败也正常获取数据(只是走API):

$memcache = new Memcache();
if ($memcache->connect('127.0.0.1', 11211)) {
    $data = LoadData($memcache); // 注意:如果这段代码不在类方法里,不要用$this->
} else {
    // 连接失败,直接调用API兜底
    $data = LoadData(null);
}

2. 防止缓存击穿,减少重复API调用

当多个请求同时访问同一个未缓存的键时,会同时触发DownloadDataFromAPI,导致API被瞬间打爆——这就是「缓存击穿」问题。我们可以用Memcache的add方法(只有键不存在时才写入)实现简单的锁机制:

function LoadData($memcache) { 
    $name = CreateMemcacheName(); 
    if ($memcache) { 
        $data = $memcache->get($name); 
        if ($data !== false) { 
            return $data; 
        }
        
        // 加临时锁,避免多请求同时调用API
        $lockKey = "lock_{$name}";
        // 锁的过期时间设10秒,防止请求超时导致锁一直存在
        if ($memcache->add($lockKey, 1, 0, 10)) {
            $data = DownloadDataFromAPI();
            $memcache->set($name, $data, 0, 3600);
            // 释放锁
            $memcache->delete($lockKey);
            return $data;
        } else {
            // 没拿到锁,等待后重试,最多重试3次
            for ($i = 0; $i < 3; $i++) {
                sleep(1);
                $data = $memcache->get($name);
                if ($data !== false) {
                    return $data;
                }
            }
            // 重试失败,直接调用API兜底
            $data = DownloadDataFromAPI();
            return $data;
        }
    } 
    $data = DownloadDataFromAPI(); 
    return $data; 
}

3. 过滤非法参数,避免生成垃圾缓存键

CreateMemcacheName里直接用$_GET的参数,可能被恶意用户传入极端值(比如超出经纬度范围的数值),生成大量无效缓存键,占用不必要的内存。可以加个参数校验:

function CreateMemcacheName() { 
    $lat = $_GET['lat'] ?? 0;
    $lon = $_GET['lon'] ?? 0;
    
    // 校验经纬度范围:纬度-90~90,经度-180~180
    if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
        throw new InvalidArgumentException("Invalid coordinates");
        // 或者返回默认键,比如"data_0_0"
        // return "data_0_0";
    }
    
    $latInt = intval($lat * 100); 
    $lonInt = intval($lon * 100); 
    return "data_{$latInt}_{$lonInt}"; 
}

4. 改用Memcached扩展(推荐)

Memcache扩展已经停止维护了,换成Memcached(带d后缀)扩展更稳定,支持更多功能(比如持久化连接、批量操作),内存管理也更高效。安装后修改代码:

$memcached = new Memcached();
$memcached->addServer('127.0.0.1', 11211);
// 后续get/set/add等方法调用逻辑基本一致,但稳定性更强

三、关于用数据库替代Memcache的可行性

如果实在不想用Memcache,用数据库存储带过期时间的缓存也是可行的,但性能不如Memcache(毕竟Memcache是纯内存数据库),适合并发量不高的场景。

数据库缓存实现思路

  1. 建表结构:
CREATE TABLE cache_data (
    cache_key VARCHAR(255) NOT NULL PRIMARY KEY,
    cache_data TEXT NOT NULL,
    expires_at INT UNSIGNED NOT NULL -- 过期时间戳
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  1. 读取逻辑:先查询cache_key,如果存在且expires_at > UNIX_TIMESTAMP(),返回数据;否则调用API,写入数据库。
  2. 清理过期数据:可以用定时任务(比如crontab)每天执行一次删除:
DELETE FROM cache_data WHERE expires_at < UNIX_TIMESTAMP();

但要注意:高并发下数据库的读写性能会成为瓶颈,需要做好索引优化,甚至分表分库,所以优先还是优化Memcache的方案更划算。


内容的提问来源于stack exchange,提问作者Martin Perry

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:52:43