http://www.cnPHP6.com/archives/28202
Practical Nosql – Solving a Real Problem with MongoDB and Redis。还是牛逼的Karl Seguin写的,08 May 2011。
作者好牛逼啊,我不懂的他都懂。
Practical NoSQL – Solving a Real Problem with MongoDB and Redis
在我写的那本小人书(我擦,免费的)里面,我提出了这样一个观点,总的来说,Nosql 扩展了我们的数据存储方式。除了新的工具,Nosql 也关心查看和保存数据,以一种开放的心态,用新的技术(就算不是最新的,也是差不多新的好吧)。
这周末,我花了点时间解决了个问题,用两个新的工具和一个新的建模方式。我好开心显摆一下。你知道我已经开始重写我的 mogade 了(点这点这),有个问题我一直耿耿于怀,就是我怎么快速处理玩家的积分榜。同时也让我有机会去重新考虑怎么处理积分页面(因为这两个有点联系)。这两个改变都跟我们怎么保存积分相关。首先,我希望解释一下第一版方法,然后让你们看看我是怎么在第二版把野鸡变凤凰的。
好吧,小白们要知道的是mogade是给游戏开发者处理积分的。(我擦,还是免费!我是不是很大方。)
渣渣设计
保存积分
第一版 mogade 的数据建模,就像你见到的所有的传统方式一样。实际上,就是用户的分数被保存在一个scores集合里面。集合包含了排行榜 id,用户的名字,一个用户的唯一标识符(假设是设备id + 名字),然后是得分日,还有用户的积分:
| lid | name | unique | date | points | | 1 | leto | device-1-leto | 2011/04/11 10:32 | 100000 | | 1 | paul | device-1-paul | 2011/04/12 09:22 | 200000 | | 2 | duke | device-3-duke | 2011/04/12 18:34 | 200000 | | 1 | jess | device-2-jess | 2011/04/13 21:44 | 300000 |
事实上,还有一些复杂的附加要求。比如说,还允许不同的时间段积分(日常,周常和历史),开发者可以控制什么时候刷新积分(就一个 UTC 设置),嗯,我觉得还挺吃惊的,这个功能的需求呼声超高。因此实际的情况是,我们有三个集合scores_daily,scores_weekly 和 scores_overall 。这些可以全塞到一个集合里面去,不过这就要用到其他的索引,而且,我觉得,会导致更复杂的代码。并且我们还允许保存任意数据。于是最终,我只保留了玩家在每个时间段的最好分数。
保存新分数非常简单。我们要做的仅仅是拿到玩家的上一次日常,周常和历史,然后如果新分数更高的话,更新他们。我们可以处理的更好一点,比如说日常分数比上次低的话,那么它的周常和历史肯定比上次低。
如果新分数比上次低,那么这个存储会调用一次读处理。如果新分数比较高,那么会调用三次读和三次写处理。
日常和周常分数可以过时。就是说,我们拿到的分数是上一次的日常或者周常就可以。我们不需要精确到什么时候换天什么时候换周。就是说,我们比较前一次的最高分,我们检查一下积分和日期…就那样,没啥难的。
拿排行榜页面的方法是:根据请求的范围(日常,周常或历史),我们查询相应的集合,按照分数逆序,然后取大于某天的分数。就像这样:lid = ‘someid’ && date >=’ 2011/04/13 12:00:00am’,简单到死。
获取排名
当我们想查找玩家排名的时候,我们先在每个范围里面找到他的最好分数(日常, 周常和历史。好了好了我知道了,不用每次都括号),然后看看最好排名,超简单,对不对?但是问题是,万一我的游戏牛逼了,我们就要去计算上百万行数据。再说,由于mogade 是针对休闲/移动游戏的。玩一把游戏的时间都挺短。意思也就是说,我们一秒要拿很多次排行。性能和存储数据条数是线性关系。这种方式根本不是网络规模的,就算你有索引(啊,好吧,我们想出了一个办法,限制只有前5000名有名次,我靠,渣爆了)。
新的设计
我们要做的第一个改变很小,不过我确实想改,就是,我们不要再保存分数创建的日期和时间了。取而代之的是,我们保存积分榜的开始。就是说,如果我们保存一个日常积分到一个 UTC 偏移是 4 的积分榜上,我们实际上存储的是2011/05/07 20:00(假设是五月八号)。为啥?俩原因。首先拿积分榜现在从 a>=变成了==。此外,我们可以存成 32bit 的,而不是 64bit 的字段,从而降低我们的索引内存开销。
另外,我们引进一个新的 high_scores 集合,一个简单集合,用来跟踪用户的所有范围内的最高分。这是一个非常重要的改变,很多情况下,他会把 3 次读取(对三个独立的集合)转化为 1 次读取。这让我们完全从重复的获取 scores_daliy,scores_weekly 和 scores_overall 中脱离出来。噢~就像你看到的那样,我们为了解决排名的问题,对保存数据做了一个显著的改变,是个很好的平衡。
新排名,第一发
真正有意思的改变,虽然都是为了解决我们的排名问题。目标是减少性能对排行榜中记录数量的依赖。第一发不走寻常路的尝试是,把不规范的排名保存到他们自己的集合里面去。忘记那个多范围和排行榜,看看我的想法有多变态:
@H_404_85@| points | rank | count | | 100000 | 5233 | 2 | | 200000 | 2088 | 1 | | 300000 | 1002 | 4 |然后我们可以拿到玩家的分数,可以找到他她的排名,只要查正确的行(以 points 为 key (还有排行榜 id, 范围 ,和日期…))
当然,你怎么保证上面的排名最新?首先,当你拿到一个新的分数,你需要更新那些战斗力低的渣渣。第二,你需要某种方式来解决删除/忽略那些不属于任何玩家的排名(因为我们一个用户只保存一个得分,因此当他/她拿到更好的分数的时候,他/她的老排名就没用了)。到这里,我决定继续下去。这不是完全浪费时间,我觉得,在我的脑中,已经有了一个排名非线性的想法。让我们来说说我遇到的难点。
新排名,第二发
哥经历了上面那个失败(很牛逼的)之后,非常不爽。我决定,我需要一个更屌的解决案。哼哼哼哼~~现在我们的 Redis 登场了。像哥那么有才华的人,在 Redis 早期版本就熟悉它了(在一年前或者更早,用我大 C# 写了一个异步驱动)。因此我有概念。
事实证明,有序集是 Redis 数据结构的核心。怎么工作的呢?你给 Redis 一个 key, 一个权重和一个值,它就能提供所有的牛逼简单的排序方法。我们的 key 被设置为排行榜 id, 权重,被设置为 范围 + 排行榜 开始时间。看起来像 3234_daily_20110507200 (这是 2011年五月七号晚上八点),那在某时间段内,在排行榜 3234 上的日常得分,都可以加到这个集合中:
@H_404_85@Redis.zadd("3234_daily_201105072000",1000000,"device1-leto") Redis.zadd("3234_daily_201105072000",200000,"device1-paul") Redis.zadd("3234_daily_201105072000",300000,"device-2-jess")最后一个参数我们称为成员,非常有用。当我们追加一个值中包含已存在成员的时候, Redis 更新老值。这完全符合我们的需求,一个用户一个得分。
获取排名同样是用成员。因此我们不需要我们的 high_scores 事先收集玩家的最高分:
@H_404_85@Redis.zrevrank("3234_daily_201105072000","device1-paul") # gives us 1 (it's 0 based)这样一来,保存积分的流程是什么?它没变太多。首先我们获取玩家的最高积分,从我们的新的 high_scores 集合里面。然后,当这个分数高于我们以前的日常,周常或者总分的时候,我们存 scores_SCOPE 到 MongoDB 集合,存 key 和 high_scores 到 Redis 集合。
如果你有注意的话,最好的情况(当心的分数比原来的分数低的时候),我们还是有 1 次读取。最坏的情况,我们有 1 次读取,4 次写入到 MongoDB 和 3 次写入到 Redis(和野鸡设计的 3 读取和 3 写入相比)。还有其他的一般情况,比日常/周常好,但是不比周常/总分好(原因和原来一样)。
思考
首先,八次外部存储操作,感觉不好。此外,我们还考虑追加另外一个类型的更复杂的情况。
关于这个,有两点需要记住的。首先,写比读少好多。优化读操作非常有意义。而且,我不是单纯的把这些操作发生的时间和地点移位。我们已经基本上改变了这些操作,尤其是对热点。我做了一些初步的性能测试,结果……太屌了。给了一个排行榜,有五百万记录,随机10000分,进行排名。原来的方法,花了好几分钟(嗯,5左右…吧,我觉得太无聊了关了)。然后用新方法,147毫秒!我擦,我花了好久去检查我的测试是不是真的起作用了,以及各种我能够相信的理由。所以……性能得到了超棒的提升(zadd 和 zrevrank 都是 O(log(N)))。我对那少几次的 save 不介意。
我考虑用 Redis 来处理我们的排行榜,不过分数可以通过不止 点数/排名(那些我提到过的数据),需要删除原先的玩家老条目创建新的。MongoDB的更新插入很快的就完成了这部分,它太牛逼了。
至于复杂性。好吧,我现在得弄两个数据库了。不过,想 MongoDB, Redis 非常容易使用和管理。我有超多重复的数据,以及一堆可能失去同步的东西。high_scores 集合需要授权,以便我能处理 scores_SCOPE 集合和 Redis 的 key。此外,我还打算运行一个 Redis master 在 MongoDB slave 上,然后是 MongoDB master 在 Redis slave 上,没额外硬件。
最后, 既然Redis 能用了, 我敢肯定,我们肯定会拿它重新设计我们其他的功能。比如说,最终我们可以提供一些实时分析。
结论
在这里我有几个收获。首先,这周末的代码写得真他妈的爽。更重要的是,你应该熟悉更多的工具和概念。这样,当你遇到问题的时候,你知道你应该往哪走。如果不是因为哥以前玩过 Redis,到现在肯定也找不到好的解决案。你不需要精通他们,不过你应该,比如说,知道什么是 supperccolumn。
此外,这更加加深了我对 Nosql 的印象了。 MongoDB 的一些功能超专业(geospatial,logging),不过它在很大程度上,是一个在 RDBMS 之上的,通用数据存储。而 Redis,我没完全发挥它的作用,它更屌,可以处理/查询/查看不同数据。这个解决案让它们俩在很好的结合在一起,不单是数据,而是更简单更高效。
顺便说一句,如果你有兴趣帮忙。 mogade 真的对 Android 终端很便利。联系我吧,如果你可以。(第二版 api 将会比第一版更干净清洁,不过你仍然可以从github 上第一版 C# 实现的 api中获得一些灵感)。
原文链接:https://www.f2er.com/nosql/203666.html