一次基于CI的Redis性能问题定位

加入全栈技术交流群(QQ群号:254842154)

使用PHP的CI 3.0框架已经有一段时间了,它内置的类库虽然比较少,但是基本的都有,例如Cache模块,它提供了几种最常用的快速缓存的封装,包括apc、memcached和redis等。其中redis是比较常用的缓存,用于存储程序中经常用到并且不会频繁变化的数据。用法:

//加载类库
$this->load->driver('cache', array('adapter' => 'redis'));
//存字符串
$this->cache->save('key_str_hello', 'value_hello', 3600);
//存数组
$this->cache->save('key_array_hello', array('a' => 123), 3600);
//取
$data = $this->cache->get('key_hello');
//删除
$this->cache->delete('key_hello');

问题描述

在一次接口性能测试中,发现取数据时即使命中缓存,耗时也会比较长,基本在200-300毫秒左右。按常理隐约感觉缓存可能出现了问题。

问题定位

问题的关键是找出耗时的方法或代码片段。检查了跟业务相关的逻辑,没有耗时的操作。由于调用了比较多的CI库函数,假如要层层深入跟踪代码,可能会比较难定位。于是想到了xhprof,定位性能问题的利器,大家可以自行查资料如何使用。

于是在测试环境装了xhprof,并在待测试的方法中开启了xhprof的数据收集。得到了一个详细的函数调用路径及耗时,如下图:

可知,此次请求耗时341ms左右,从每个方法的耗时中不难发现,其中有一个Redis::sMembers方法耗时289ms之多,占到了将近85%时间。继续深入查看,发现Redis::get函数耗时19.4%占用了大部分时间。

sMembers是一个redis集合操作函数,用于获取指定key值的集合信息。于是马上想到了CI封装的redis类库,找到system/libraries/Cache/drivers/Cache_redis.php 文件。

经过代码阅读,发现在构造函数 __construct中有一行代码读取了_ci_redis_serialized这个key对应的值:

// Initialize the index of serialized values.
$serialized = $this->_redis->sMembers('_ci_redis_serialized');
empty($serialized) OR $this->_serialized = array_flip($serialized);

完整阅读完代码后发现,CI的redis类库在存储数据时,会区分普通字符串数据,和对象或数组数据,对于对象或数组,在存储到redis之前,会进行序列化操作,并把这个数据的key值存储到redis集合中(这个集合使用ciredis_serialized作为key);在取出数据时,首先从集合中查找对应的key是否存在,假如存在则进行反序列化操作。

也就是说,随着redis中存储的数组或对象越来越多,集合中存储的key也就越来越多。而每次redis的操作,都会触发一次取集合数据的操作。当数据量较大时,这个取集合数据的网络延时也会变得比较大。

问题解决

改写CI的redis类库,在构造函数中去掉取集合数据的操作,并通过 sIsMember 方法来判断当前的key是否为对象或数组。

总结

  • 对于一些顽疾问题,追根溯源时别忘了借助一些外部工具
  • 不要太相信框架

标签: redis, ci

→ 如果你觉得作者的文章不错,一不小心帮到了你,欢迎打赏作者几包辣条的钱💰