PHP-Memcache存储2万条小字符串内存溢出问题求助
先聊聊你的问题:理论上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是纯内存数据库),适合并发量不高的场景。
数据库缓存实现思路
- 建表结构:
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;
- 读取逻辑:先查询
cache_key,如果存在且expires_at > UNIX_TIMESTAMP(),返回数据;否则调用API,写入数据库。 - 清理过期数据:可以用定时任务(比如crontab)每天执行一次删除:
DELETE FROM cache_data WHERE expires_at < UNIX_TIMESTAMP();
但要注意:高并发下数据库的读写性能会成为瓶颈,需要做好索引优化,甚至分表分库,所以优先还是优化Memcache的方案更划算。
内容的提问来源于stack exchange,提问作者Martin Perry

