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构建方法及相关常量定义
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -604,15 +610,159 @@ 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 userNoteLikeZSetKey 用户点赞笔记 ZSet KEY
|
||||||
|
*/
|
||||||
|
private void asynInitUserNoteLikesZSet(Long userId, String userNoteLikeZSetKey) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步批量添加笔记点赞记录到布隆过滤器中,并设置过期时间
|
* 异步批量添加笔记点赞记录到布隆过滤器中,并设置过期时间
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user