地图信息需要保存在服务器端。用 Redis 做这件事不太合适:一是数据量太大,二是大部分属于冷数据。虽然排序用 Redis 会方便很多,但整体还是倾向于用 MySQL。
功能需求
- 玩家可以任意创作地图,并记录相关信息,例如 id、创建者 id、时间戳、鸡蛋数量、鲜花数量等。
- 玩家可以查看已有的地图,类似留言板,支持大量查询和动态加载。
- 每天挑选好评数最多的 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;
- 暴力全量查询:
select * from map_info,耗时 3.282s - LIMIT 分页暴力查询:
SELECT * FROM `map_info` LIMIT 3000000, 20,耗时 0.77s - WHERE id 优化分页查询:
SELECT * FROM `map_info` WHERE id > 3000000 LIMIT 20,耗时 0.000s - 倒序优化分页查询:
SELECT * FROM `map_info` WHERE id < 2000000000000 ORDER BY id DESC LIMIT 10,耗时 0.000s
排序性能测试
排序使用常规 ORDER BY,因为排序只需每天执行一次,耗时长一些也可以接受。
- 按鸡蛋数排序:
SELECT * FROM `map_info` WHERE id < 2000000000000 ORDER BY hate DESC LIMIT 50,耗时 1.7s - 1E 条数据完全排序耗时:84.170s
方案三:Redis 与 MySQL 协作
MySQL 查询优化后速度很快,但排序较慢。解决思路是缓存排序结果。由于实际需求只需每天凌晨更新一次排序,这恰好可以把复杂的 SQL 排序工作异步化处理。
方案:数据的查询和写入直接操作 MySQL,每天凌晨执行一次排序,将结果缓存到 Redis。当天所有自定义地图(排序后的数据)的请求全部访问 Redis 缓存。如果优化后的查询仍无法满足性能需求,可进一步利用 Redis 缓存热点查询结果(具体取决于实际性能指标,例如大量并发连接下单台机器虽然是毫秒级,但多机扩展时性能会下降)。
此外,可以与策划协商在排序期间(如凌晨半小时内)临时禁止玩家上传地图,避免排序过程中出现意外情况。