From 94729e517018e5563b4d70b44d3d1fd4766c0def Mon Sep 17 00:00:00 2001
From: Hanserwei <2628273921@qq.com>
Date: Sun, 9 Nov 2025 22:09:23 +0800
Subject: [PATCH] =?UTF-8?q?refactor(note):=E4=BC=98=E5=8C=96=E7=AC=94?=
=?UTF-8?q?=E8=AE=B0=E7=82=B9=E8=B5=9E=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BD=BF?=
=?UTF-8?q?=E7=94=A8=20Roaring=20Bitmap=20=E6=9B=BF=E4=BB=A3=E5=B8=83?=
=?UTF-8?q?=E9=9A=86=E8=BF=87=E6=BB=A4=E5=99=A8=20-=20=E4=BF=AE=E6=94=B9?=
=?UTF-8?q?=E6=B6=88=E8=B4=B9=E8=80=85=E7=BB=84=E5=90=8D=E7=A7=B0=EF=BC=8C?=
=?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83=20-=20?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=20HTTP=20=E5=AE=A2=E6=88=B7=E7=AB=AF?=
=?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E4=B8=AD=E7=9A=84=E6=8E=88?=
=?UTF-8?q?=E6=9D=83=E4=BB=A4=E7=89=8C=E5=92=8C=E7=AC=94=E8=AE=B0=20ID=20-?=
=?UTF-8?q?=20=E5=BC=95=E5=85=A5=20NoteLikeDOMapper=20=E5=B9=B6=E6=9B=BF?=
=?UTF-8?q?=E6=8D=A2=E5=8E=9F=E6=9C=89=E7=9A=84=20service=20=E6=9F=A5?=
=?UTF-8?q?=E8=AF=A2=E6=96=B9=E5=BC=8F=20-=20=E5=B0=86=E5=B8=83=E9=9A=86?=
=?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=99=A8=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91?=
=?UTF-8?q?=E5=85=A8=E9=83=A8=E6=9B=BF=E6=8D=A2=E4=B8=BA=20Roaring=20Bitma?=
=?UTF-8?q?p=20=E5=AE=9E=E7=8E=B0=20-=20=E6=96=B0=E5=A2=9E=E5=A4=9A?=
=?UTF-8?q?=E4=B8=AA=20Lua=20=E8=84=9A=E6=9C=AC=E6=94=AF=E6=8C=81=20Roarin?=
=?UTF-8?q?g=20Bitmap=20=E7=9A=84=E6=93=8D=E4=BD=9C=E4=B8=8E=E5=88=9D?=
=?UTF-8?q?=E5=A7=8B=E5=8C=96=20-=20=E6=B7=BB=E5=8A=A0=20Roaring=20Bitmap?=
=?UTF-8?q?=20=E7=9B=B8=E5=85=B3=E7=9A=84=20Redis=20Key=20=E6=9E=84?=
=?UTF-8?q?=E5=BB=BA=E6=96=B9=E6=B3=95=20-=20=E5=88=A0=E9=99=A4=E6=97=A7?=
=?UTF-8?q?=E6=9C=89=E7=9A=84=E5=B8=83=E9=9A=86=E8=BF=87=E6=BB=A4=E5=99=A8?=
=?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91=E5=8F=8A=E5=86=97=E4=BD=99?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81=20-=20=E6=9B=B4=E6=96=B0=20Redis=20Key=20?=
=?UTF-8?q?=E5=B8=B8=E9=87=8F=E7=B1=BB=EF=BC=8C=E5=A2=9E=E5=8A=A0=20Roarin?=
=?UTF-8?q?g=20Bitmap=20=E7=9B=B8=E5=85=B3=E5=AE=9A=E4=B9=89=20-=20?=
=?UTF-8?q?=E6=97=A5=E5=BF=97=E5=AD=97=E5=85=B8=E6=96=87=E4=BB=B6=E4=B8=AD?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=20rbitmap=20=E5=85=B3=E9=94=AE=E8=AF=8D=20-?=
=?UTF-8?q?=20=E4=BC=98=E5=8C=96=E7=82=B9=E8=B5=9E=E5=92=8C=E5=8F=96?=
=?UTF-8?q?=E6=B6=88=E7=82=B9=E8=B5=9E=E6=B5=81=E7=A8=8B=EF=BC=8C=E6=8F=90?=
=?UTF-8?q?=E5=8D=87=E6=80=A7=E8=83=BD=E4=B8=8E=E5=87=86=E7=A1=AE=E6=80=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.idea/dictionaries/project.xml | 1 +
.../biz/consumer/CountNoteLikeConsumer.java | 2 +-
.../note/biz/constant/RedisKeyConstants.java | 15 +++
.../biz/service/impl/NoteServiceImpl.java | 108 +++++++++++-------
.../lua/rbitmap_add_note_like_and_expire.lua | 10 ++
...rbitmap_batch_add_note_like_and_expire.lua | 12 ++
.../resources/lua/rbitmap_note_like_check.lua | 20 ++++
.../lua/rbitmap_note_unlike_check.lua | 17 +++
http-client/gateApi.http | 6 +-
9 files changed, 143 insertions(+), 48 deletions(-)
create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_add_note_like_and_expire.lua
create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_batch_add_note_like_and_expire.lua
create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_like_check.lua
create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_unlike_check.lua
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 5bc28bd..ab5fea2 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -9,6 +9,7 @@
mget
nacos
operationlog
+ rbitmap
rustfs
zadd
zrevrangebyscore
diff --git a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java
index 12c7a29..e66fe13 100644
--- a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java
+++ b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
@Component
@Slf4j
@RocketMQMessageListener(
- consumerGroup = "han_note_group_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
+ consumerGroup = "han_note_count_group_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
topic = MQConstants.TOPIC_LIKE_OR_UNLIKE
)
public class CountNoteLikeConsumer implements RocketMQListener {
diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java
index c4eadb5..0927ede 100644
--- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java
+++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java
@@ -7,6 +7,11 @@ public class RedisKeyConstants {
*/
public static final String NOTE_DETAIL_KEY = "note:detail:";
+ /**
+ * Roaring Bitmap:用户笔记点赞 前缀
+ */
+ public static final String R_BITMAP_USER_NOTE_LIKE_LIST_KEY = "rbitmap:note:likes:";
+
/**
* 布隆过滤器:用户笔记点赞
*/
@@ -76,4 +81,14 @@ public class RedisKeyConstants {
public static String buildUserNoteCollectZSetKey(Long userId) {
return USER_NOTE_COLLECT_ZSET_KEY + userId;
}
+
+ /**
+ * 构建完整的 Roaring Bitmap:用户笔记点赞 KEY
+ *
+ * @param userId 用户ID
+ * @return Roaring Bitmap:用户笔记点赞 KEY
+ */
+ public static String buildRBitmapUserNoteLikeListKey(Long userId) {
+ return R_BITMAP_USER_NOTE_LIKE_LIST_KEY + userId;
+ }
}
\ No newline at end of file
diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java
index d2d0f1f..f7ad016 100644
--- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java
+++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java
@@ -21,6 +21,7 @@ import com.hanserwei.hannote.note.biz.domain.dataobject.NoteDO;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
import com.hanserwei.hannote.note.biz.domain.dataobject.TopicDO;
import com.hanserwei.hannote.note.biz.domain.mapper.NoteDOMapper;
+import com.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper;
import com.hanserwei.hannote.note.biz.enums.*;
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
@@ -76,6 +77,8 @@ public class NoteServiceImpl extends ServiceImpl implement
private RedisTemplate redisTemplate;
@Resource
private RocketMQTemplate rocketMQTemplate;
+ @Resource
+ private NoteLikeDOMapper noteLikeDOMapper;
/**
* 笔记详情本地缓存
@@ -630,14 +633,17 @@ public class NoteServiceImpl extends ServiceImpl implement
// 2. 判断目标笔记,是否已经点赞过
Long userId = LoginUserContextHolder.getUserId();
- // 布隆过滤器Key
- String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
+ // Roaring Bitmap Key
+ String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId);
+
DefaultRedisScript script = new DefaultRedisScript<>();
// Lua 脚本路径
- script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_like_check.lua")));
+ script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_note_like_check.lua")));
+ // 返回值类型
script.setResultType(Long.class);
+
// 执行 Lua 脚本,拿到返回结果
- Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
+ Long result = redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId);
NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);
@@ -659,38 +665,25 @@ public class NoteServiceImpl extends ServiceImpl implement
// 目标笔记已经被点赞
if (count > 0) {
- // 异步初始化布隆过滤器
- threadPoolTaskExecutor.submit(() -> batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey));
+ // 异步初始化 Roaring Bitmap
+ threadPoolTaskExecutor.submit(() ->
+ batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey));
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
}
- // 若笔记未被点赞,查询当前用户是否点赞其他用户,有则同步初始化布隆过滤器
- batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
+ // 若目标笔记未被点赞,查询当前用户是否有点赞其他笔记,有则同步初始化 Roaring Bitmap
+ batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey);
- // 若数据库中也没有点赞记录,说明该用户还未点赞过任何笔记
+ // 添加当前点赞笔记 ID 到 Roaring Bitmap 中
// Lua 脚本路径
- script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_like_and_expire.lua")));
+ script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_add_note_like_and_expire.lua")));
// 返回值类型
script.setResultType(Long.class);
- redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId, expireSeconds);
+ redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId, expireSeconds);
}
// 目标笔记已经被点赞
case NOTE_LIKED -> {
- // 校验 ZSet 列表中是否包含被点赞的笔记ID
- Double score = redisTemplate.opsForZSet().score(userNoteLikeZSetKey, noteId);
-
- if (Objects.nonNull(score)) {
- throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
- }
- // 若 Score 为空,则表示 ZSet 点赞列表中不存在,查询数据库校验
- long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class)
- .eq(NoteLikeDO::getNoteId, noteId)
- .eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode()));
- if (count > 0) {
- // 数据库里面有点赞记录,而 Redis 中 ZSet 不存在,需要重新异步初始化 ZSet
- asynInitUserNoteLikesZSet(userId, userNoteLikeZSetKey);
- throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
- }
+ throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
}
}
// 3. 更新用户 ZSET 点赞列表
@@ -768,6 +761,37 @@ public class NoteServiceImpl extends ServiceImpl implement
return Response.success();
}
+ /**
+ * 初始化笔记点赞 Roaring Bitmap
+ *
+ * @param userId 用户 ID
+ * @param expireSeconds 过期时间
+ * @param rbitmapUserNoteLikeListKey RBitmap 列表 Key
+ */
+ private void batchAddNoteLike2RBitmapAndExpire(Long userId, long expireSeconds, String rbitmapUserNoteLikeListKey) {
+ try {
+ // 异步全量同步一下,并设置过期时间
+ List noteLikeDOS = noteLikeDOMapper.selectList(new LambdaQueryWrapper<>(NoteLikeDO.class)
+ .eq(NoteLikeDO::getUserId, userId));
+
+ if (CollUtil.isNotEmpty(noteLikeDOS)) {
+ DefaultRedisScript script = new DefaultRedisScript<>();
+ // Lua 脚本路径
+ script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_batch_add_note_like_and_expire.lua")));
+ // 返回值类型
+ script.setResultType(Long.class);
+
+ // 构建 Lua 参数
+ List