Compare commits
3 Commits
54c34706fb
...
61cfbd6b81
| Author | SHA1 | Date | |
|---|---|---|---|
| 61cfbd6b81 | |||
| 1ac61d1b06 | |||
| 65b089de70 |
@@ -0,0 +1,99 @@
|
||||
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.NoteCollectionDO;
|
||||
import com.hanserwei.hannote.note.biz.domain.mapper.NoteCollectionDOMapper;
|
||||
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
||||
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
|
||||
@Slf4j
|
||||
@RocketMQMessageListener(
|
||||
consumerGroup = "han_note_" + MQConstants.TOPIC_COLLECT_OR_UN_COLLECT,
|
||||
topic = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT,
|
||||
consumeMode = ConsumeMode.ORDERLY
|
||||
)
|
||||
public class CollectUnCollectNoteConsumer implements RocketMQListener<Message> {
|
||||
|
||||
// 每秒创建 5000 个令牌
|
||||
private final RateLimiter rateLimiter = RateLimiter.create(5000);
|
||||
@Resource
|
||||
private NoteCollectionDOMapper noteCollectionDOMapper;
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message) {
|
||||
// 流量削峰:通过获取令牌,如果没有令牌可用,将阻塞,直到获得
|
||||
rateLimiter.acquire();
|
||||
|
||||
// 幂等性: 通过联合唯一索引保证
|
||||
|
||||
// 消息体
|
||||
String bodyJsonStr = new String(message.getBody());
|
||||
// 标签
|
||||
String tags = message.getTags();
|
||||
|
||||
log.info("==> CollectUnCollectNoteConsumer 消费了消息 {}, tags: {}", bodyJsonStr, tags);
|
||||
|
||||
// 根据 MQ 标签,判断操作类型
|
||||
if (Objects.equals(tags, MQConstants.TAG_COLLECT)) { // 收藏笔记
|
||||
handleCollectNoteTagMessage(bodyJsonStr);
|
||||
} else if (Objects.equals(tags, MQConstants.TAG_UN_COLLECT)) { // 取消收藏笔记
|
||||
handleUnCollectNoteTagMessage(bodyJsonStr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理取消收藏笔记的 MQ 消息
|
||||
*
|
||||
* @param bodyJsonStr 消息体
|
||||
*/
|
||||
private void handleUnCollectNoteTagMessage(String bodyJsonStr) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收藏笔记的 MQ 消息
|
||||
*
|
||||
* @param bodyJsonStr 消息体
|
||||
*/
|
||||
private void handleCollectNoteTagMessage(String bodyJsonStr) {
|
||||
// 消息体 JSON 字符串转 DTO
|
||||
CollectUnCollectNoteMqDTO collectUnCollectNoteMqDTO = JsonUtils.parseObject(bodyJsonStr, CollectUnCollectNoteMqDTO.class);
|
||||
|
||||
if (Objects.isNull(collectUnCollectNoteMqDTO)) return;
|
||||
|
||||
// 用户ID
|
||||
Long userId = collectUnCollectNoteMqDTO.getUserId();
|
||||
// 收藏的笔记ID
|
||||
Long noteId = collectUnCollectNoteMqDTO.getNoteId();
|
||||
// 操作类型
|
||||
Integer type = collectUnCollectNoteMqDTO.getType();
|
||||
// 收藏时间
|
||||
LocalDateTime createTime = collectUnCollectNoteMqDTO.getCreateTime();
|
||||
|
||||
// 构建 DO 对象
|
||||
NoteCollectionDO noteCollectionDO = NoteCollectionDO.builder()
|
||||
.userId(userId)
|
||||
.noteId(noteId)
|
||||
.createTime(createTime)
|
||||
.status(type)
|
||||
.build();
|
||||
|
||||
// 添加或更新笔记收藏记录
|
||||
boolean isSuccess = noteCollectionDOMapper.insertOrUpdate(noteCollectionDO);
|
||||
|
||||
// TODO: 发送计数 MQ
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ public interface MQConstants {
|
||||
*/
|
||||
String TOPIC_COUNT_NOTE_LIKE = "CountNoteLikeTopic";
|
||||
|
||||
/**
|
||||
* Topic: 收藏、取消收藏共用一个
|
||||
*/
|
||||
String TOPIC_COLLECT_OR_UN_COLLECT = "CollectUnCollectTopic";
|
||||
|
||||
/**
|
||||
* 点赞标签
|
||||
*/
|
||||
@@ -31,4 +36,14 @@ public interface MQConstants {
|
||||
* Tag 标签:取消点赞
|
||||
*/
|
||||
String TAG_UNLIKE = "Unlike";
|
||||
|
||||
/**
|
||||
* Tag 标签:收藏
|
||||
*/
|
||||
String TAG_COLLECT = "Collect";
|
||||
|
||||
/**
|
||||
* Tag 标签:取消收藏
|
||||
*/
|
||||
String TAG_UN_COLLECT = "UnCollect";
|
||||
}
|
||||
@@ -12,11 +12,21 @@ public class RedisKeyConstants {
|
||||
*/
|
||||
public static final String BLOOM_USER_NOTE_LIKE_LIST_KEY = "bloom:note:likes:";
|
||||
|
||||
/**
|
||||
* 布隆过滤器:用户笔记收藏 前缀
|
||||
*/
|
||||
public static final String BLOOM_USER_NOTE_COLLECT_LIST_KEY = "bloom:note:collects:";
|
||||
|
||||
/**
|
||||
* 用户笔记点赞列表 ZSet 前缀
|
||||
*/
|
||||
public static final String USER_NOTE_LIKE_ZSET_KEY = "user:note:likes:";
|
||||
|
||||
/**
|
||||
* 用户笔记收藏列表 ZSet 前缀
|
||||
*/
|
||||
public static final String USER_NOTE_COLLECT_ZSET_KEY = "user:note:collects:";
|
||||
|
||||
|
||||
/**
|
||||
* 构建完整的笔记详情 KEY
|
||||
@@ -37,6 +47,16 @@ public class RedisKeyConstants {
|
||||
return BLOOM_USER_NOTE_LIKE_LIST_KEY + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的布隆过滤器:用户笔记收藏 KEY
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 布隆过滤器:用户笔记收藏 KEY
|
||||
*/
|
||||
public static String buildBloomUserNoteCollectListKey(Long userId) {
|
||||
return BLOOM_USER_NOTE_COLLECT_LIST_KEY + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的用户笔记点赞列表 ZSet KEY
|
||||
*
|
||||
@@ -47,4 +67,13 @@ public class RedisKeyConstants {
|
||||
return USER_NOTE_LIKE_ZSET_KEY + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的用户笔记收藏列表 ZSet KEY
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户笔记收藏列表 ZSet KEY
|
||||
*/
|
||||
public static String buildUserNoteCollectZSetKey(Long userId) {
|
||||
return USER_NOTE_COLLECT_ZSET_KEY + userId;
|
||||
}
|
||||
}
|
||||
@@ -68,4 +68,10 @@ public class NoteController {
|
||||
return noteService.unlikeNote(unlikeNoteReqVO);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/collect")
|
||||
@ApiOperationLog(description = "收藏笔记")
|
||||
public Response<?> collectNote(@Validated @RequestBody CollectNoteReqVO collectNoteReqVO) {
|
||||
return noteService.collectNote(collectNoteReqVO);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.util.Date;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 笔记收藏表
|
||||
*/
|
||||
@@ -41,11 +42,11 @@ public class NoteCollectionDO {
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time")
|
||||
private Date createTime;
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 收藏状态(0:取消收藏 1:收藏)
|
||||
*/
|
||||
@TableField(value = "`status`")
|
||||
private Byte status;
|
||||
private Integer status;
|
||||
}
|
||||
@@ -6,4 +6,11 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface NoteCollectionDOMapper extends BaseMapper<NoteCollectionDO> {
|
||||
/**
|
||||
* 新增笔记收藏记录,若已存在,则更新笔记收藏记录
|
||||
*
|
||||
* @param noteCollectionDO 笔记收藏记录
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean insertOrUpdate(NoteCollectionDO noteCollectionDO);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.hanserwei.hannote.note.biz.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CollectStatusEnum {
|
||||
COLLECT(1), // 收藏
|
||||
UNCOLLECTED(0), // 取消收藏
|
||||
;
|
||||
|
||||
private final Integer code;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.hanserwei.hannote.note.biz.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CollectUnCollectNoteTypeEnum {
|
||||
// 收藏
|
||||
COLLECT(1),
|
||||
// 取消收藏
|
||||
UN_COLLECT(0),
|
||||
;
|
||||
|
||||
private final Integer code;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.hanserwei.hannote.note.biz.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum NoteCollectLuaResultEnum {
|
||||
// 布隆过滤器或者 ZSet 不存在
|
||||
NOT_EXIST(-1L),
|
||||
// 笔记已收藏
|
||||
NOTE_COLLECTED(1L),
|
||||
// 笔记收藏成功
|
||||
NOTE_COLLECTED_SUCCESS(0L),
|
||||
;
|
||||
|
||||
private final Long code;
|
||||
|
||||
/**
|
||||
* 根据类型 code 获取对应的枚举
|
||||
*
|
||||
* @param code 类型 code
|
||||
* @return 枚举
|
||||
*/
|
||||
public static NoteCollectLuaResultEnum valueOf(Long code) {
|
||||
for (NoteCollectLuaResultEnum noteCollectLuaResultEnum : NoteCollectLuaResultEnum.values()) {
|
||||
if (Objects.equals(code, noteCollectLuaResultEnum.getCode())) {
|
||||
return noteCollectLuaResultEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
NOTE_CANT_OPERATE("NOTE-20007", "您无法操作该笔记"),
|
||||
NOTE_ALREADY_LIKED("NOTE-20008", "您已经点赞过该笔记"),
|
||||
NOTE_NOT_LIKED("NOTE-20009", "您未点赞该篇笔记,无法取消点赞"),
|
||||
NOTE_ALREADY_COLLECTED("NOTE-20010", "您已经收藏过该笔记"),
|
||||
;
|
||||
|
||||
// 异常码
|
||||
|
||||
@@ -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 CollectUnCollectNoteMqDTO {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Long noteId;
|
||||
|
||||
/**
|
||||
* 0: 取消收藏, 1:收藏
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.hanserwei.hannote.note.biz.model.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class CollectNoteReqVO {
|
||||
|
||||
@NotNull(message = "笔记 ID 不能为空")
|
||||
private Long id;
|
||||
|
||||
}
|
||||
@@ -65,4 +65,12 @@ public interface NoteService extends IService<NoteDO> {
|
||||
*/
|
||||
Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO);
|
||||
|
||||
/**
|
||||
* 收藏笔记
|
||||
*
|
||||
* @param collectNoteReqVO 收藏笔记请求
|
||||
* @return 收藏笔记结果
|
||||
*/
|
||||
Response<?> collectNote(CollectNoteReqVO collectNoteReqVO);
|
||||
|
||||
}
|
||||
@@ -16,16 +16,19 @@ import com.hanserwei.framework.common.utils.DateUtils;
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.note.biz.constant.MQConstants;
|
||||
import com.hanserwei.hannote.note.biz.constant.RedisKeyConstants;
|
||||
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteCollectionDO;
|
||||
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.enums.*;
|
||||
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
||||
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
|
||||
import com.hanserwei.hannote.note.biz.model.vo.*;
|
||||
import com.hanserwei.hannote.note.biz.rpc.DistributedIdGeneratorRpcService;
|
||||
import com.hanserwei.hannote.note.biz.rpc.KeyValueRpcService;
|
||||
import com.hanserwei.hannote.note.biz.rpc.UserRpcService;
|
||||
import com.hanserwei.hannote.note.biz.service.NoteCollectionDOService;
|
||||
import com.hanserwei.hannote.note.biz.service.NoteLikeDOService;
|
||||
import com.hanserwei.hannote.note.biz.service.NoteService;
|
||||
import com.hanserwei.hannote.note.biz.service.TopicDOService;
|
||||
@@ -37,6 +40,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.rocketmq.client.producer.SendCallback;
|
||||
import org.apache.rocketmq.client.producer.SendResult;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
@@ -84,6 +88,8 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
.build();
|
||||
@Resource
|
||||
private NoteLikeDOService noteLikeDOService;
|
||||
@Autowired
|
||||
private NoteCollectionDOService noteCollectionDOService;
|
||||
|
||||
@Override
|
||||
public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
|
||||
@@ -794,6 +800,261 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<?> collectNote(CollectNoteReqVO collectNoteReqVO) {
|
||||
// 笔记ID
|
||||
Long noteId = collectNoteReqVO.getId();
|
||||
|
||||
// 1. 校验被收藏的笔记是否存在
|
||||
checkNoteIsExist(noteId);
|
||||
|
||||
// 2. 判断目标笔记,是否已经收藏过
|
||||
// 当前登录用户ID
|
||||
Long userId = LoginUserContextHolder.getUserId();
|
||||
|
||||
// 布隆过滤器Key
|
||||
String bloomUserNoteCollectListKey = RedisKeyConstants.buildBloomUserNoteCollectListKey(userId);
|
||||
// 构建 Redis Key
|
||||
String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(userId);
|
||||
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
// Lua 脚本路径
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_collect_check.lua")));
|
||||
// 返回值类型
|
||||
script.setResultType(Long.class);
|
||||
|
||||
// 执行 Lua 脚本,拿到返回结果
|
||||
Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId);
|
||||
|
||||
NoteCollectLuaResultEnum noteCollectLuaResultEnum = NoteCollectLuaResultEnum.valueOf(result);
|
||||
log.info("==> 【笔记收藏】Lua 脚本返回结果: {}", noteCollectLuaResultEnum);
|
||||
assert noteCollectLuaResultEnum != null;
|
||||
switch (noteCollectLuaResultEnum) {
|
||||
// 布隆过滤器不存在
|
||||
case NOT_EXIST -> {
|
||||
// 从数据库中校验笔记是否被收藏,并异步初始化布隆过滤器,设置过期时间
|
||||
long count = noteCollectionDOService.count(new LambdaQueryWrapper<>(NoteCollectionDO.class)
|
||||
.eq(NoteCollectionDO::getUserId, userId)
|
||||
.eq(NoteCollectionDO::getNoteId, noteId)
|
||||
.eq(NoteCollectionDO::getStatus, CollectStatusEnum.COLLECT.getCode()));
|
||||
// 设置隋朝过期时间
|
||||
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
||||
|
||||
// 若目标笔记已经收藏
|
||||
if (count > 0) {
|
||||
// 异步初始化布隆过滤器
|
||||
threadPoolTaskExecutor.submit(() -> {
|
||||
batchAddNoteCollect2BloomAndExpire(userId, expireSeconds, bloomUserNoteCollectListKey);
|
||||
});
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
|
||||
}
|
||||
// 若目标笔记未被收藏,查询当前用户是否有收藏其他笔记,有则同步初始化布隆过滤器
|
||||
batchAddNoteCollect2BloomAndExpire(userId, expireSeconds, bloomUserNoteCollectListKey);
|
||||
// 添加当前收藏笔记 ID 到布隆过滤器中
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_collect_and_expire.lua")));
|
||||
// 返回值类型
|
||||
script.setResultType(Long.class);
|
||||
redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId, expireSeconds);
|
||||
}
|
||||
// 目标笔记已经被收藏 (可能存在误判,需要进一步确认)
|
||||
case NOTE_COLLECTED -> {
|
||||
// 校验ZSet列表中是否有被收藏的笔记ID
|
||||
Double score = redisTemplate.opsForZSet().score(userNoteCollectZSetKey, noteId);
|
||||
if (Objects.nonNull(score)) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
|
||||
}
|
||||
// 若score为空,则说明该笔记未被收藏,查数据库确认
|
||||
long count = noteCollectionDOService.count(new LambdaQueryWrapper<>(NoteCollectionDO.class)
|
||||
.eq(NoteCollectionDO::getUserId, userId)
|
||||
.eq(NoteCollectionDO::getNoteId, noteId)
|
||||
.eq(NoteCollectionDO::getStatus, CollectStatusEnum.COLLECT.getCode()));
|
||||
if (count > 0) {
|
||||
// 数据库里面有收藏记录,而 Redis 中 ZSet 已过期被删除的话,需要重新异步初始化 ZSet
|
||||
asynInitUserNoteCollectsZSet(userId, userNoteCollectZSetKey);
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_COLLECTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 更新用户 ZSET 收藏列表
|
||||
// 3. 更新用户 ZSET 收藏列表
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
// Lua 脚本路径
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/note_collect_check_and_update_zset.lua")));
|
||||
// 返回值类型
|
||||
script.setResultType(Long.class);
|
||||
|
||||
// 执行 Lua 脚本,拿到返回结果
|
||||
result = redisTemplate.execute(script, Collections.singletonList(userNoteCollectZSetKey), noteId, DateUtils.localDateTime2Timestamp(now));
|
||||
|
||||
// 若 ZSet 列表不存在,需要重新初始化
|
||||
if (Objects.equals(result, NoteCollectLuaResultEnum.NOT_EXIST.getCode())) {
|
||||
// 查询当前用户最新收藏的 300 篇笔记
|
||||
Page<NoteCollectionDO> page = noteCollectionDOService.page(new Page<>(1, 300), new LambdaQueryWrapper<>(NoteCollectionDO.class)
|
||||
.select(NoteCollectionDO::getNoteId, NoteCollectionDO::getCreateTime)
|
||||
.eq(NoteCollectionDO::getUserId, userId)
|
||||
.eq(NoteCollectionDO::getStatus, CollectStatusEnum.COLLECT.getCode())
|
||||
.orderByDesc(NoteCollectionDO::getCreateTime));
|
||||
List<NoteCollectionDO> noteCollectionDOS = 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_collect_zset_and_expire.lua")));
|
||||
// 返回值类型
|
||||
script2.setResultType(Long.class);
|
||||
|
||||
// 若数据库中存在历史收藏笔记,需要批量同步
|
||||
if (CollUtil.isNotEmpty(noteCollectionDOS)) {
|
||||
// 构建 Lua 参数
|
||||
Object[] luaArgs = buildNoteCollectZSetLuaArgs(noteCollectionDOS, expireSeconds);
|
||||
|
||||
redisTemplate.execute(script2, Collections.singletonList(userNoteCollectZSetKey), luaArgs);
|
||||
|
||||
// 再次调用 note_collect_check_and_update_zset.lua 脚本,将当前收藏的笔记添加到 zset 中
|
||||
redisTemplate.execute(script, Collections.singletonList(userNoteCollectZSetKey), 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(userNoteCollectZSetKey), luaArgs.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. 发送 MQ, 将收藏数据落库
|
||||
// 构建消息体 DTO
|
||||
CollectUnCollectNoteMqDTO collectUnCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
|
||||
.userId(userId)
|
||||
.noteId(noteId)
|
||||
.type(CollectUnCollectNoteTypeEnum.COLLECT.getCode()) // 收藏笔记
|
||||
.createTime(now)
|
||||
.build();
|
||||
|
||||
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(collectUnCollectNoteMqDTO))
|
||||
.build();
|
||||
|
||||
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||
String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_COLLECT;
|
||||
|
||||
String hashKey = String.valueOf(userId);
|
||||
|
||||
// 异步发送顺序 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步初始化用户收藏笔记 ZSet
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param userNoteCollectZSetKey 用户收藏笔记 ZSet KEY
|
||||
*/
|
||||
private void asynInitUserNoteCollectsZSet(Long userId, String userNoteCollectZSetKey) {
|
||||
threadPoolTaskExecutor.submit(() -> {
|
||||
// 判断用户笔记收藏 ZSET 是否存在
|
||||
boolean hasKey = redisTemplate.hasKey(userNoteCollectZSetKey);
|
||||
|
||||
// 不存在则初始化
|
||||
if (!hasKey) {
|
||||
// 查询当前用户最新收藏的 300 篇笔记
|
||||
Page<NoteCollectionDO> page = noteCollectionDOService.page(new Page<>(1, 300), new LambdaQueryWrapper<>(NoteCollectionDO.class)
|
||||
.select(NoteCollectionDO::getNoteId, NoteCollectionDO::getCreateTime)
|
||||
.eq(NoteCollectionDO::getUserId, userId)
|
||||
.eq(NoteCollectionDO::getStatus, CollectStatusEnum.COLLECT.getCode())
|
||||
.orderByDesc(NoteCollectionDO::getCreateTime));
|
||||
List<NoteCollectionDO> noteCollectionDOS = page.getRecords();
|
||||
if (CollUtil.isNotEmpty(noteCollectionDOS)) {
|
||||
// 保底1天+随机秒数
|
||||
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
||||
// 构建 Lua 参数
|
||||
Object[] luaArgs = buildNoteCollectZSetLuaArgs(noteCollectionDOS, expireSeconds);
|
||||
|
||||
DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
|
||||
// Lua 脚本路径
|
||||
script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/batch_add_note_collect_zset_and_expire.lua")));
|
||||
// 返回值类型
|
||||
script2.setResultType(Long.class);
|
||||
|
||||
redisTemplate.execute(script2, Collections.singletonList(userNoteCollectZSetKey), luaArgs);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建笔记收藏 ZSET Lua 脚本参数
|
||||
*
|
||||
* @param noteCollectionDOS 笔记收藏列表
|
||||
* @param expireSeconds 过期时间
|
||||
* @return Lua 脚本参数
|
||||
*/
|
||||
private Object[] buildNoteCollectZSetLuaArgs(List<NoteCollectionDO> noteCollectionDOS, long expireSeconds) {
|
||||
int argsLength = noteCollectionDOS.size() * 2 + 1; // 每个笔记收藏关系有 2 个参数(score 和 value),最后再跟一个过期时间
|
||||
Object[] luaArgs = new Object[argsLength];
|
||||
|
||||
int i = 0;
|
||||
for (NoteCollectionDO noteCollectionDO : noteCollectionDOS) {
|
||||
// 收藏时间作为 score
|
||||
luaArgs[i] = DateUtils.localDateTime2Timestamp(noteCollectionDO.getCreateTime());
|
||||
// 笔记ID 作为 ZSet value
|
||||
luaArgs[i + 1] = noteCollectionDO.getNoteId();
|
||||
i += 2;
|
||||
}
|
||||
|
||||
luaArgs[argsLength - 1] = expireSeconds; // 最后一个参数是 ZSet 的过期时间
|
||||
return luaArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加用户收藏笔记到布隆过滤器中
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param expireSeconds 过期时间
|
||||
* @param bloomUserNoteCollectListKey 布隆过滤器:用户收藏笔记
|
||||
*/
|
||||
private void batchAddNoteCollect2BloomAndExpire(Long userId, long expireSeconds, String bloomUserNoteCollectListKey) {
|
||||
try {
|
||||
// 异步全量同步一下,并设置过期时间
|
||||
List<NoteLikeDO> noteCollectionDOS = noteLikeDOService.list(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
||||
.select(NoteLikeDO::getNoteId)
|
||||
.eq(NoteLikeDO::getUserId, userId)
|
||||
.eq(NoteLikeDO::getStatus, CollectStatusEnum.COLLECT.getCode()));
|
||||
if (CollUtil.isNotEmpty(noteCollectionDOS)) {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
// Lua 脚本路径
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_batch_add_note_collect_and_expire.lua")));
|
||||
// 返回值类型
|
||||
script.setResultType(Long.class);
|
||||
|
||||
// 构造Lua参数
|
||||
List<Object> luaParams = Lists.newArrayList();
|
||||
// 将每个收藏的笔记 ID 传入
|
||||
noteCollectionDOS.forEach(noteLikeDO -> luaParams.add(noteLikeDO.getNoteId()));
|
||||
// 最后一个参数是过期时间
|
||||
luaParams.add(expireSeconds);
|
||||
redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), luaParams.toArray());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("## 异步初始化【笔记收藏】布隆过滤器异常: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步初始化用户点赞笔记 ZSet
|
||||
*
|
||||
|
||||
@@ -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,10 @@
|
||||
-- 操作的 Key
|
||||
local key = KEYS[1]
|
||||
local noteId = ARGV[1] -- 笔记ID
|
||||
local expireSeconds = ARGV[2] -- 过期时间(秒)
|
||||
|
||||
redis.call("BF.ADD", key, noteId)
|
||||
|
||||
-- 设置过期时间
|
||||
redis.call("EXPIRE", key, expireSeconds)
|
||||
return 0
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 操作的 Key
|
||||
local key = KEYS[1]
|
||||
|
||||
for i = 1, #ARGV - 1 do
|
||||
redis.call("BF.ADD", key, ARGV[i])
|
||||
end
|
||||
|
||||
---- 最后一个参数为过期时间
|
||||
local expireTime = ARGV[#ARGV]
|
||||
-- 设置过期时间
|
||||
redis.call("EXPIRE", key, expireTime)
|
||||
return 0
|
||||
@@ -0,0 +1,20 @@
|
||||
-- LUA 脚本:笔记收藏布隆过滤器
|
||||
|
||||
local key = KEYS[1] -- 操作的 Redis Key
|
||||
local noteId = ARGV[1] -- 笔记ID
|
||||
|
||||
-- 使用 EXISTS 命令检查布隆过滤器是否存在
|
||||
local exists = redis.call('EXISTS', key)
|
||||
if exists == 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 校验该篇笔记是否被收藏过(1 表示已经收藏,0 表示未收藏)
|
||||
local isCollected = redis.call('BF.EXISTS', key, noteId)
|
||||
if isCollected == 1 then
|
||||
return 1
|
||||
end
|
||||
|
||||
-- 未被收藏,添加收藏数据
|
||||
redis.call('BF.ADD', key, noteId)
|
||||
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)
|
||||
|
||||
-- 若已经收藏了 300 篇笔记,则移除最早收藏的那篇
|
||||
if size >= 300 then
|
||||
redis.call('ZPOPMIN', key)
|
||||
end
|
||||
|
||||
-- 添加新的笔记收藏关系
|
||||
redis.call('ZADD', key, timestamp, noteId)
|
||||
return 0
|
||||
@@ -14,4 +14,11 @@
|
||||
<!--@mbg.generated-->
|
||||
id, user_id, note_id, create_time, `status`
|
||||
</sql>
|
||||
|
||||
<insert id="insertOrUpdate" parameterType="com.hanserwei.hannote.note.biz.domain.dataobject.NoteCollectionDO">
|
||||
INSERT INTO t_note_collection (user_id, note_id, create_time, status)
|
||||
VALUES (#{userId}, #{noteId}, #{createTime}, #{status})
|
||||
ON DUPLICATE KEY UPDATE
|
||||
create_time = #{createTime}, status = #{status};
|
||||
</insert>
|
||||
</mapper>
|
||||
@@ -199,6 +199,15 @@ POST http://localhost:8000/note/note/unlike
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"id": 1977249693272375330
|
||||
}
|
||||
|
||||
### 笔记收藏入口
|
||||
POST http://localhost:8000/note/note/collect
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"id": 1977249693272375330
|
||||
}
|
||||
Reference in New Issue
Block a user