今天开始学习Redis~从SQL过渡到Redis~
这篇文章是KLab Advent Calendar 2015的第22天的文章。
你好。作为KLab公司,这是我们很久以来的Advent Calendar参战。排名第22位也有些紧张呢。
我是在大阪办公室编写服务器应用程序的小狗。喜欢创建管理工具和运维支持工具。
首先
本文将介绍使用Redis对服务器应用进行加速和调优。虽然主要是针对游戏编写的,但我认为它也适用于其他Web应用。
Redis是什么?
我放弃了。
如果您能看看官方的Redis,大部分情况应该都能明白。
需要什么样的调音?
不会讲这么复杂的话。请将其视为实例提示。
文章中的示例代码是用PHP编写的,但基本上只是将SQL(MySQL)更改为Redis,所以我写的代码不需要太在意。如果不清楚,请在评论中指出。
总的来说,就是在现有的应用程序中,使用Redis来加速运行较重的SQL。
这种事情可能发生吗?
-
- 思いの外、アクティビティ系のページの閲覧数が高め
-
- 思ってもみなかった条件でのDB検索ロジックの追加
- GROUP BY とか ORDER とかの重たい SQL が Home画面 とかで利用されちゃってる
在这种情况下,使用Redis可以加速!(应该会有很多。虽然我知道SQL很重,但是没办法啊…对于已经放弃的人,我强烈推荐您尝试一下这种方法。那么实际上,让我们从(可能是)最容易上手的排名处理开始,使用Redis来加速试试看吧。
常见的排行榜
如果提到游戏和Redis,许多人会想到这个吧。
表:舞台分数
SELECT player_id, stage_id, max(score) as score
FROM stage_score
WHERE stage_id = 1
GROUP BY player_id
ORDER BY score DESC
LIMIT 0, 10;
如果我使用纯粹的SQL,就会变成这样。
GROUP BY、ORDER,甚至在规格上添加LIMIT也是很常见的事情。
实际上,如果仅仅使用MySQL来实现,可能会导致全表扫描,负载非常高……这样的情况也经常发生。仅仅使用MySQL来实现实时排名是非常困难的。
那么,让我们尝试使用Redis来实现吧。
// Redis接続
$redis = new Redis();
$redis->connect("localhost",6379);
// stage:1のTOP 10を取得
$stage_id = 1;
$with_score = true;// スコア情報も結果セットに含むかどうか
$key = "ranking:stage_id:{$stage_id}";// ランキング専用KeyでStageIdごとにkeyを作成
$topPlayerIds = $redis->zRevRange($key, 0, 9, $with_score);
好容易啊。
最近有什么变化吗? shé me ma?)
上述的SQL如何转换为Redis是通过将Key设置为每个StageId进行处理来接管SQL中的WHERE子句的处理。
此外,通过使用排序集合作为数据类型,ORDER子句的处理在数据集设置时自动进行,并在内部重新排序。
命令的意义 de
这次我们使用了zRevRange方法,实际上调用的是Redis本身的zrevrange。具体命令的详细解释将被省略,可以将其分为z / rev / range三个部分。
z ソート済みセット型に対するコマンド
rev reverseの略で降順にソートする
range 一定範囲内を指すことが多い
通过这个命令zrevrange,可以按降序获取已排序集合的一定范围内的元素。它几乎相当于之前提到的SQL语句的含义。而且命令名称非常直观易懂。
-
- じゃあ昇順は?
zrange
n点〜m点のリストを取得するには?
zrangebyscore or zrevrangebyscore
この人、何位?
zrank
這真是非常明白易懂。
除了排名以外的排序数据
Redis的有序集合经常用于排行榜的主题,除了这种用法外还有许多其他用途。
有序集合的排序特性可以消除类似于SQL中的ORDER BY子句和LIMIT子句的思考方式(复杂的情况可能会变得困难…)。
例如,最新的朋友打招呼列表。
表:friend_greet
SELECT player_id, target_id, DATE(created_at) AS date, MAX(created_at) as updated_at
FROM friend_greet
WHERE player_id = 123
GROUP BY target_id, player_id, DATE(created_at)
ORDER BY updated_at DESC
LIMIT 20
不想在考虑性能的游戏API中执行这个SQL语句呢。这个SQL的目的是获取以TargetId问候过的PlayerId每天的最后问候时间(updated_at)的列表。那我们试试用Redis吧。在这种情况下,我们仍然使用排序集合。真是太喜欢了。排序集合的score不仅仅是真正的分数值,时间戳也可以。在某种意义上,它就像是时间戳分数的要求。排序集合的member好在可以使用string类型传递数据,所以我们将在member中存储PlayerId和日期的数据。
$player_id = 123;
// 挨拶用Key
$key = "greet:target_id:{$target_id}";
$member = getGreetMemberString($player_id);
// 追加時
$redis->zAdd($key, time(), $member);
// 抽出時
$with_score = true;
$result = $redis->zRevRange($key, 0, 20, $with_score);
function getGreetMemberString($player_id) {
return $player_id . "_" . date("Ymd", time());
}
在此次Redis化过程中的关键是将多个GROUP BY指定重新分配给Key和Member,以便进行单一Key的管理。
Redis能够将GROUP句、ORDER句和LIMIT句全部解脱,因此我们尽量不希望在前端API中发出这些指令。非常感谢Redis。
最后
根据数据存储方式的不同,即使JOIN是必需的,也可以使用单一的键进行管理。如果我们有意识地进行简单的数据设计,我认为可以更多地依赖Redis来实现加速。与MySQL相反,所有的命令都是原子执行的。因此,要进行类似于MySQL中常见的事务处理或同时更新其他数据,需要进行一些额外的处理。(在Lua中实现这个过程很容易)
这次的主题是关于将现有的常用SQL转换成Redis的文章,面向初学者。如果有人能读完这篇文章,然后明天就去用grep命令找出”GROUP BY”等内容,那就可以吃上美味的饭菜了。请一定要尝试一下。
由于时间原因,我想到这里就结束了,但我个人希望在第二篇文章中写更多精彩的Redis用法。这是我自己的想法,只是为了编写时间不足而找的借口。晚安。•̀.̫•́✧
明天是 VoQn。我非常兴奋(๑•̀ㅂ•́)و✧。
蛇尾(不是最后的)
在排名方面的对同一分数的讨论中。
在Redis中,仅根据排序进行排名,而不考虑相同分数或不同分数的情况。
即使有5个人得到了相同的分数,结果也会是1到5名。
如果想要获取考虑相同分数的排名,可以使用zcount等方法进行操作。(有几种方法可以使用
使用zcount方法可以查找大于指定分数的成员数,从而获得想要了解的排名的分数。
$score = 12345;
$count = $redis->zCount($key, $score + 1, "+inf");
$rank = $count + 1;
配对
这是一种功能,可以在玩家之间找到状态相近的人。它从满足特定条件的子集中随机抽取一部分。近年来,许多网络服务也在做类似的事情。虽然在MySQL等数据库中也可以实现,但是可悲的是…在执行随机抽取时,查询缓存就不再有效,性能会显著降低。
では、少し複雑になってしまいますが特定のレベルから±2のプレイヤー20人を抽出しましょう。
// ソート済みセットで score=level member=player_id の状態で格納されているKey
$rank_key = "player:rank";
// 抽出の元となるプレイヤーのレベル
$target_level = 10;
// 部分集合の条件
$level_range = 2;
$min = $target_level - $level_range;
$max = $target_level + $level_range;
// min~maxの中で一番レベルの低いPlayerIdを取得する
$min_lv_player = $redis->zRangeByScore($rank_key, $min, $max,["limit"=>[0,1]]);
// 全体で何位のレベルなのかを知る
$min_player_rank = $redis->zRank($rank_key, $min_lv_player[0]);
// min~maxの中で一番レベルの高いPlayerIdを取得する
$max_lv_player = $redis->zRevRangeByScore($rank_key, $max, $min,["limit"=>[0,1]]);
// 全体で何位のレベルなのかを知る
$max_player_rank = $redis->zRank($rank_key, $max_lv_player[0]);
// $min_rank~$max_rankに居るはずなので、その中でランダムなrankを取得する
$target_rank = rand($min_player_rank, $max_player_rank);
// 一時的に取得するプレイヤーの数
$tmp_list_count = 500;
$hit_player_id_list = $redis->zrange($rank_key, $target_rank - $tmp_list_count, $target_rank + $tmp_list_count);
// 取得できたのでこの中から20人取得しましょう。
shuffle($hit_player_id_list);
$return = [];
for($i=0; $i<20; $i++) {
$return[] = $hit_player_id_list[$i];
}
実際ここはRedisを使っていても結構コストの高い処理になるので、$hit_player_id_listなどを別のキャッシュとして持たせておいて、別プロセスで30秒ごとに更新等やってあげるといいかもしれませんね。
但是,我也想要实时完成!对那些这样说的人来说。
使用Lua+EVAL进行处理
如果使用上述的匹配方式,在处理过程中无法考虑到排名信息发生变化的情况。在这种情况下,可以使用Lua将多个命令合并为一个ScriptCommand进行执行。
local rank_key = 'player:rank'
local target_level = ARGV[1]
local level_range = ARGV[2]
local min_lv_player = redis.Call(
'zrangebyscore',
rank_key,
target_level - level_range,
target_level + level_range,
'LIMIT', 0, 1
)
local min_player_rank = redis.Call('zrank', rank_key, min_lv_player[0])
local max_lv_player = redis.Call(
'zrangebyscore',
rank_key,
target_level + level_range,
target_level - level_range,
'LIMIT', 0, 1
)
local max_player_rank = redis.Call('zrank', rank_key, max_lv_player[0])
local target_rank = math.random(min_player_rank, max_player_rank)
local tmp_list_count = ARGV[3]
return redis.Call(
rank_key,
target_rank - tmp_list_count,
target_rank + tmp_list_count
)
将以此方式编写的Lua脚本传递给Redis的EVAL命令。
$lua_script = getLuaScript(); //Lua:matchingの内容を返す関数
$params = [
$target_level,
$level_range,
$tmp_list_count,
];
$hit_player_id_list = $redis->eval($lua_script, $params);
由于在EVAL中执行的脚本会被保存在Redis中,所以将可变的值通过ARGV传递会更为经济。