Redis排序后MySQL查詢亂序問題的原因及解決方法
引言
在日常開發(fā)中,我們經(jīng)常會(huì)遇到「需要按特定順序展示數(shù)據(jù)」的場景——比如按點(diǎn)贊時(shí)間展示前N名用戶、按操作時(shí)間展示最近操作記錄、按熱度排序展示內(nèi)容等。為了提升性能,很多開發(fā)者會(huì)用Redis做排序存儲(chǔ),再用MySQL查詢詳細(xì)數(shù)據(jù),但往往會(huì)遇到一個(gè)共性問題:Redis返回的順序是正確的,可MySQL查詢后,順序就徹底亂了。
這篇博客就詳細(xì)拆解這個(gè)高頻問題,從錯(cuò)誤場景、根本原因,到具體解決方案,再到核心代碼的逐行解析,全程通用,不管你做的是社交、電商還是其他項(xiàng)目,只要遇到「Redis排序+MySQL查詢」的組合,都能直接復(fù)用解決方案。
一、錯(cuò)誤出現(xiàn)的通用場景(不止某一個(gè)項(xiàng)目)
只要滿足以下3個(gè)條件,就大概率會(huì)遇到這個(gè)亂序問題,幾乎覆蓋所有需要「排序+詳情查詢」的業(yè)務(wù)場景:
- 用Redis的ZSet結(jié)構(gòu)存儲(chǔ)需要排序的數(shù)據(jù)(比如用戶ID、內(nèi)容ID),以時(shí)間戳、熱度值等作為score,實(shí)現(xiàn)按指定規(guī)則排序(如時(shí)間正序、熱度倒序);
- 需要從Redis中獲取排序后的前N條ID(比如前5個(gè)點(diǎn)贊用戶、前10條熱門內(nèi)容);
- 根據(jù)Redis返回的ID,去MySQL中查詢詳細(xì)數(shù)據(jù)(比如用戶頭像、昵稱,內(nèi)容標(biāo)題、作者等),最終將數(shù)據(jù)返回給前端展示。
最終表現(xiàn):前端展示的內(nèi)容順序,和Redis中排序的順序完全不一致,甚至毫無規(guī)律(比如按點(diǎn)贊時(shí)間排序,結(jié)果展示的是按用戶ID排序的頭像)。
二、完整執(zhí)行流程
我們用「按點(diǎn)贊時(shí)間展示前5個(gè)用戶頭像」這個(gè)最通用的場景,拆解從數(shù)據(jù)存儲(chǔ)到前端展示的全流程,清晰看到問題出在哪里。
步驟1:Redis存儲(chǔ)排序數(shù)據(jù)(順序完全正確)
為了實(shí)現(xiàn)「按點(diǎn)贊時(shí)間正序排序」,我們用Redis的ZSet存儲(chǔ)點(diǎn)贊記錄,核心邏輯如下(通用代碼,不限語言,這里以Java為例):
// key:業(yè)務(wù)標(biāo)識(shí)(如「內(nèi)容點(diǎn)贊集合_內(nèi)容ID」)
// value:需要排序的ID(如用戶ID)
// score:排序依據(jù)(如點(diǎn)贊時(shí)間戳,保證按時(shí)間正序排序)
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會(huì)自動(dòng)根據(jù)score(時(shí)間戳)排序,最早點(diǎn)贊的用戶ID排在最前面。此時(shí)Redis中存儲(chǔ)的順序是:103 → 101 → 105(正確順序)。
步驟2:從Redis獲取排序后的ID(順序依然正確)
我們從Redis中獲取前5個(gè)點(diǎn)贊用戶的ID,代碼如下:
// range(0, 4):獲取排序后前5個(gè)ID,順序與Redis存儲(chǔ)一致
Set<String> sortedIds = stringRedisTemplate.opsForZSet().range("like:content:100", 0, 4);
// 轉(zhuǎn)換為Long類型集合,方便后續(xù)查詢MySQL
List<Long> ids = sortedIds.stream().map(Long::valueOf).collect(Collectors.toList());此時(shí)ids集合的順序是:[103, 101, 105](依然是正確的點(diǎn)贊時(shí)間順序)。
步驟3:MySQL查詢詳細(xì)數(shù)據(jù)(順序被打亂)
我們需要根據(jù)上面的ids集合,去MySQL中查詢用戶的詳細(xì)信息(頭像、昵稱等),代碼如下:
// 根據(jù)ID集合查詢用戶,這是最常用的批量查詢方式 List<User> userList = userMapper.listByIds(ids);
這段代碼對(duì)應(yīng)的SQL語句(不管用什么ORM框架,最終都會(huì)生成類似SQL):
SELECT * FROM user WHERE id IN (103, 101, 105);
這里就是問題的核心:MySQL的IN查詢,不會(huì)按照我們傳入的ID順序返回結(jié)果!
MySQL默認(rèn)的排序規(guī)則是「按主鍵ID升序排列」,所以實(shí)際返回的userList順序是:101 → 103 → 105(打亂了Redis的正確順序)。
步驟4:直接返回前端(亂序展示)
如果我們不做任何處理,直接將MySQL查詢到的userList轉(zhuǎn)換為前端需要的格式并返回,前端就會(huì)按照「101 → 103 → 105」的順序展示頭像,和我們期望的「103 → 101 → 105」(點(diǎn)贊時(shí)間順序)完全不一致,問題爆發(fā)。
三、錯(cuò)誤的根本原因(通用,所有項(xiàng)目都適用)
很多開發(fā)者會(huì)誤以為是Redis排序出了問題,或者M(jìn)ySQL查詢出錯(cuò)了,但其實(shí)兩者都沒有錯(cuò),問題出在「兩者的職責(zé)差異」和「我們的遺漏處理」:
- Redis的職責(zé):只負(fù)責(zé)「存儲(chǔ)需要排序的ID」和「按指定規(guī)則排序」,不存儲(chǔ)詳細(xì)數(shù)據(jù)(如用戶頭像、昵稱),所以它只能返回排序后的ID,無法直接返回前端需要的完整數(shù)據(jù);
- MySQL的職責(zé):存儲(chǔ)詳細(xì)數(shù)據(jù),支持批量查詢,但MySQL的IN查詢「不保證返回順序」,默認(rèn)按主鍵ID升序排列(不同數(shù)據(jù)庫可能有差異,但都不會(huì)按傳入的IN參數(shù)順序返回);
- 我們的遺漏:沒有對(duì)MySQL返回的亂序數(shù)據(jù),做「順序修復(fù)」,直接將亂序數(shù)據(jù)返回給前端,導(dǎo)致展示錯(cuò)誤。
一句話總結(jié):Redis給了正確的順序,MySQL打亂了順序,我們沒修復(fù),所以亂序。
四、通用解決方案(核心,直接復(fù)制可用)
解決方案的核心思路非常簡單:保留Redis返回的正確ID順序,在Java內(nèi)存中,將MySQL查詢到的亂序數(shù)據(jù),按照正確的ID順序重新排序。
這種方式的優(yōu)勢:不操作數(shù)據(jù)庫,僅在內(nèi)存中排序,性能損耗可忽略不計(jì),且通用所有項(xiàng)目,不管你用的是Spring、MyBatis還是其他框架,都能直接復(fù)用。
完整修復(fù)代碼(通用Java版)
// 1. 從Redis獲取排序后的ID(正確順序)
String redisKey = "like:content:" + contentId; // 通用業(yè)務(wù)key,替換為自己的即可
Set<String> sortedIds = stringRedisTemplate.opsForZSet().range(redisKey, 0, 4);
// 處理空值,避免空指針
if (sortedIds == null || sortedIds.isEmpty()) {
return Result.ok(Collections.emptyList()); // Result替換為自己項(xiàng)目的返回工具類
}
// 2. 轉(zhuǎn)換為Long類型的ID集合(正確順序)
List<Long> ids = sortedIds.stream().map(Long::valueOf).collect(Collectors.toList());
// 3. 從MySQL查詢用戶詳細(xì)數(shù)據(jù)(亂序)
List<User> userList = userMapper.listByIds(ids);
// 4. 關(guān)鍵:按照Redis的正確順序,重新排序用戶列表(核心修復(fù)代碼)
List<UserDTO> userDTOList = userList.stream()
// 排序核心邏輯,下面會(huì)逐行詳解
.sorted(Comparator.comparing(user -> ids.indexOf(user.getId())))
// 轉(zhuǎn)換為前端需要的DTO(根據(jù)自己項(xiàng)目調(diào)整)
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
// 5. 返回給前端(此時(shí)順序已正確)
return Result.ok(userDTOList);五、核心排序代碼逐行詳解(最易懂,小白也能懂)
很多開發(fā)者卡在這里,不是不會(huì)用,而是看不懂排序代碼的語法和作用,這里逐行拆解,全程大白話,不繞彎。
核心排序代碼(單獨(dú)拎出來,重點(diǎn)講解):
.sorted(Comparator.comparing(user -> ids.indexOf(user.getId())))
1. 先搞懂每個(gè)部分的作用(通俗版)
sorted():這是Java Stream流的排序方法,僅在內(nèi)存中排序,不操作任何數(shù)據(jù)庫,相當(dāng)于我們把MySQL查出來的亂序用戶列表,在代碼里手動(dòng)重新排了一遍;Comparator.comparing():指定排序的「依據(jù)」——告訴程序,我們要按照什么規(guī)則來排序;user -> ids.indexOf(user.getId()):排序的核心規(guī)則,我們拆成兩部分看:user.getId():獲取當(dāng)前遍歷的用戶ID(比如101、103、105);ids.indexOf(用戶ID):獲取這個(gè)用戶ID在「Redis正確順序的ids集合」中的「下標(biāo)位置」(下標(biāo)從0開始,數(shù)字越小,排越前)。
2. 用例子看懂執(zhí)行過程(最直觀)
已知:
- Redis正確順序的ids集合:[103, 101, 105];
- MySQL查詢返回的亂序userList:[101, 103, 105]。
我們逐一遍歷userList中的每個(gè)用戶,計(jì)算排序依據(jù),再排序:
- 用戶101:
ids.indexOf(101)→ 下標(biāo)是1; - 用戶103:
ids.indexOf(103)→ 下標(biāo)是0; - 用戶105:
ids.indexOf(105)→ 下標(biāo)是2。
排序規(guī)則:按照「下標(biāo)數(shù)字從小到大」排序,所以最終排序后的順序是:
下標(biāo)0(103)→ 下標(biāo)1(101)→ 下標(biāo)2(105),和Redis的正確順序完全一致!
3. 一句話總結(jié)這段代碼的作用
「讓MySQL查出來的亂序用戶,按照Redis給出的正確順序,重新排隊(duì),還原我們想要的排序規(guī)則(比如點(diǎn)贊時(shí)間順序)」。
六、拓展方案:在MyBatis中直接排序(無需Java內(nèi)存排序)
如果你不想用Java代碼排序,也可以在MySQL層面直接強(qiáng)制排序,讓MySQL返回正確順序的結(jié)果,這種方式適合對(duì)SQL熟悉的開發(fā)者,同樣通用。
1. Mapper接口(通用版)
/**
* 根據(jù)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>;
<!-- 關(guān)鍵:強(qiáng)制按照傳入的ID順序排序 -->
ORDER BY FIELD(id,
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</select>3. 核心說明
ORDER BY FIELD(id, 103, 101, 105)是MySQL的專用語法,作用是「強(qiáng)制按照括號(hào)內(nèi)的ID順序返回結(jié)果」,括號(hào)內(nèi)的ID順序就是我們從Redis獲取的正確順序。
優(yōu)點(diǎn):查詢結(jié)果直接有序,無需Java代碼額外處理;缺點(diǎn):SQL復(fù)雜度略有提升,且僅適用于MySQL數(shù)據(jù)庫。
七、總結(jié)(通用,所有開發(fā)者必看)
1. 問題共性
只要用「Redis ZSet排序 + MySQL IN查詢詳細(xì)數(shù)據(jù)」,就一定會(huì)遇到「順序亂掉」的問題,這不是Redis或MySQL的bug,而是兩者的職責(zé)差異導(dǎo)致的。
2. 核心解決方案(優(yōu)先推薦)
用Java Stream的sorted(Comparator.comparing(user -> ids.indexOf(user.getId()))) ,在內(nèi)存中修復(fù)順序,通用、簡單、無性能損耗,直接復(fù)制可用。
3. 關(guān)鍵提醒
- 不要誤以為MySQL的IN查詢會(huì)按傳入順序返回,這是很多開發(fā)者的常見誤區(qū);
- 排序代碼不操作數(shù)據(jù)庫,僅內(nèi)存排序,不用擔(dān)心性能問題;
- 不管你做的是點(diǎn)贊、熱門內(nèi)容、操作記錄等場景,只要涉及「Redis排序+MySQL查詢」,這個(gè)解決方案都能直接復(fù)用。
最后,希望這篇博客能幫到所有遇到同類問題的開發(fā)者,避免踩坑,高效解決亂序問題。
以上就是Redis排序后MySQL查詢亂序問題的原因及解決方法的詳細(xì)內(nèi)容,更多關(guān)于Redis排序后MySQL查詢亂序的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性
這篇文章主要介紹了如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性,文中舉了兩個(gè)場景例子介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
Redis 跳表(Skip List)原理實(shí)現(xiàn)
跳表是zset有序集合的底層實(shí)現(xiàn)之一,本文主要介紹了Redis 跳表(Skip List)原理實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04

