Compare commits
2 Commits
e0cf96edbf
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 94729e5170 | |||
| 6e0f226b42 |
1
.idea/dictionaries/project.xml
generated
1
.idea/dictionaries/project.xml
generated
@@ -9,6 +9,7 @@
|
||||
<w>mget</w>
|
||||
<w>nacos</w>
|
||||
<w>operationlog</w>
|
||||
<w>rbitmap</w>
|
||||
<w>rustfs</w>
|
||||
<w>zadd</w>
|
||||
<w>zrevrangebyscore</w>
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.hanserwei.hannote.comment.biz.consumer;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.comment.biz.constants.MQConstants;
|
||||
import com.hanserwei.hannote.comment.biz.constants.RedisKeyConstants;
|
||||
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
|
||||
import com.hanserwei.hannote.comment.biz.domain.mapper.CommentDOMapper;
|
||||
import com.hanserwei.hannote.comment.biz.domain.mapper.NoteCountDOMapper;
|
||||
import com.hanserwei.hannote.comment.biz.enums.CommentLevelEnum;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.client.producer.SendCallback;
|
||||
import org.apache.rocketmq.client.producer.SendResult;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
@Component
|
||||
@Slf4j
|
||||
@RocketMQMessageListener(consumerGroup = "han_note_group_" + MQConstants.TOPIC_DELETE_COMMENT, // Group
|
||||
topic = MQConstants.TOPIC_DELETE_COMMENT // 消费的主题 Topic
|
||||
)
|
||||
public class DeleteCommentConsumer implements RocketMQListener<String> {
|
||||
|
||||
// 每秒创建 1000 个令牌
|
||||
private final RateLimiter rateLimiter = RateLimiter.create(1000);
|
||||
@Resource
|
||||
private CommentDOMapper commentDOMapper;
|
||||
@Resource
|
||||
private NoteCountDOMapper noteCountDOMapper;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Override
|
||||
public void onMessage(String body) {
|
||||
// 令牌桶流控
|
||||
rateLimiter.acquire();
|
||||
|
||||
log.info("## 【删除评论 - 后续业务处理】消费者消费成功, body: {}", body);
|
||||
|
||||
CommentDO commentDO = JsonUtils.parseObject(body, CommentDO.class);
|
||||
|
||||
// 评论级别
|
||||
Integer level = null;
|
||||
if (commentDO != null) {
|
||||
level = commentDO.getLevel();
|
||||
}
|
||||
|
||||
CommentLevelEnum commentLevelEnum = CommentLevelEnum.valueOf(level);
|
||||
|
||||
if (commentLevelEnum != null) {
|
||||
switch (commentLevelEnum) {
|
||||
case ONE -> // 一级评论
|
||||
handleOneLevelComment(commentDO);
|
||||
case TWO -> // 二级评论
|
||||
handleTwoLevelComment(commentDO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一级评论处理
|
||||
*
|
||||
* @param commentDO 评论
|
||||
*/
|
||||
private void handleOneLevelComment(CommentDO commentDO) {
|
||||
Long commentId = commentDO.getId();
|
||||
Long noteId = commentDO.getNoteId();
|
||||
|
||||
// 1. 关联评论删除(一级评论下所有子评论,都需要删除)
|
||||
int count = commentDOMapper.deleteByParentId(commentId);
|
||||
|
||||
// 2. 计数更新(笔记下总评论数)
|
||||
// 更新 Redis 缓存
|
||||
String redisKey = RedisKeyConstants.buildNoteCommentTotalKey(noteId);
|
||||
boolean hasKey = redisTemplate.hasKey(redisKey);
|
||||
|
||||
if (hasKey) {
|
||||
// 笔记评论总数 -1
|
||||
redisTemplate.opsForHash().increment(redisKey, RedisKeyConstants.FIELD_COMMENT_TOTAL, -(count + 1));
|
||||
}
|
||||
|
||||
// 更新 t_note_count 计数表
|
||||
noteCountDOMapper.updateCommentTotalByNoteId(noteId, -(count + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 二级评论处理
|
||||
*
|
||||
* @param commentDO 评论
|
||||
*/
|
||||
private void handleTwoLevelComment(CommentDO commentDO) {
|
||||
Long commentId = commentDO.getId();
|
||||
|
||||
// 1. 批量删除关联评论(递归查询回复评论,并批量删除)
|
||||
List<Long> replyCommentIds = Lists.newArrayList();
|
||||
recurrentGetReplyCommentId(replyCommentIds, commentId);
|
||||
|
||||
// 被删除的行数
|
||||
int count = 0;
|
||||
if (CollUtil.isNotEmpty(replyCommentIds)) {
|
||||
count = commentDOMapper.deleteByIds(replyCommentIds);
|
||||
}
|
||||
|
||||
// 2. 更新一级评论的计数
|
||||
Long parentCommentId = commentDO.getParentId();
|
||||
String redisKey = RedisKeyConstants.buildCountCommentKey(parentCommentId);
|
||||
|
||||
boolean hasKey = redisTemplate.hasKey(redisKey);
|
||||
if (hasKey) {
|
||||
redisTemplate.opsForHash().increment(redisKey, RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, -(count + 1));
|
||||
}
|
||||
|
||||
// 3. 若是最早的发布的二级评论被删除,需要更新一级评论的 first_reply_comment_id
|
||||
|
||||
// 查询一级评论
|
||||
CommentDO oneLevelCommentDO = commentDOMapper.selectById(parentCommentId);
|
||||
Long firstReplyCommentId = oneLevelCommentDO.getFirstReplyCommentId();
|
||||
|
||||
// 若删除的是最早回复的二级评论
|
||||
if (Objects.equals(firstReplyCommentId, commentId)) {
|
||||
// 查询数据库,重新获取一级评论最早回复的评论
|
||||
CommentDO earliestCommentDO = commentDOMapper.selectEarliestByParentId(parentCommentId);
|
||||
|
||||
// 最早回复的那条评论 ID。若查询结果为 null, 则最早回复的评论 ID 为 null
|
||||
Long earliestCommentId = Objects.nonNull(earliestCommentDO) ? earliestCommentDO.getId() : null;
|
||||
// 更新其一级评论的 first_reply_comment_id
|
||||
commentDOMapper.updateFirstReplyCommentIdByPrimaryKey(earliestCommentId, parentCommentId);
|
||||
}
|
||||
|
||||
// 4. 重新计算一级评论的热度值
|
||||
Set<Long> commentIds = Sets.newHashSetWithExpectedSize(1);
|
||||
commentIds.add(parentCommentId);
|
||||
|
||||
// 异步发送计数 MQ, 更新评论热度值
|
||||
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(commentIds))
|
||||
.build();
|
||||
|
||||
// 异步发送 MQ 消息
|
||||
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COMMENT_HEAT_UPDATE, message, new SendCallback() {
|
||||
@Override
|
||||
public void onSuccess(SendResult sendResult) {
|
||||
log.info("==> 【评论热度值更新】MQ 发送成功,SendResult: {}", sendResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Throwable throwable) {
|
||||
log.error("==> 【评论热度值更新】MQ 发送异常: ", throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取全部回复的评论 ID
|
||||
*
|
||||
* @param commentIds 评论 ID 列表
|
||||
* @param commentId 评论 ID
|
||||
*/
|
||||
private void recurrentGetReplyCommentId(List<Long> commentIds, Long commentId) {
|
||||
CommentDO replyCommentDO = commentDOMapper.selectByReplyCommentId(commentId);
|
||||
|
||||
if (Objects.isNull(replyCommentDO)) return;
|
||||
|
||||
commentIds.add(replyCommentDO.getId());
|
||||
Long replyCommentId = replyCommentDO.getId();
|
||||
// 递归调用
|
||||
recurrentGetReplyCommentId(commentIds, replyCommentId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.hanserwei.hannote.comment.biz.consumer;
|
||||
|
||||
import com.hanserwei.hannote.comment.biz.constants.MQConstants;
|
||||
import com.hanserwei.hannote.comment.biz.service.CommentService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.annotation.MessageModel;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RocketMQMessageListener(consumerGroup = "han_note_group_" + MQConstants.TOPIC_DELETE_COMMENT_LOCAL_CACHE, // Group
|
||||
topic = MQConstants.TOPIC_DELETE_COMMENT_LOCAL_CACHE, // 消费的主题 Topic
|
||||
messageModel = MessageModel.BROADCASTING) // 广播模式
|
||||
public class DeleteCommentLocalCacheConsumer implements RocketMQListener<String> {
|
||||
|
||||
@Resource
|
||||
private CommentService commentService;
|
||||
|
||||
@Override
|
||||
public void onMessage(String body) {
|
||||
Long commentId = Long.valueOf(body);
|
||||
log.info("## 消费者消费成功, commentId: {}", commentId);
|
||||
|
||||
commentService.deleteCommentLocalCache(commentId);
|
||||
}
|
||||
}
|
||||
@@ -120,4 +120,30 @@ public interface CommentDOMapper extends BaseMapper<CommentDO> {
|
||||
*/
|
||||
List<CommentDO> selectChildCommentsByParentIdAndLimit(@Param("parentId") Long parentId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 删除一级评论下,所有二级评论
|
||||
*
|
||||
* @param commentId 一级评论 ID
|
||||
* @return 删除数量
|
||||
*/
|
||||
int deleteByParentId(Long commentId);
|
||||
|
||||
/**
|
||||
* 批量删除评论
|
||||
*
|
||||
* @param commentIds 评论 ID 列表
|
||||
* @return 删除数量
|
||||
*/
|
||||
int deleteByIds(@Param("commentIds") List<Long> commentIds);
|
||||
|
||||
|
||||
/**
|
||||
* 根据 reply_comment_id 查询
|
||||
*
|
||||
* @param commentId 回复的评论 ID
|
||||
* @return 评论
|
||||
*/
|
||||
CommentDO selectByReplyCommentId(Long commentId);
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.hanserwei.hannote.comment.biz.domain.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.hanserwei.hannote.comment.biz.domain.dataobject.NoteCountDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface NoteCountDOMapper extends BaseMapper<NoteCountDO> {
|
||||
@@ -14,4 +15,14 @@ public interface NoteCountDOMapper extends BaseMapper<NoteCountDO> {
|
||||
* @return 笔记评论总数
|
||||
*/
|
||||
Long selectCommentTotalByNoteId(Long noteId);
|
||||
|
||||
/**
|
||||
* 更新评论总数
|
||||
*
|
||||
* @param noteId 笔记 ID
|
||||
* @param count 评论总数
|
||||
* @return 更新数量
|
||||
*/
|
||||
int updateCommentTotalByNoteId(@Param("noteId") Long noteId,
|
||||
@Param("count") int count);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.hanserwei.hannote.comment.biz.enums;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CommentLevelEnum {
|
||||
@@ -14,4 +16,18 @@ public enum CommentLevelEnum {
|
||||
|
||||
private final Integer code;
|
||||
|
||||
/**
|
||||
* 根据类型 code 获取对应的枚举
|
||||
*
|
||||
* @param code 类型 code
|
||||
* @return 枚举
|
||||
*/
|
||||
public static CommentLevelEnum valueOf(Integer code) {
|
||||
for (CommentLevelEnum commentLevelEnum : CommentLevelEnum.values()) {
|
||||
if (Objects.equals(code, commentLevelEnum.getCode())) {
|
||||
return commentLevelEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -54,4 +54,11 @@ public interface CommentService extends IService<CommentDO> {
|
||||
* @return 响应
|
||||
*/
|
||||
Response<?> deleteComment(DeleteCommentReqVO deleteCommentReqVO);
|
||||
|
||||
/**
|
||||
* 删除本地评论缓存
|
||||
*
|
||||
* @param commentId 评论ID
|
||||
*/
|
||||
void deleteCommentLocalCache(Long commentId);
|
||||
}
|
||||
|
||||
@@ -718,6 +718,11 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteCommentLocalCache(Long commentId) {
|
||||
LOCAL_CACHE.invalidate(commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化评论点赞布隆过滤器
|
||||
*
|
||||
|
||||
@@ -201,4 +201,26 @@
|
||||
order by create_time
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<delete id="deleteByParentId" parameterType="long">
|
||||
delete
|
||||
from t_comment
|
||||
where parent_id = #{commentId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByIds" parameterType="map">
|
||||
delete
|
||||
from t_comment
|
||||
where id in
|
||||
<foreach collection="commentIds" item="commentId" open="(" separator="," close=")">
|
||||
#{commentId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<select id="selectByReplyCommentId" resultMap="BaseResultMap" parameterType="long">
|
||||
select
|
||||
<include refid="Base_Column_List"/>
|
||||
from t_comment
|
||||
where reply_comment_id = #{commentId}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -20,4 +20,10 @@
|
||||
from t_note_count
|
||||
where note_id = #{noteId}
|
||||
</select>
|
||||
|
||||
<update id="updateCommentTotalByNoteId" parameterType="map">
|
||||
update t_note_count
|
||||
set comment_total = comment_total + #{count}
|
||||
where note_id = #{noteId}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -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<String> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<NoteDOMapper, NoteDO> implement
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
@Resource
|
||||
private NoteLikeDOMapper noteLikeDOMapper;
|
||||
|
||||
/**
|
||||
* 笔记详情本地缓存
|
||||
@@ -630,14 +633,17 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
// 2. 判断目标笔记,是否已经点赞过
|
||||
Long userId = LoginUserContextHolder.getUserId();
|
||||
|
||||
// 布隆过滤器Key
|
||||
String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
|
||||
// Roaring Bitmap Key
|
||||
String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId);
|
||||
|
||||
DefaultRedisScript<Long> 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<NoteDOMapper, NoteDO> 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<NoteDOMapper, NoteDO> 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<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectList(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
||||
.eq(NoteLikeDO::getUserId, userId));
|
||||
|
||||
if (CollUtil.isNotEmpty(noteLikeDOS)) {
|
||||
DefaultRedisScript<Long> 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<Object> luaArgs = Lists.newArrayList();
|
||||
noteLikeDOS.forEach(noteLikeDO -> luaArgs.add(noteLikeDO.getNoteId())); // 将每个点赞的笔记 ID 传入
|
||||
luaArgs.add(expireSeconds); // 最后一个参数是过期时间(秒)
|
||||
redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), luaArgs.toArray());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("## 异步初始化【笔记点赞】Roaring Bitmap 异常: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) {
|
||||
// 笔记ID
|
||||
@@ -780,37 +804,38 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
// 当前登录用户ID
|
||||
Long userId = LoginUserContextHolder.getUserId();
|
||||
|
||||
// 布隆过滤器Key
|
||||
String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
|
||||
// Roaring Bitmap Key
|
||||
String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId);
|
||||
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
// Lua 脚本路径
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua")));
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_note_unlike_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);
|
||||
|
||||
NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);
|
||||
log.info("==> 【笔记取消点赞】Lua 脚本返回结果: {}", noteUnlikeLuaResultEnum);
|
||||
switch (Objects.requireNonNull(noteUnlikeLuaResultEnum)) {
|
||||
// 布隆过滤器不存在
|
||||
case NOT_EXIST -> {//笔记不存在
|
||||
//异步初始化布隆过滤器
|
||||
// 异步初始化 Roaring Bitmap
|
||||
threadPoolTaskExecutor.submit(() -> {
|
||||
// 保底1天+随机秒数
|
||||
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
||||
batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
|
||||
batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey);
|
||||
});
|
||||
|
||||
// 从数据库中校验笔记是否被点赞
|
||||
long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
||||
long count = noteLikeDOMapper.selectCount(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
||||
.eq(NoteLikeDO::getUserId, userId)
|
||||
.eq(NoteLikeDO::getNoteId, noteId)
|
||||
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode()));
|
||||
if (count == 0) {
|
||||
log.info("==> 【笔记取消点赞】用户未点赞该笔记");
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||
}
|
||||
.eq(NoteLikeDO::getNoteId, noteId));
|
||||
|
||||
// 未点赞,无法取消点赞操作,抛出业务异常
|
||||
log.info("1111111");
|
||||
if (count == 0) throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||
}
|
||||
// 布隆过滤器校验目标笔记未被点赞(判断绝对正确)
|
||||
case NOTE_NOT_LIKED -> throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||
@@ -820,14 +845,9 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
// 用户点赞列表ZsetKey
|
||||
String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
|
||||
|
||||
// TODO: 后续考虑换掉布隆过滤器。
|
||||
|
||||
Long removed = redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId);
|
||||
|
||||
if (Objects.nonNull(removed) && removed == 0) {
|
||||
log.info("==> 【笔记取消点赞】用户未点赞该笔记");
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||
}
|
||||
|
||||
//4. 发送 MQ, 数据更新落库
|
||||
// 构建MQ消息体
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 操作的 Key
|
||||
local key = KEYS[1]
|
||||
local noteId = ARGV[1] -- 笔记ID
|
||||
local expireSeconds = ARGV[2] -- 过期时间(秒)
|
||||
|
||||
redis.call("R64.SETBIT", key, noteId, 1)
|
||||
|
||||
-- 设置过期时间
|
||||
redis.call("EXPIRE", key, expireSeconds)
|
||||
return 0
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 操作的 Key
|
||||
local key = KEYS[1]
|
||||
|
||||
for i = 1, #ARGV - 1 do
|
||||
redis.call("R64.SETBIT", key, ARGV[i], 1)
|
||||
end
|
||||
|
||||
-- 最后一个参数为过期时间
|
||||
local expireTime = ARGV[#ARGV]
|
||||
-- 设置过期时间
|
||||
redis.call("EXPIRE", key, expireTime)
|
||||
return 0
|
||||
@@ -0,0 +1,20 @@
|
||||
-- LUA 脚本:点赞 Roaring Bitmap
|
||||
|
||||
local key = KEYS[1] -- 操作的 Redis Key
|
||||
local noteId = ARGV[1] -- 笔记ID
|
||||
|
||||
-- 使用 EXISTS 命令检查 Roaring Bitmap 是否存在
|
||||
local exists = redis.call('EXISTS', key)
|
||||
if exists == 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 校验该篇笔记是否被点赞过(1 表示已经点赞,0 表示未点赞)
|
||||
local isLiked = redis.call('R64.GETBIT', key, noteId)
|
||||
if isLiked == 1 then
|
||||
return 1
|
||||
end
|
||||
|
||||
-- 未被点赞,添加点赞数据
|
||||
redis.call('R64.SETBIT', key, noteId, 1)
|
||||
return 0
|
||||
@@ -0,0 +1,17 @@
|
||||
local key = KEYS[1] -- 操作的 Redis Key
|
||||
local noteId = ARGV[1] -- 笔记ID
|
||||
|
||||
-- 使用 EXISTS 命令检查 Roaring Bitmap 是否存在
|
||||
local exists = redis.call('EXISTS', key)
|
||||
if exists == 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 校验该篇笔记是否被点赞过(1 表示已经点赞,0 表示未点赞)
|
||||
local isLiked = redis.call('R64.GETBIT', key, noteId)
|
||||
if isLiked == 0 then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- 取消点赞,设置 Value 为 0
|
||||
return redis.call('R64.SETBIT', key, noteId, 0)
|
||||
@@ -202,16 +202,16 @@ Content-Type: application/json
|
||||
Authorization: Bearer {{thirdToken}}
|
||||
|
||||
{
|
||||
"id": 1981698494959714362
|
||||
"id": 1985254482941837349
|
||||
}
|
||||
|
||||
### 笔记取消点赞入口
|
||||
POST http://localhost:8000/note/note/unlike
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{otherToken}}
|
||||
Authorization: Bearer {{thirdToken}}
|
||||
|
||||
{
|
||||
"id": 1977249693272375330
|
||||
"id": 1985254482941837349
|
||||
}
|
||||
|
||||
### 笔记收藏入口
|
||||
|
||||
Reference in New Issue
Block a user