起因
最近排查服务器,发现有一个后台进程反复报错“unserialize(): Error at offset 0 of 2 bytes
”,根据日志记录,发现问题是因为读取缓存内容不正确导致的,其触发时机为:
- tp框架开启了字段缓存,就是config\database.php中的fields_cache选项,开启该选项可以让tp框架在模型等操作时,不再需要从数据库获取表字段定义信息,而是直接从缓存中获取
- cli模式下运行时,没有定期重置容器中的db对象
- 长时间运行,cache对象中的句柄,实际已被服务端关闭,导致客户端每次获取数据时候,都获取到的是“
:0
”字符,导致后续反序列化失败。
ThinkPHP内部机制
- tp自从5.1开始,已经全部转向容器机制,其核心组件都是依赖于容器进行创建,具体的可以查看
vendor\topthink\framework\src\think\App.php
中的$bind变量。
- db实例会在应用执行数据库相关操作时被创建,容器会根据标识对应的类去创建实例,创建前会检测该类是否存在__make的静态方法,若存在,则调用该方法来创建实例,参考
vendor\topthink\framework\src\think\Container.php
中的invokeClass
函数 - db对象的__make方法在执行的时候,会利用ThinkPHP提供的依赖注入能力,为自己内部的成员注入Event、Config、Log、Cache几个对象。即在db实例创建的时刻,框架会通过容器获取cache对应实例,让db保存在内部,可参考
vendor\topthink\framework\src\think\Db.php
中的__make
函数 - 当db执行sql前,创建数据库连接对象时,会将自身保存的cache对象赋值给connection对象,方便其在内部获取表字段信息时可以从cache中获取,可参考
vendor\topthink\think-orm\src\DbManager.php
中的createConnection
函数 - 最终,在connection对象需要获取数据表的schema信息时,会根据database配置文件中的
fields_cache
来决定是从缓存中获取字段信息还是从数据库中获取,参考vendor\topthink\think-orm\src\db\PDOConnection.php
中的getSchemaInfo
分析
- cli模式因为其要长时间运行,所有在这个模式下的长连接,都需要开启心跳机制来保活,避免长时间没有消息交互导致服务端主动关闭了连接。在其他的专门基于swoole的框架中,都会有连接池的存在,用于管理维护内部连接,定期发送心跳数据保活,及时清理无效连接
- cli模式下第一次运行时,db、cache都是全新创建的,其内部实际上的连接句柄都是全新创建(一般都是内部的handler成员变量),所以此事不存在任何问题
- 运行一段时间后,因为种种原因,缓存服务端可能会关闭该链接,那么此时Cache下的某个Driver内部的handler成员变量所对应的连接已经关闭,但是ThinkPHP内的相关Driver都没有能力及时获取该链接的最新状态,只能等待下一次缓存读取异常时做出应对,但很遗憾的是,一般情况下都只能抛出一些其他异常信息,无法直观有效的了解是链接出了问题
解决方案
- 最早我清楚该问题,再查阅了ThinkPHP的源码后,写下了如图的代码,用于在cli循环中定时执行:
- 这个写法解决了数据库连接被关闭问题(因为我定期重新执行connect方法,给的参数就是重新创建新连接),也解决了缓存问题,因为我直接从容器中删除了缓存标识,这样下一次需要获取缓存对象时,容器内发现没有实例了,会重新创建对象,那么对象内部的handler也就是全新创建的
- 但是当前问题存在于db对象内部提前保存了一个cache对象,导致我单纯创建数据库链接只能解决数据库连接的问题,在db执行过程中需要获取表信息时依然使用的是最早的cache对象(因为重新创建的是db下的Connection实例,而不是db自身)
- 所以此时应该将db也如cache一样,直接从容器中删除,这样之后再次调用时候,就会使用全新创建的db了,那么修改后的代码如图:
总结,cli模式下需要注意各类链接是否关闭的问题,包括但不限于数据库、缓存、MQ、ftp、tcp等使用长连接的组件,最好使用前可以检查下连通性,或者重新创建新对象。
最后,分享一个redis的连通性检测代码
文章评论