Laravel缓存MaxMind Reader实例报Attempt to read from a closed MaxMind DB求助
问题根因
你遇到的报错和Reader实例无法缓存的核心原因有两个:
GeoIp2\Database\Reader内部持有mmdb数据库的文件句柄,而PHP中的资源类型(文件句柄属于资源)无法被序列化存储,存入Laravel缓存时会直接丢失资源引用,取出的实例根本无法正常读取数据库- 即使不存在序列化问题,你在第一次调用完查询后执行了
$reader->close(),直接关闭了文件句柄,后续从缓存取出的实例对应的文件已经处于关闭状态,自然会抛出Attempt to read from a closed MaxMind DB异常
Reader实例本身就不支持序列化后跨请求缓存,这条路走不通,不要尝试继续用这个方案
可行的性能优化方案
根据你的运行环境可以选择对应的优化方案:
方案1:同请求内复用Reader(适合所有环境,基础优化)
利用Laravel服务容器绑定单例,同一个请求内如果多次调用IP查询逻辑,只会加载一次mmdb文件,不需要重复实例化Reader
- 在
app/Providers/AppServiceProvider.php的register方法中添加绑定逻辑:
public function register() { $this->app->singleton(\GeoIp2\Database\Reader::class, function () { return new \GeoIp2\Database\Reader(storage_path('/app/ipToCountry/GeoLite2-City-v2.mmdb')); }); }
- 控制器中直接注入Reader实例使用,不要手动调用close方法,请求结束后PHP会自动释放资源:
public function check(Request $request, \GeoIp2\Database\Reader $reader) { return $reader->city($request->ip()); }
方案2:缓存IP查询结果(适合FPM环境,优化效果明显)
不要缓存Reader实例,转而缓存每个IP对应的查询结果,相同IP在缓存有效期内不需要重复读取mmdb文件:
public function check(Request $request) { $ip = $request->ip(); // 可根据业务需要调整缓存有效期,这里示例缓存24小时 return \Cache::remember('geo_ip_result_' . $ip, now()->addDay(), function () use ($ip) { $reader = new \GeoIp2\Database\Reader(storage_path('/app/ipToCountry/GeoLite2-City-v2.mmdb')); $result = $reader->city($ip); $reader->close(); return $result; }); }
这个方案可以过滤掉绝大多数重复IP的查询请求,实际运行中mmdb文件的加载次数会下降90%以上,性能提升非常明显。
方案3:常驻内存环境复用Reader(性能最优,适合高并发场景)
如果你的业务请求量极高,建议切换到Swoole/Workerman等PHP常驻内存运行环境,Reader单例实例会一直驻留在内存中,跨请求不需要重复加载mmdb文件,完全消除70MB文件的加载开销,性能可以提升数十倍。
使用这种方案同样通过服务容器绑定单例即可,不需要额外修改业务代码,且不要手动调用close方法。
内容的提问来源于stack exchange,提问作者Dani Banai
相关产品推荐
相关产品推荐

