引言
在日常开发中,我们经常会遇到「需要按特定顺序展示数据」的场景——比如按点赞时间展示前n名用户、按操作时间展示最近操作记录、按热度排序展示内容等。为了提升性能,很多开发者会用redis做排序存储,再用mysql查询详细数据,但往往会遇到一个共性问题:redis返回的顺序是正确的,可mysql查询后,顺序就彻底乱了。
这篇博客就详细拆解这个高频问题,从错误场景、根本原因,到具体解决方案,再到核心代码的逐行解析,全程通用,不管你做的是社交、电商还是其他项目,只要遇到「redis排序+mysql查询」的组合,都能直接复用解决方案。
一、错误出现的通用场景(不止某一个项目)
只要满足以下3个条件,就大概率会遇到这个乱序问题,几乎覆盖所有需要「排序+详情查询」的业务场景:
- 用redis的zset结构存储需要排序的数据(比如用户id、内容id),以时间戳、热度值等作为score,实现按指定规则排序(如时间正序、热度倒序);
- 需要从redis中获取排序后的前n条id(比如前5个点赞用户、前10条热门内容);
- 根据redis返回的id,去mysql中查询详细数据(比如用户头像、昵称,内容标题、作者等),最终将数据返回给前端展示。
最终表现:前端展示的内容顺序,和redis中排序的顺序完全不一致,甚至毫无规律(比如按点赞时间排序,结果展示的是按用户id排序的头像)。
二、完整执行流程
我们用「按点赞时间展示前5个用户头像」这个最通用的场景,拆解从数据存储到前端展示的全流程,清晰看到问题出在哪里。
步骤1:redis存储排序数据(顺序完全正确)
为了实现「按点赞时间正序排序」,我们用redis的zset存储点赞记录,核心逻辑如下(通用代码,不限语言,这里以java为例):
// key:业务标识(如「内容点赞集合_内容id」)
// value:需要排序的id(如用户id)
// score:排序依据(如点赞时间戳,保证按时间正序排序)
stringredistemplate.opsforzset().add("like:content:100", "103", system.currenttimemillis());
stringredistemplate.opsforzset().add("like:content:100", "101", system.currenttimemillis() + 1000);
stringredistemplate.opsforzset().add("like:content:100", "105", system.currenttimemillis() + 2000);redis的zset会自动根据score(时间戳)排序,最早点赞的用户id排在最前面。此时redis中存储的顺序是:103 → 101 → 105(正确顺序)。
步骤2:从redis获取排序后的id(顺序依然正确)
我们从redis中获取前5个点赞用户的id,代码如下:
// range(0, 4):获取排序后前5个id,顺序与redis存储一致
set<string> sortedids = stringredistemplate.opsforzset().range("like:content:100", 0, 4);
// 转换为long类型集合,方便后续查询mysql
list<long> ids = sortedids.stream().map(long::valueof).collect(collectors.tolist());此时ids集合的顺序是:[103, 101, 105](依然是正确的点赞时间顺序)。
步骤3:mysql查询详细数据(顺序被打乱)
我们需要根据上面的ids集合,去mysql中查询用户的详细信息(头像、昵称等),代码如下:
// 根据id集合查询用户,这是最常用的批量查询方式 list<user> userlist = usermapper.listbyids(ids);
这段代码对应的sql语句(不管用什么orm框架,最终都会生成类似sql):
select * from user where id in (103, 101, 105);
这里就是问题的核心:mysql的in查询,不会按照我们传入的id顺序返回结果!
mysql默认的排序规则是「按主键id升序排列」,所以实际返回的userlist顺序是:101 → 103 → 105(打乱了redis的正确顺序)。
步骤4:直接返回前端(乱序展示)
如果我们不做任何处理,直接将mysql查询到的userlist转换为前端需要的格式并返回,前端就会按照「101 → 103 → 105」的顺序展示头像,和我们期望的「103 → 101 → 105」(点赞时间顺序)完全不一致,问题爆发。
三、错误的根本原因(通用,所有项目都适用)
很多开发者会误以为是redis排序出了问题,或者mysql查询出错了,但其实两者都没有错,问题出在「两者的职责差异」和「我们的遗漏处理」:
- redis的职责:只负责「存储需要排序的id」和「按指定规则排序」,不存储详细数据(如用户头像、昵称),所以它只能返回排序后的id,无法直接返回前端需要的完整数据;
- mysql的职责:存储详细数据,支持批量查询,但mysql的in查询「不保证返回顺序」,默认按主键id升序排列(不同数据库可能有差异,但都不会按传入的in参数顺序返回);
- 我们的遗漏:没有对mysql返回的乱序数据,做「顺序修复」,直接将乱序数据返回给前端,导致展示错误。
一句话总结:redis给了正确的顺序,mysql打乱了顺序,我们没修复,所以乱序。
四、通用解决方案(核心,直接复制可用)
解决方案的核心思路非常简单:保留redis返回的正确id顺序,在java内存中,将mysql查询到的乱序数据,按照正确的id顺序重新排序。
这种方式的优势:不操作数据库,仅在内存中排序,性能损耗可忽略不计,且通用所有项目,不管你用的是spring、mybatis还是其他框架,都能直接复用。
完整修复代码(通用java版)
// 1. 从redis获取排序后的id(正确顺序)
string rediskey = "like:content:" + contentid; // 通用业务key,替换为自己的即可
set<string> sortedids = stringredistemplate.opsforzset().range(rediskey, 0, 4);
// 处理空值,避免空指针
if (sortedids == null || sortedids.isempty()) {
return result.ok(collections.emptylist()); // result替换为自己项目的返回工具类
}
// 2. 转换为long类型的id集合(正确顺序)
list<long> ids = sortedids.stream().map(long::valueof).collect(collectors.tolist());
// 3. 从mysql查询用户详细数据(乱序)
list<user> userlist = usermapper.listbyids(ids);
// 4. 关键:按照redis的正确顺序,重新排序用户列表(核心修复代码)
list<userdto> userdtolist = userlist.stream()
// 排序核心逻辑,下面会逐行详解
.sorted(comparator.comparing(user -> ids.indexof(user.getid())))
// 转换为前端需要的dto(根据自己项目调整)
.map(user -> beanutil.copyproperties(user, userdto.class))
.collect(collectors.tolist());
// 5. 返回给前端(此时顺序已正确)
return result.ok(userdtolist);五、核心排序代码逐行详解(最易懂,小白也能懂)
很多开发者卡在这里,不是不会用,而是看不懂排序代码的语法和作用,这里逐行拆解,全程大白话,不绕弯。
核心排序代码(单独拎出来,重点讲解):
.sorted(comparator.comparing(user -> ids.indexof(user.getid())))
1. 先搞懂每个部分的作用(通俗版)
sorted():这是java stream流的排序方法,仅在内存中排序,不操作任何数据库,相当于我们把mysql查出来的乱序用户列表,在代码里手动重新排了一遍;comparator.comparing():指定排序的「依据」——告诉程序,我们要按照什么规则来排序;user -> ids.indexof(user.getid()):排序的核心规则,我们拆成两部分看:user.getid():获取当前遍历的用户id(比如101、103、105);ids.indexof(用户id):获取这个用户id在「redis正确顺序的ids集合」中的「下标位置」(下标从0开始,数字越小,排越前)。
2. 用例子看懂执行过程(最直观)
已知:
- redis正确顺序的ids集合:[103, 101, 105];
- mysql查询返回的乱序userlist:[101, 103, 105]。
我们逐一遍历userlist中的每个用户,计算排序依据,再排序:
- 用户101:
ids.indexof(101)→ 下标是1; - 用户103:
ids.indexof(103)→ 下标是0; - 用户105:
ids.indexof(105)→ 下标是2。
排序规则:按照「下标数字从小到大」排序,所以最终排序后的顺序是:
下标0(103)→ 下标1(101)→ 下标2(105),和redis的正确顺序完全一致!
3. 一句话总结这段代码的作用
「让mysql查出来的乱序用户,按照redis给出的正确顺序,重新排队,还原我们想要的排序规则(比如点赞时间顺序)」。
六、拓展方案:在mybatis中直接排序(无需java内存排序)
如果你不想用java代码排序,也可以在mysql层面直接强制排序,让mysql返回正确顺序的结果,这种方式适合对sql熟悉的开发者,同样通用。
1. mapper接口(通用版)
/**
* 根据id集合查询用户,按传入的id顺序返回
* @param ids redis返回的正确顺序id集合
* @return 按正确顺序排列的用户列表
*/
list<user> listbyidswithorder(@param("ids") list<long> ids);2. mybatis xml映射文件(核心sql)
<select id="listbyidswithorder" resulttype="com.xxx.entity.user">
select * from user
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">;
#{id}
</foreach>;
<!-- 关键:强制按照传入的id顺序排序 -->
order by field(id,
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</select>3. 核心说明
order by field(id, 103, 101, 105)是mysql的专用语法,作用是「强制按照括号内的id顺序返回结果」,括号内的id顺序就是我们从redis获取的正确顺序。
优点:查询结果直接有序,无需java代码额外处理;缺点:sql复杂度略有提升,且仅适用于mysql数据库。
七、总结(通用,所有开发者必看)
1. 问题共性
只要用「redis zset排序 + mysql in查询详细数据」,就一定会遇到「顺序乱掉」的问题,这不是redis或mysql的bug,而是两者的职责差异导致的。
2. 核心解决方案(优先推荐)
用java stream的sorted(comparator.comparing(user -> ids.indexof(user.getid()))) ,在内存中修复顺序,通用、简单、无性能损耗,直接复制可用。
3. 关键提醒
- 不要误以为mysql的in查询会按传入顺序返回,这是很多开发者的常见误区;
- 排序代码不操作数据库,仅内存排序,不用担心性能问题;
- 不管你做的是点赞、热门内容、操作记录等场景,只要涉及「redis排序+mysql查询」,这个解决方案都能直接复用。
最后,希望这篇博客能帮到所有遇到同类问题的开发者,避免踩坑,高效解决乱序问题。
以上就是redis排序后mysql查询乱序问题的原因及解决方法的详细内容,更多关于redis排序后mysql查询乱序的资料请关注代码网其它相关文章!
发表评论