地图信息需要保存在服务器端。用 Redis 做这件事不太合适:一是数据量太大,二是大部分属于冷数据。虽然排序用 Redis 会方便很多,但整体还是倾向于用 MySQL。

功能需求

  1. 玩家可以任意创作地图,并记录相关信息,例如 id、创建者 id、时间戳、鸡蛋数量、鲜花数量等。
  2. 玩家可以查看已有的地图,类似留言板,支持大量查询和动态加载。
  3. 每天挑选好评数最多的 50 张地图加入自定义地图池(类似 LOL 每周免费英雄)。所有玩家的地图 = 官方地图 + 自定义地图。

方案一:Redis

用 Redis 实现时,数据结构设计如下:

地图编辑器的  redis 结构


每张地图信息 string   记录着地图创建者 id 鲜花 鸡蛋  时间戳等信息
key=map-info-XXXX  value={"like:12,hate:12"}
XXX表示唯一id

每天上传地图信息  list   只是地图id列表
key=maps-info-day-2018-1-1  value=list 只记录list id 索引

每天的排行榜信息 sorted-set
key=maps-score-day-2018-1-1 value=sorted-set 只记录分数 和地图id  在这里可动态更新  set 集合的 score 和member(value)--排序,分数由鲜花鸡蛋等 计算出来

虽然 Redis 能很好地完成排序和单个玩家的索引,但"查看所有地图"(功能 2)就不好处理了。要么遍历所有相关 key,要么额外维护一个 key-list 保存所有玩家的地图 id。这样一来 key 数量过多、结构复杂,删除一条记录时其他 key 都要同步更新,集群环境下还无法使用 mset。

  • 缺陷:整体查询不易实现。
  • 优点:排序方便(性能待与 MySQL 对比)。

方案二:MySQL 查询优化

主要涉及两个问题:查询和排序。首先建立索引:

create index idx on map_info(id);

查询性能测试

先创建 500W 条记录,打开 navicat for mysql 统计:

set profiling=1;
show profiles;
  1. 暴力全量查询:select * from map_info,耗时 3.282s
  2. LIMIT 分页暴力查询:SELECT * FROM `map_info` LIMIT 3000000, 20,耗时 0.77s
  3. WHERE id 优化分页查询:SELECT * FROM `map_info` WHERE id > 3000000 LIMIT 20,耗时 0.000s
  4. 倒序优化分页查询:SELECT * FROM `map_info` WHERE id < 2000000000000 ORDER BY id DESC LIMIT 10,耗时 0.000s

排序性能测试

排序使用常规 ORDER BY,因为排序只需每天执行一次,耗时长一些也可以接受。

  1. 按鸡蛋数排序:SELECT * FROM `map_info` WHERE id < 2000000000000 ORDER BY hate DESC LIMIT 50,耗时 1.7s
  2. 1E 条数据完全排序耗时:84.170s

方案三:Redis 与 MySQL 协作

MySQL 查询优化后速度很快,但排序较慢。解决思路是缓存排序结果。由于实际需求只需每天凌晨更新一次排序,这恰好可以把复杂的 SQL 排序工作异步化处理。

方案:数据的查询和写入直接操作 MySQL,每天凌晨执行一次排序,将结果缓存到 Redis。当天所有自定义地图(排序后的数据)的请求全部访问 Redis 缓存。如果优化后的查询仍无法满足性能需求,可进一步利用 Redis 缓存热点查询结果(具体取决于实际性能指标,例如大量并发连接下单台机器虽然是毫秒级,但多机扩展时性能会下降)。

此外,可以与策划协商在排序期间(如凌晨半小时内)临时禁止玩家上传地图,避免排序过程中出现意外情况。