Compare commits

..

2 Commits

Author SHA1 Message Date
bb44cd3d23 bugFix: (note)
- Bloom 布隆过滤器不存在时,未校验是否点赞其他笔记
2025-10-17 22:20:20 +08:00
7c92bd91f6 feat(note): 实现笔记点赞功能及Redis ZSet同步
- 新增笔记点赞MQ消费者LikeUnlikeNoteConsumer,支持点赞与取消点赞操作
- 添加LikeUnlikeNoteMqDTO数据传输对象和LikeUnlikeNoteTypeEnum枚举类
- 扩展NoteLikeDO实体类使用LocalDateTime并调整status字段类型为Integer
- 实现NoteLikeDOMapper的insertOrUpdate方法支持插入或更新点赞记录- 新增两个Lua脚本用于批量添加点赞记录及检查更新用户点赞ZSet-优化NoteServiceImpl中的点赞逻辑,增强幂等性校验和ZSet初始化机制
- 引入Redis ZSet维护用户最近100条点赞记录,并支持过期时间设置
- 调整MQ常量定义,增加点赞相关主题与标签配置
- 迁移DateUtils工具类至公共模块并修复相关引用路径
- 增加用户笔记点赞列表ZSet的Key构建方法及相关常量定义
2025-10-17 22:07:48 +08:00
15 changed files with 415 additions and 30 deletions

View File

@@ -0,0 +1,100 @@
package com.hanserwei.hannote.note.biz.comsumer;
import com.google.common.util.concurrent.RateLimiter;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.note.biz.constant.MQConstants;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
import com.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper;
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Objects;
@SuppressWarnings("UnstableApiUsage")
@Component
@RocketMQMessageListener(
consumerGroup = "han_note_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
topic = MQConstants.TOPIC_LIKE_OR_UNLIKE,
consumeMode = ConsumeMode.ORDERLY// 顺序消费
)
@Slf4j
public class LikeUnlikeNoteConsumer implements RocketMQListener<Message> {
// 每秒创建 5000 个令牌
private final RateLimiter rateLimiter = RateLimiter.create(5000);
@Resource
private NoteLikeDOMapper noteLikeDOMapper;
@Override
public void onMessage(Message message) {
// 流量削峰:通过获取令牌,如果没有令牌可用,将阻塞,直到获得
rateLimiter.acquire();
// 幂等性,通过联合索引保证
// 消息体
String bodyJsonStr = new String(message.getBody());
// 标签
String tags = message.getTags();
log.info("==> LikeUnlikeNoteConsumer 消费了消息 {}, tags: {}", bodyJsonStr, tags);
// 根据 MQ 标签,判断操作类型
if (Objects.equals(tags, MQConstants.TAG_LIKE)) { // 点赞笔记
handleLikeNoteTagMessage(bodyJsonStr);
} else if (Objects.equals(tags, MQConstants.TAG_UNLIKE)) { // 取消点赞笔记
handleUnlikeNoteTagMessage(bodyJsonStr);
}
}
/**
* 处理取消点赞笔记的 MQ 消息
*
* @param bodyJsonStr 消息体
*/
private void handleUnlikeNoteTagMessage(String bodyJsonStr) {
}
/**
* 处理点赞笔记的 MQ 消息
*
* @param bodyJsonStr 消息体
*/
private void handleLikeNoteTagMessage(String bodyJsonStr) {
// 消息体 JSON 字符串转 DTO
LikeUnlikeNoteMqDTO likeNoteMqDTO = JsonUtils.parseObject(bodyJsonStr, LikeUnlikeNoteMqDTO.class);
if (Objects.isNull(likeNoteMqDTO)) return;
// 用户ID
Long userId = likeNoteMqDTO.getUserId();
// 点赞的笔记ID
Long noteId = likeNoteMqDTO.getNoteId();
// 操作类型
Integer type = likeNoteMqDTO.getType();
// 点赞时间
LocalDateTime createTime = likeNoteMqDTO.getCreateTime();
// 构建 DO 对象
NoteLikeDO noteLikeDO = NoteLikeDO.builder()
.userId(userId)
.noteId(noteId)
.createTime(createTime)
.status(type)
.build();
// 添加或更新笔记点赞记录
boolean count = noteLikeDOMapper.insertOrUpdate(noteLikeDO);
// TODO: 发送计数 MQ
}
}

View File

@@ -11,4 +11,19 @@ public interface MQConstants {
* Topic 主题:延迟双删 Redis 笔记缓存 * Topic 主题:延迟双删 Redis 笔记缓存
*/ */
String TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE = "DelayDeleteNoteRedisCacheTopic"; String TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE = "DelayDeleteNoteRedisCacheTopic";
/**
* Topic: 点赞、取消点赞共用一个
*/
String TOPIC_LIKE_OR_UNLIKE = "LikeUnlikeTopic";
/**
* 点赞标签
*/
String TAG_LIKE = "Like";
/**
* Tag 标签:取消点赞
*/
String TAG_UNLIKE = "Unlike";
} }

View File

@@ -12,6 +12,11 @@ public class RedisKeyConstants {
*/ */
public static final String BLOOM_USER_NOTE_LIKE_LIST_KEY = "bloom:note:likes:"; public static final String BLOOM_USER_NOTE_LIKE_LIST_KEY = "bloom:note:likes:";
/**
* 用户笔记点赞列表 ZSet 前缀
*/
public static final String USER_NOTE_LIKE_ZSET_KEY = "user:note:likes:";
/** /**
* 构建完整的笔记详情 KEY * 构建完整的笔记详情 KEY
@@ -32,4 +37,14 @@ public class RedisKeyConstants {
return BLOOM_USER_NOTE_LIKE_LIST_KEY + userId; return BLOOM_USER_NOTE_LIKE_LIST_KEY + userId;
} }
/**
* 构建完整的用户笔记点赞列表 ZSet KEY
*
* @param userId 用户ID
* @return 用户笔记点赞列表 ZSet KEY
*/
public static String buildUserNoteLikeZSetKey(Long userId) {
return USER_NOTE_LIKE_ZSET_KEY + userId;
}
} }

View File

@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/** /**
* 笔记点赞表 * 笔记点赞表
*/ */
@@ -41,11 +42,11 @@ public class NoteLikeDO {
* 创建时间 * 创建时间
*/ */
@TableField(value = "create_time") @TableField(value = "create_time")
private Date createTime; private LocalDateTime createTime;
/** /**
* 点赞状态(0取消点赞 1点赞) * 点赞状态(0取消点赞 1点赞)
*/ */
@TableField(value = "`status`") @TableField(value = "`status`")
private Byte status; private Integer status;
} }

View File

@@ -6,4 +6,11 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
public interface NoteLikeDOMapper extends BaseMapper<NoteLikeDO> { public interface NoteLikeDOMapper extends BaseMapper<NoteLikeDO> {
/**
* 新增笔记点赞记录,若已存在,则更新笔记点赞记录
*
* @param noteLikeDO 笔记点赞记录
* @return 影响行数
*/
boolean insertOrUpdate(NoteLikeDO noteLikeDO);
} }

View File

@@ -0,0 +1,17 @@
package com.hanserwei.hannote.note.biz.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum LikeUnlikeNoteTypeEnum {
// 点赞
LIKE(1),
// 取消点赞
UNLIKE(0),
;
private final Integer code;
}

View File

@@ -9,7 +9,9 @@ import java.util.Objects;
@AllArgsConstructor @AllArgsConstructor
public enum NoteLikeLuaResultEnum { public enum NoteLikeLuaResultEnum {
// 布隆过滤器不存在 // 布隆过滤器不存在
BLOOM_NOT_EXIST(-1L), NOT_EXIST(-1L),
// 笔记点赞成功
NOTE_LIKE_SUCCESS(0L),
// 笔记已点赞 // 笔记已点赞
NOTE_LIKED(1L), NOTE_LIKED(1L),
; ;

View File

@@ -0,0 +1,26 @@
package com.hanserwei.hannote.note.biz.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LikeUnlikeNoteMqDTO {
private Long userId;
private Long noteId;
/**
* 0: 取消点赞, 1点赞
*/
private Integer type;
private LocalDateTime createTime;
}

View File

@@ -3,6 +3,7 @@ package com.hanserwei.hannote.note.biz.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
@@ -11,6 +12,7 @@ import com.google.common.collect.Lists;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder; import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.exception.ApiException; import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response; import com.hanserwei.framework.common.response.Response;
import com.hanserwei.framework.common.utils.DateUtils;
import com.hanserwei.framework.common.utils.JsonUtils; import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.note.biz.constant.MQConstants; import com.hanserwei.hannote.note.biz.constant.MQConstants;
import com.hanserwei.hannote.note.biz.constant.RedisKeyConstants; import com.hanserwei.hannote.note.biz.constant.RedisKeyConstants;
@@ -19,6 +21,7 @@ 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.dataobject.TopicDO;
import com.hanserwei.hannote.note.biz.domain.mapper.NoteDOMapper; import com.hanserwei.hannote.note.biz.domain.mapper.NoteDOMapper;
import com.hanserwei.hannote.note.biz.enums.*; import com.hanserwei.hannote.note.biz.enums.*;
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
import com.hanserwei.hannote.note.biz.model.vo.*; import com.hanserwei.hannote.note.biz.model.vo.*;
import com.hanserwei.hannote.note.biz.rpc.DistributedIdGeneratorRpcService; import com.hanserwei.hannote.note.biz.rpc.DistributedIdGeneratorRpcService;
import com.hanserwei.hannote.note.biz.rpc.KeyValueRpcService; import com.hanserwei.hannote.note.biz.rpc.KeyValueRpcService;
@@ -577,10 +580,13 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result); NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);
// 用户点赞列表 ZSet Key
String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
assert noteLikeLuaResultEnum != null; assert noteLikeLuaResultEnum != null;
switch (noteLikeLuaResultEnum) { switch (noteLikeLuaResultEnum) {
// Redis 中布隆过滤器不存在 // Redis 中布隆过滤器不存在
case BLOOM_NOT_EXIST -> { case NOT_EXIST -> {
// TODO: 从数据库中校验笔记是否被点赞,并异步初始化布隆过滤器,设置过期时间 // TODO: 从数据库中校验笔记是否被点赞,并异步初始化布隆过滤器,设置过期时间
long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class) long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class)
.eq(NoteLikeDO::getNoteId, noteId) .eq(NoteLikeDO::getNoteId, noteId)
@@ -592,10 +598,13 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
// 目标笔记已经被点赞 // 目标笔记已经被点赞
if (count > 0) { if (count > 0) {
// 异步初始化布隆过滤器 // 异步初始化布隆过滤器
asynBatchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey); threadPoolTaskExecutor.submit(() -> batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey));
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED); throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
} }
// 若笔记未被点赞,查询当前用户是否点赞其他用户,有则同步初始化布隆过滤器
batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
// 若数据库中也没有点赞记录,说明该用户还未点赞过任何笔记 // 若数据库中也没有点赞记录,说明该用户还未点赞过任何笔记
// Lua 脚本路径 // Lua 脚本路径
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_like_and_expire.lua"))); script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_like_and_expire.lua")));
@@ -604,27 +613,173 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId, expireSeconds); redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId, expireSeconds);
} }
// 目标笔记已经被点赞 // 目标笔记已经被点赞
case NOTE_LIKED -> throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED); 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);
}
}
} }
// 3. 更新用户 ZSET 点赞列表 // 3. 更新用户 ZSET 点赞列表
LocalDateTime now = LocalDateTime.now();
// lua 脚本路径
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_like_check_and_update_zset.lua")));
// 返回值类型
script.setResultType(Long.class);
// 执行 Lua 脚本,拿到返回结果
result = redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
// 若 ZSet 列表不存在,需要重新初始化
if (Objects.equals(result, NoteLikeLuaResultEnum.NOT_EXIST.getCode())) {
// 查询当前用户最新点赞的100篇笔记
Page<NoteLikeDO> page = noteLikeDOService.page(new Page<>(1, 100),
new LambdaQueryWrapper<>(NoteLikeDO.class)
.select(NoteLikeDO::getNoteId, NoteLikeDO::getCreateTime)
.eq(NoteLikeDO::getUserId, userId)
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode())
.orderByDesc(NoteLikeDO::getCreateTime));
List<NoteLikeDO> noteLikeDOS = page.getRecords();
// 保底1天+随机秒数
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
// Lua 脚本路径
script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
// 返回值类型
script2.setResultType(Long.class);
// 若数据库中存在点赞记录,需要批量同步
if (CollUtil.isNotEmpty(noteLikeDOS)) {
// 构建 Lua 参数
Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expireSeconds);
redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs);
// 再次调用 note_like_check_and_update_zset.lua 脚本,将点赞的笔记添加到 zset 中
redisTemplate.execute(script, Collections.singletonList(userNoteLikeZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
} else { // 若数据库中,无点赞过的笔记记录,则直接将当前点赞的笔记 ID 添加到 ZSet 中,随机过期时间
List<Object> luaArgs = Lists.newArrayList();
luaArgs.add(DateUtils.localDateTime2Timestamp(LocalDateTime.now())); // score :点赞时间戳
luaArgs.add(noteId); // 当前点赞的笔记 ID
luaArgs.add(expireSeconds); // 随机过期时间
redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs.toArray());
}
}
// 4. 发送 MQ, 将点赞数据落库 // 4. 发送 MQ, 将点赞数据落库
// 构建MQ消息体
LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder()
.userId(userId)
.noteId(noteId)
.type(LikeStatusEnum.LIKE.getCode()) // 点赞
.createTime(now)
.build();
// 构建消息将DTO转换为JSON字符串设置到消息体中
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build();
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_LIKE;
String hashKey = String.valueOf(noteId);
// 异步发送 MQ 消息,提升接口响应速度
rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("==> 【笔记点赞】MQ 发送成功SendResult: {}", sendResult);
}
@Override
public void onException(Throwable throwable) {
log.error("==> 【笔记点赞】MQ 发送异常: ", throwable);
}
});
return Response.success(); return Response.success();
} }
/** /**
* 异步批量添加笔记点赞记录到布隆过滤器中,并设置过期时间 * 异步初始化用户点赞笔记 ZSet
* *
* @param userId 用户ID * @param userId 用户ID
* @param expireSeconds 过期时间(秒) * @param userNoteLikeZSetKey 用户点赞笔记 ZSet KEY
* @param bloomUserNoteLikeListKey 布隆过滤器 Key
*/ */
private void asynBatchAddNoteLike2BloomAndExpire(Long userId, long expireSeconds, String bloomUserNoteLikeListKey) { private void asynInitUserNoteLikesZSet(Long userId, String userNoteLikeZSetKey) {
threadPoolTaskExecutor.submit(() -> { threadPoolTaskExecutor.submit(() -> {
// 判断用户笔记点赞 ZSET 是否存在
boolean hasKey = redisTemplate.hasKey(userNoteLikeZSetKey);
// 若不存在,则初始化
if (!hasKey) {
// 查询当前用户最新点赞的100篇笔记
Page<NoteLikeDO> page = noteLikeDOService.page(new Page<>(1, 100),
new LambdaQueryWrapper<>(NoteLikeDO.class)
.select(NoteLikeDO::getNoteId, NoteLikeDO::getCreateTime)
.eq(NoteLikeDO::getUserId, userId)
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode())
.orderByDesc(NoteLikeDO::getCreateTime));
List<NoteLikeDO> noteLikeDOS = page.getRecords();
if (CollUtil.isNotEmpty(noteLikeDOS)) {
// 保底1天+随机秒数
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
// 构建 Lua 参数
Object[] luaArgs = buildNoteLikeZSetLuaArgs(noteLikeDOS, expireSeconds);
DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
// Lua 脚本路径
script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_like_zset_and_expire.lua")));
// 返回值类型
script2.setResultType(Long.class);
redisTemplate.execute(script2, Collections.singletonList(userNoteLikeZSetKey), luaArgs);
}
}
});
}
/**
* 构建 Lua 脚本参数
*
* @param noteLikeDOS 点赞记录
* @param expireSeconds 过期时间(秒)
* @return 参数列表
*/
private Object[] buildNoteLikeZSetLuaArgs(List<NoteLikeDO> noteLikeDOS, long expireSeconds) {
int argsLength = noteLikeDOS.size() * 2 + 1; // 每个笔记点赞关系有 2 个参数score 和 value最后再跟一个过期时间
Object[] luaArgs = new Object[argsLength];
int i = 0;
for (NoteLikeDO noteLikeDO : noteLikeDOS) {
luaArgs[i] = DateUtils.localDateTime2Timestamp(noteLikeDO.getCreateTime()); // 点赞时间作为 score
luaArgs[i + 1] = noteLikeDO.getNoteId(); // 笔记ID 作为 ZSet value
i += 2;
}
luaArgs[argsLength - 1] = expireSeconds; // 最后一个参数是 ZSet 的过期时间
return luaArgs;
}
/**
* 异步初始化布隆过滤器
* @param userId 用户ID
* @param expireSeconds 过期时间(秒)
* @param bloomUserNoteLikeListKey 布隆过滤器 KEY
*/
private void batchAddNoteLike2BloomAndExpire(Long userId, long expireSeconds, String bloomUserNoteLikeListKey) {
try { try {
// 异步全量同步一下,并设置过期时间
List<NoteLikeDO> noteLikeDOS = noteLikeDOService.list(new LambdaQueryWrapper<>(NoteLikeDO.class) List<NoteLikeDO> noteLikeDOS = noteLikeDOService.list(new LambdaQueryWrapper<>(NoteLikeDO.class)
.eq(NoteLikeDO::getUserId, userId)); .select(NoteLikeDO::getNoteId)
.eq(NoteLikeDO::getUserId, userId)
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode()));
if (CollUtil.isNotEmpty(noteLikeDOS)) { if (CollUtil.isNotEmpty(noteLikeDOS)) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(); DefaultRedisScript<Long> script = new DefaultRedisScript<>();
// Lua 脚本路径 // Lua 脚本路径
@@ -639,9 +794,8 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), luaArgs.toArray()); redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), luaArgs.toArray());
} }
} catch (Exception e) { } catch (Exception e) {
log.error("异步批量添加笔记点赞记录到布隆过滤器中,并设置过期时间失败...", e); log.error("## 异步初始化布隆过滤器异常: ", e);
} }
});
} }
/** /**

View File

@@ -0,0 +1,20 @@
-- 操作的 Key
local key = KEYS[1]
-- 准备批量添加数据的参数表
local zaddArgs = {}
-- 遍历 ARGV 参数,将分数和值按顺序插入到 zaddArgs 变量中
for i = 1, #ARGV - 1, 2 do
table.insert(zaddArgs, ARGV[i]) -- 分数(点赞时间)
table.insert(zaddArgs, ARGV[i + 1]) -- 值笔记ID
end
-- 调用 ZADD 批量插入数据
redis.call('ZADD', key, unpack(zaddArgs))
-- 设置 ZSet 的过期时间
local expireTime = ARGV[#ARGV] -- 最后一个参数为过期时间
redis.call('EXPIRE', key, expireTime)
return 0

View File

@@ -0,0 +1,21 @@
local key = KEYS[1] -- Redis Key
local noteId = ARGV[1] -- 笔记ID
local timestamp = ARGV[2] -- 时间戳
-- 使用 EXISTS 命令检查 ZSET 笔记点赞列表是否存在
local exists = redis.call('EXISTS', key)
if exists == 0 then
return -1
end
-- 获取笔记点赞列表大小
local size = redis.call('ZCARD', key)
-- 若已经点赞了 100 篇笔记,则移除最早点赞的那篇
if size >= 100 then
redis.call('ZPOPMIN', key)
end
-- 添加新的笔记点赞关系
redis.call('ZADD', key, timestamp, noteId)
return 0

View File

@@ -14,4 +14,11 @@
<!--@mbg.generated--> <!--@mbg.generated-->
id, user_id, note_id, create_time, `status` id, user_id, note_id, create_time, `status`
</sql> </sql>
<insert id="insertOrUpdate" parameterType="com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO">
INSERT INTO t_note_like (user_id, note_id, create_time, status)
VALUES (#{userId}, #{noteId}, #{createTime}, #{status})
ON DUPLICATE KEY UPDATE
create_time = #{createTime}, status = #{status};
</insert>
</mapper> </mapper>

View File

@@ -2,6 +2,7 @@ package com.hanserwei.hannote.user.relation.biz.consumer;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.RateLimiter;
import com.hanserwei.framework.common.utils.DateUtils;
import com.hanserwei.framework.common.utils.JsonUtils; import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants; import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
import com.hanserwei.hannote.user.relation.biz.constant.RedisKeyConstants; import com.hanserwei.hannote.user.relation.biz.constant.RedisKeyConstants;
@@ -13,7 +14,6 @@ import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO; import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO;
import com.hanserwei.hannote.user.relation.biz.service.FansDOService; import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService; import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
import com.hanserwei.hannote.user.relation.biz.util.DateUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@@ -8,6 +8,7 @@ import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.exception.ApiException; import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.PageResponse; import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.framework.common.response.Response; import com.hanserwei.framework.common.response.Response;
import com.hanserwei.framework.common.utils.DateUtils;
import com.hanserwei.framework.common.utils.JsonUtils; import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.user.dto.req.FindFollowingListReqVO; import com.hanserwei.hannote.user.dto.req.FindFollowingListReqVO;
import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO; import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO;
@@ -28,7 +29,6 @@ import com.hanserwei.hannote.user.relation.biz.rpc.UserRpcService;
import com.hanserwei.hannote.user.relation.biz.service.FansDOService; import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService; import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
import com.hanserwei.hannote.user.relation.biz.service.RelationService; import com.hanserwei.hannote.user.relation.biz.service.RelationService;
import com.hanserwei.hannote.user.relation.biz.util.DateUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendCallback;

View File

@@ -1,4 +1,4 @@
package com.hanserwei.hannote.user.relation.biz.util; package com.hanserwei.framework.common.utils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;