redis优化

最近在做S线的业务中,需要计算用户的排名以及很多杂项数据。由于数据量过多,为了保证系统响应速度和负载能力,所以在Redis中产生了缓存(基于每天)。

pipe line

介绍

Redis的pipeline(管道)功能在命令行中没有,但redis是支持pipeline的,而且在各个语言版的client中都有相应的实现。 由于网络开销延迟,就算redis server端有很强的处理能力,也会由于收到的client消息少,而造成吞吐量小。当client 使用pipelining 发送命令时,redis server必须将部分请求放到队列中(使用内存),执行完毕后一次性发送结果;如果发送的命令很多的话,建议对返回的结果加标签,当然这也会增加使用的内存。

Pipeline在某些场景下非常有用,比如有多个command需要被“及时的”提交,而且他们对相应结果没有互相依赖,而且对结果响应也无需立即获得,那么pipeline就可以充当这种“批处理”的工具;而且在一定程度上,可以较大的提升性能,性能提升的原因主要是TCP链接中较少了“交互往返”的时间。

语法(php)

$redis = new Redis();
$redis->connect('host','port');
$redis->pipeline();
//do any more 
$redis->exec();
$redis-close();

redis lua

介绍

Redis内置了对LUA脚本的支持,并且在计算过程中保证了脚本中执行的原子性。因此在开发过程中对Redis对Lua的支持进行了学习。从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用EVAL命令对 Lua 脚本进行求值。以下将Redis对LUA的支持进行总结。

官网文档上有这样一段话(官方文档):

A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.

由此可以看出,官方还是支持大家尽量使用lua script来代替transaction的。

关于更多设计细节,可以参考Redis 设计与实现

lua语法(php)

$redis = new Redis();
$redis->connect('host','port');

$script = "
local result ={}
for i = 1,#(KEYS) do
   result[i]= redis.call('get',KEYS[i])  //do any more
end
return result
";

$result  = $redis->eval($script, $keys, count($keys));

性能测试代码

基础测试(get string key)

$host = '';
$port = '';
$keys = ['test1','test2','test3','test4','test5','tese6','test7'];
$redis = new Redis();
$redis->connect($host, $port);
foreach($keys as $key) {
	$random = rand(1,100);
	$value = '';
	for ($i=0; $i < $random; $i++) { 
		$value .= $key;
	}
	$redis->set($key, $value);
}
$redis->close();

//定义的lua脚本
$script = "local result ={} 
for i = 1,#(KEYS) do 
   result[i]= redis.call('get',KEYS[i]) 
end 
return result";

for($i =0 ; $i < 100; $i ++ ) {
	$start = microtime(true);

	$redis = new Redis();
	$redis->connect($host, $port);
	foreach($keys as $key) {
	        $redis->get($key);
	}
	$redis->close();
	$foreachTime[$i] = ( microtime(true) - $start)*1000;
	file_put_contents('redis_time.txt','foreach:'.$foreachTime[$i], 8);
	file_put_contents('redis_time.txt', "\n", 8);


	$start = microtime(true);
	$redis = new Redis();
	$redis->connect($host, $port);
	$redis->pipeline();
	foreach($keys as $key) {
		$redis->get($key);
	}
	$redis->exec();
	$redis->close();
	$pipeTime[$i] = ( microtime(true) - $start)*1000;
	file_put_contents('redis_time.txt','pipeline:'.$pipeTime[$i], 8);
	file_put_contents('redis_time.txt', "\n", 8);


	$start = microtime(true);
	$redis = new Redis();
	$redis->connect($host, $port);
	$redis->eval($script, $keys, count($keys));
	$redis->close();
	$evalTime[$i] = ( microtime(true) - $start)*1000;
	file_put_contents('redis_time.txt','eval:'.$evalTime[$i], 8);
	file_put_contents('redis_time.txt', "\n", 8);
	echo $foreachTime[$i];
	echo PHP_EOL;
	echo $pipeTime[$i];
	echo PHP_EOL;
	echo $evalTime[$i];
	echo PHP_EOL;
}

file_put_contents('redis_time.txt', "\n", 8);
file_put_contents('redis_time.txt', "\n", 8);
file_put_contents('redis_time.txt', "\n", 8);

file_put_contents('redis_time.txt','foreach avg: '.( array_sum($foreachTime)/100), 8);
file_put_contents('redis_time.txt', "\n", 8);
file_put_contents('redis_time.txt','pipe avg: '.( array_sum($pipeTime)/100), 8);
file_put_contents('redis_time.txt', "\n", 8);
file_put_contents('redis_time.txt','eval avg: '.( array_sum($evalTime)/100), 8);
file_put_contents('redis_time.txt', "\n", 8);

测试结果(ms)

foreach avg: 191.03681325912
pipe avg: 53.837163448334
eval avg: 54.453134536743

从结果来看,lua和pipeline的性能提升差不多,但是相对而言,lua更加灵活(可写简单业务)。 同样,由于redis lua和 redis pipeline的耗时性,由于redis原子性的要求,导致同一时间只能执行一个命令,因此,单个pipeline/lua不建议太大,导致系统被占用,从而引起其他服务无法正常进行。

包含逻辑测试(假定需求,多个用户排行榜)

构建数据

$host = '';
$port = '';
$redis = new Redis();

$redis->connect($host, $port);

$rankTypes = [1,3,4,5,6,9,14];
for($i =  0; $i< 1000; $i++){
	$rand = array_rand($rankTypes);
	
	$key = 'user:test:'.$i;
	$redis->hset($key,'rank_type',$rankTypes[$rand]);
	$redis->hset($key,'rank',$i);
	$redis->hset($key,'score', $i);
	$redis->hset($key,'blog_read_score', $i);
	$redis->hset($key,'blog_read_rank', $i);
	$redis->hset($key,'interact_score', $i);
	$redis->hset($key,'interact_rank', $i);
	$redis->hset($key,'close_score',$i);
	$redis->hset($key,'close_rank',$i);
	$redis->hset($key,'mention_score',$i);
	$redis->hset($key,'mention_rank',$i);

	
	$key = 'rank_key:'.$rankTypes[$rand];
	$redis->lpush($key, $i);
}

普通查询

$key_format = 'rank_key:%d';
$redis = new Redis();
$redis->connect($host,$port);
$user = [];
$start = microtime(true);
foreach ($rankTypes as $type) {
	$key = sprintf($key_format, $type);
	$top50 = $redis->lrange($key, 0, 50);
	foreach ($top50 as $uid) {
		$key = 'user:test:'.$uid;
		$user[$type][$uid] = $redis->hGetAll($key);
	}
}
$redis->close();
echo ( microtime(true) - $start)*1000; //耗时2S~3S+
echo PHP_EOL;

pipeline查询(需要两条,因为需求限定需要上次的查询结果)

$key_format = 'rank_key:%d';
$start = microtime(true);
$redis = new Redis();
$redis->connect($host,$port);
$redis->pipeline();
foreach ($rankTypes as $type) {
	$key = sprintf($key_format, $type);
	$redis->lrange($key, 0, 49);
}
$top50 = $redis->exec();
$redis->pipeline();
foreach ($rankTypes as $key=>$type) {
	foreach ($top50[$key] as $uid) {
		$key = 'user:test:'.$uid;
		$redis->hGetAll($key);
	}
}
$users = $redis->exec();
$redis->close();
$result = [];
$i = 0;
foreach ($users as $key=>$user) {
	if($key != 0 && $key%50 == 0) {
		$i++;
		continue;
	}
	$result[$rankTypes[$i]][] = $user;
	# code...
}
echo ( microtime(true) - $start)*1000;  //耗时100ms~400ms
echo PHP_EOL;

lua脚本查询

$script = "local users = {}
for i = 1,#(KEYS) do 
   local key = 'rank_key:'..KEYS[i]
   users[i] = redis.call('lrange',key,0,49)
end
local result ={}
for j = 1,#(KEYS) do
	local user_table = {}
	for k=1,50,1 do
	    local user_key = 'user:test:'..users[j][k]
		user_table[k] = redis.call('hgetall', user_key)
	end 
	result[j] = user_table
end
return result";
$start = microtime(true);
$redis = new Redis();
$redis->connect($host,$port);
$result = $redis->eval($script, $rankTypes, count($rankTypes));
$redis->close();
echo ( microtime(true) - $start)*1000;  //耗时100ms~200ms
echo PHP_EOL;