LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

redis rocketmq 点赞功能开发

freeflydom
2025年4月19日 8:48 本文热度 64

一、功能设计

点赞与收藏的逻辑是一样的,这里就选取点赞功能来做开发。

按照本项目的设计,点赞业务涉两个个方面:

  • 要知道题目的点赞数
  • 还要知道每个人点赞的题目

点赞的业务特性:频繁。用户一多,时时刻刻都在进行点赞,收藏等。如果采取传统的数据库模式,交互量是非常大的,很难抗住并发问题,所以采取 redis 的方式来做。

查询的数据交互,可以和 redis 直接来做,持久化的数据,通过数据库查询即可,采取定时任务 xxl-job 定期来刷数据,将数据同步到数据库。

记录的时候三个关键信息,点赞的人,被点赞的题目,点赞的状态。

最终的数据结构就是 hash,string,string 类型。

hash类型用于同步数据库:key:value([hashKey, hashVal]...)有一个总key,value分为一个个hashKey和hashVal,此处hashKey定义为subjectId:userId,hashVal为status点赞状态

第一个string类型存题目对应点赞数key=subjectId,value=count点赞数;第二个string类型存题目对应点赞人key=subjectId:userId,value="1"标记点赞(该string与上面hash类似,在判断当前用户是否点赞题目时处理方便)

数据库设计:

二、基本功能开发

2.1 新增/取消点赞

直接操作redis,存hash,存题目数量+-1,存题目和点赞人的关联

相关redisUtil:

/**
 * Hset key hashKey hashValue (hash类型存数据)
 * @param key
 * @param hashKey
 * @param hashValue
 */
public void putHash(String key, String hashKey, Object hashValue) {
    redisTemplate.opsForHash().put(key, hashKey, hashValue);
}
/**
 * 获取int类型缓存
 * @param key
 * @return
 */
public Integer getInt(String key) {
    return (Integer) redisTemplate.opsForValue().get(key);
}
/**
 * 对指定key对应的value值加count(key不存在时会创建key,count为初始值)
 * @param key
 */
public void increment(String key, Integer count) {
    redisTemplate.opsForValue().increment(key, count);
}

controller入口层

@PostMapping("/add")
public Result<Boolean> add(@RequestBody SubjectLikedDTO subjectLikedDto) {
    try {
        if (log.isInfoEnabled()) {
            log.info("SubjectLikedController.add.dto:{}", JSON.toJSONString(subjectLikedDto));
        }
        Preconditions.checkNotNull(subjectLikedDto.getSubjectId(), "题目id不能为空");
        Preconditions.checkNotNull(subjectLikedDto.getStatus(), "点赞状态不能为空");
        subjectLikedDto.setLikeUserId(LoginUtil.getLoginId());
        Preconditions.checkNotNull(subjectLikedDto.getLikeUserId(), "点赞人不能为空");
        SubjectLikedBO subjectLikedBO = SubjectLikedDTOConvert.INSTANCE.subjectLikedDtoToBo(subjectLikedDto);
        subjectLikedDomainService.add(subjectLikedBO);
        return Result.ok(true);
    } catch (Exception e) {
        log.info("SubjectLikedController.add.error:{}", e.getMessage(), e);
        return Result.fail("题目点赞失败");
    }
}

点赞状态枚举类

@Getter
public enum SubjectLikedStatusEnum {
    LIKED(1, "点赞"),
    UN_LIKED(0, "未点赞");
    private int code;
    private String desc;
    SubjectLikedStatusEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

domain防腐层

/**
 * 构建点赞hashKey
 */
private String buildSubjectLikedKey(String subjectId, String likeUserId) {
    return subjectId + ":" + likeUserId;
}
@Resource
private RedisUtil redisUtil;
/**
 * 点赞hash的总key
 */
private static final String SUBJECT_LIKED_KEY = "subject.liked";
/**
 * 题目点赞数key前缀
 */
private static final String SUBJECT_LIKED_COUNT_KEY = "subject.liked.count";
/**
 * 题目点赞人key前置
 */
private static final String SUBJECT_LIKED_DETAIL_KEY = "subject.liked.detail";
/**
 * 新增/取消点赞
 * @return
 */
@Override
public void add(SubjectLikedBO subjectLikedBO) {
    String likeUserId = subjectLikedBO.getLikeUserId();
    Long subjectId = subjectLikedBO.getSubjectId();
    Integer status = subjectLikedBO.getStatus();
    String hashKey = buildSubjectLikedKey(subjectId.toString(), likeUserId);
    redisUtil.putHash(SUBJECT_LIKED_KEY, hashKey, status);
    String countKey = SUBJECT_LIKED_COUNT_KEY + "." + subjectId;
    String detailKey = SUBJECT_LIKED_DETAIL_KEY + "." + subjectId + "." + likeUserId;
    if(SubjectLikedStatusEnum.LIKED.getCode() == status) { //点赞状态
        redisUtil.increment(countKey, 1);
        redisUtil.set(detailKey, "1"); //value用1标记
    } else {
        Integer count = redisUtil.getInt(countKey);
        if(Objects.isNull(count) || count <= 0) { //当数量不存在或为0时直接结束
            return;
        }
        redisUtil.increment(countKey, -1);
        redisUtil.del(detailKey);
    }
    ;
}

2.2 题目详情增加点赞数据

此处涉及两个功能:查询当前题目被点赞的数量,查询当前题目被当前用户是否点过赞

直接与reids交换,查询key即可

subjectLiked的domain层实现以上两个功能:

/**
 * 判断当前用户是否点赞
 */
@Override
public Boolean isLiked(String subjectId, String userId) {
    String detailKey = SUBJECT_LIKED_DETAIL_KEY + "." + subjectId + "." + userId;
    return redisUtil.exist(detailKey);
}
/**
 * 获取题目点赞数量
 */
@Override
public Integer getLikedCount(String subjectId) {
    String countKey = SUBJECT_LIKED_COUNT_KEY + "." + subjectId;
    Integer count = redisUtil.getInt(countKey);
    if(Objects.isNull(count) || count <= 0) {
        count = 0;
    }
    return count;
}

在获取题目详情的返回值基础上添加题目点赞数和当前用户是否点赞属性,最后在domain层组装

在subjectInfoDTO和BO中添加private Boolean liked(是否被当前用户点赞); private Integer likedCount(题目点赞数量);

domain层组装:

@Override
public SubjectInfoBO querySubjectInfo(SubjectInfoBO subjectInfoBO) {
    if(log.isInfoEnabled()) {
        log.info("SubjectInfoDomainService.querySubjectInfo.subjectInfoBO:{}", JSON.toJSONString(subjectInfoBO));
    }
    //先查询题目主表数据
    SubjectInfo subjectInfo = subjectInfoServices.queryById(subjectInfoBO.getId());
    //工厂 + 策略 查询具体类型题目的数据
    SubejctTypeHandler handler = subjectTypeHandlerFactory.getHandler(subjectInfo.getSubjectType());
    SubjectOptionBO subjectOptionBO = handler.query(subjectInfoBO.getId());
    //将主表数据info 和 具体题目数据(答案、选项信息) 一起转为 infoBo
    SubjectInfoBO bo = SubjectInfoBOConvert.INSTANCE.subjectOptionBoAndInfoToBo(subjectInfo, subjectOptionBO);
    //查询标签id->标签name
    SubjectMapping subjectMapping = new SubjectMapping();
    subjectMapping.setSubjectId(bo.getId());
    subjectMapping.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());
    List<SubjectMapping> subjectMappingList = subjectMappingService.queryByLabelId(subjectMapping);
    List<Long> labelIds = subjectMappingList.stream().map(SubjectMapping::getLabelId).collect(Collectors.toList());
    List<SubjectLabel> subjectLabelList = subjectLabelService.queryByLabelIds(labelIds);
    List<String> labelNames = subjectLabelList.stream().map(SubjectLabel::getLabelName).collect(Collectors.toList());
    bo.setLabelName(labelNames);
    //返回点赞数、是否点赞
    Integer likedCount = subjectLikedDomainService.getLikedCount(bo.getId().toString());
    Boolean liked = subjectLikedDomainService.isLiked(bo.getId().toString(), LoginUtil.getLoginId());
    bo.setLikedCount(likedCount);
    bo.setLiked(liked);
    return bo;
}

三、数据库同步reids点赞数据

通过xxl-job每隔一秒向数据库同步redis的hash点赞数据并删除hash类型,因为间隔一秒执行一次,所以当并发量大时会有细微的延迟。

3.1 xxl-job执行定时任务

@Component
@Log4j2
public class SyncLikedJob {
   @Resource
   private SubjectLikedDomainService subjectLikedDomainService;
   /**
    * 数据库同步redis点赞数据
    * @throws Exception
    */
   @XxlJob("syncLikedJobHandler")
   public void syncLikedJobHandler() throws Exception {
       XxlJobHelper.log("syncLikedJobHandler.start"); //xxljob的日志方法会在任务调度中心显示
       try {
           subjectLikedDomainService.syncLiked();
       } catch (Exception e) {
           XxlJobHelper.log("syncLikedJobHandler.error" + e.getMessage());
       }
   }
}

3.2 相关redisUtil

/**
 * 获取并删除hash类型缓存并组装为Map
 * scan(key, ScanOptions.NONE):扫描指定key的hash表;扫描选项,NONE 表示使用默认的扫描行为,即不限制扫描的字段数量,也不使用正则表达式匹配字段。。
 */
public Map<Object, Object> getHashAndDelete(String key) {
    Map<Object, Object> map = new HashMap<>();
    Cursor<Map.Entry<Object, Object>> scan = redisTemplate.opsForHash().scan(key, ScanOptions.NONE);
    while (scan.hasNext()) { //检查游标(scan)是否还有下一个元素。
        Map.Entry<Object, Object> entry = scan.next(); //获取游标中的下一个元素
        map.put(entry.getKey(), entry.getValue());
        redisTemplate.opsForHash().delete(key, entry.getKey());
    }
    return map;
}

3.3 domain层核心逻辑

@Override
public void syncLiked() {
    Map<Object, Object> subjectLikedMap = redisUtil.getHashAndDelete(SUBJECT_LIKED_KEY);
    if(log.isInfoEnabled()) {
        log.info("syncLiked.subjectLikedMap:{}", JSON.toJSONString(subjectLikedMap));
    }
    if(subjectLikedMap.isEmpty()) {
        return;
    }
    //批量同步数据库
    List<SubjectLiked> subjectLikedList = new ArrayList<>();
    subjectLikedMap.forEach((key, val) -> {
        SubjectLiked subjectLiked = new SubjectLiked();
        String[] split = key.toString().split(":"); //subjectId:userId
        subjectLiked.setSubjectId(Long.valueOf(split[0]));
        subjectLiked.setLikeUserId(split[1]);
        subjectLiked.setStatus(Integer.valueOf(val.toString()));
        subjectLikedList.add(subjectLiked);
    });
    subjectLikedService.batchInsert(subjectLikedList);
}

3.4 infra原子性操作

<insert id="batchInsert">
    INSERT INTO subject_liked (subject_id, like_user_id, status, created_by, created_time, update_by, update_time)
    VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.subjectId}, #{item.likeUserId}, #{item.status}, #{item.createdBy}, #{item.createdTime}, #{item.updateBy}, #{item.updateTime})
    </foreach>
</insert>

四、我的点赞

直接与数据库交换,分页查询即可。因为xxl-job每隔一秒同步一次数据,所以当并发量大时,会有微小延迟。

SubjectLikedDTO和BO都要继承PageInfo,并添加subjectName在页面显示

@PostMapping("/getSubjectLikedPage")
public Result<PageResult<SubjectLikedDTO>> getSubjectLikedPage(@RequestBody SubjectLikedDTO subjectLikedDTO) {
    try {
        if (log.isInfoEnabled()) {
            log.info("SubjectLikedController.getSubjectLikedPage.dto:{}", JSON.toJSONString(subjectLikedDTO));
        }
        SubjectLikedBO subjectLikedBO = SubjectLikedDTOConvert.INSTANCE.subjectLikedDtoToBo(subjectLikedDTO);
        PageResult<SubjectLikedBO> subjectLikedBOList = subjectLikedDomainService.getSubjectLikedPage(subjectLikedBO);
        //直接返回BO,转DTO繁琐
        return Result.ok(subjectLikedBOList);
    } catch (Exception e) {
        log.info("SubjectLikedController.getSubjectLikedPage.error:{}", e.getMessage(), e);
        return Result.fail("查询点赞记录失败");
    }
}
@Override
public PageResult<SubjectLikedBO> getSubjectLikedPage(SubjectLikedBO subjectLikedBO) {
    PageResult<SubjectLikedBO> pageResult = new PageResult<>();
    pageResult.setPageNo(subjectLikedBO.getPageNo());
    pageResult.setPageSize(subjectLikedBO.getPageSize());
    int start = (subjectLikedBO.getPageNo() - 1) * subjectLikedBO.getPageSize();
    SubjectLiked subjectLiked = SubjectLikedBOConvert.INSTANCE.subjectLikedBoToSubjectLiked(subjectLikedBO);
    subjectLiked.setLikeUserId(LoginUtil.getLoginId());
    int count = subjectLikedService.countByCondition(subjectLiked);
    if(count == 0) {
        return pageResult;
    }
    List<SubjectLiked> subjectLikedList = subjectLikedService.queryPage(subjectLiked, start, subjectLikedBO.getPageSize());
    List<SubjectLikedBO> subjectLikedBOList = SubjectLikedBOConvert.INSTANCE.subjectLikedsToBos(subjectLikedList);
    subjectLikedBOList.forEach(info -> {
        SubjectInfo subjectInfo = subjectInfoService.queryById(info.getSubjectId());
        info.setSubjectName(subjectInfo.getSubjectName());
    });
    pageResult.setRecords(subjectLikedBOList);
    pageResult.setTotal(count);
    return pageResult;
}
<select id="countByCondition" resultType="java.lang.Integer">
    SELECT count(1) FROM subject_liked where like_user_id = #{likeUserId} and status = 1 and is_deleted = 0
</select>
<select id="queryPage" resultMap="SubjectLikedMap">
    SELECT * FROM subject_liked
    where status = 1 and is_deleted = 0
    and like_user_id = #{subjectLiked.likeUserId}
    limit #{start}, #{pageSize}
</select>

五、Rocketmq优化点赞业务

之前的业务中,通过redis的hash表来保存用户的点赞数据,并配合xxl-job来定时刷到数据库。这样太过依赖redis和xxl-job的可靠性,数据量大时可能会丢失数据,在此使用mq,每当用户点赞题目后,直接与mysql交互。

domain层修改,SubjectLikedMessage主要有subjectId,likedUserId,status

    @Override
    public void add(SubjectLikedBO subjectLikedBO) {
        String likeUserId = subjectLikedBO.getLikeUserId();
        Long subjectId = subjectLikedBO.getSubjectId();
        Integer status = subjectLikedBO.getStatus();
//        String hashKey = buildSubjectLikedKey(subjectId.toString(), likeUserId);
//        redisUtil.putHash(SUBJECT_LIKED_KEY, hashKey, status);
        //将每次的点赞消息发送到mq中直接与数据库交互,替换redis-hash表
        SubjectLikedMessage subjectLikedMessage = new SubjectLikedMessage();
        subjectLikedMessage.setSubjectId(subjectId);
        subjectLikedMessage.setLikeUserId(likeUserId);
        subjectLikedMessage.setStatus(status);
        rocketMQTemplate.convertAndSend("subject-liked", JSON.toJSONString(subjectLikedMessage));
        String countKey = SUBJECT_LIKED_COUNT_KEY + "." + subjectId;
        String detailKey = SUBJECT_LIKED_DETAIL_KEY + "." + subjectId + "." + likeUserId;
        if(SubjectLikedStatusEnum.LIKED.getCode() == status) { //点赞状态
            redisUtil.increment(countKey, 1);
            redisUtil.set(detailKey, "1"); //value用1标记
        } else {
            Integer count = redisUtil.getInt(countKey);
            if(Objects.isNull(count) || count <= 0) { //当数量不存在或为0时直接结束
                return;
            }
            redisUtil.increment(countKey, -1);
            redisUtil.del(detailKey);
        }
        ;
    }

mq消费层

@Component
@RocketMQMessageListener(topic = "subject-liked", consumerGroup = "subject-group")
@Log4j2
public class SubjectLikedConsumer implements RocketMQListener<String> {
    @Resource
    private SubjectLikedDomainService subjectLikedDomainService;
    @Override
    public void onMessage(String message) {
        log.info("SubjectLikedConsumer.onMessage.message:{}", message);
        SubjectLikedBO subjectLikedBO = JSON.parseObject(message, SubjectLikedBO.class);
        subjectLikedDomainService.syncLikedMsg(subjectLikedBO);
    }
}

syncLikedMsg方法与数据库交互

@Override
public void syncLikedMsg(SubjectLikedBO subjectLikedBO) {
    //同步到数据库
    SubjectLiked subjectLiked = new SubjectLiked();
    subjectLiked.setSubjectId(subjectLikedBO.getSubjectId());
    subjectLiked.setLikeUserId(subjectLikedBO.getLikeUserId());
    subjectLiked.setStatus(subjectLikedBO.getStatus());
    subjectLiked.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());
    List<SubjectLiked> subjectLikedList = new LinkedList<>();
    subjectLikedList.add(subjectLiked);
    subjectLikedService.batchInsertOrUpdate(subjectLikedList);
}

六、点赞数据不更新BUG修复

在上述的操作中,用户每次点赞和取消点赞都会保存到数据库,导致同一个用户id,同一个题目id,在数据库中有点赞和未点赞两个状态,当用户查询我的点赞时,会从头到尾遍历数据库status为1的题目,当用户先点赞后取消点赞时,题目仍在我的点赞列表中。

通过为subjectId和likedUserId建立唯一索引来保证subject_id 和 like_user_id 的组合值必须是唯一的,不能有重复记录。

ALTER TABLE subject_liked ADD UNIQUE KEY unique_subject_like (subject_id, like_user_id);

向表中添加一个名为unique_subject_like的唯一索引。

同时修改插入点赞数据的sql语句:

<insert id="batchInsertOrUpdate">
    INSERT INTO subject_liked
    (subject_id, like_user_id, status, created_by, created_time, update_by, update_time, is_deleted)
    VALUES
    <foreach collection="entities" item="item" separator=",">
        (#{item.subjectId}, #{item.likeUserId}, #{item.status}, #{item.createdBy}, #{item.createdTime}, #{item.updateBy}, #{item.updateTime}, #{item.isDeleted})
    </foreach>
    ON DUPLICATE KEY UPDATE
    status = VALUES(status),
    created_by = VALUES(created_by),
    created_time = VALUES(created_time),
    update_by = VALUES(update_by),
    update_time = VALUES(update_time),
    is_deleted = VALUES(is_deleted)
</insert>

ON DUPLICATE KEY UPDATE: 当插入的数据违反唯一键约束时,会触发此更新操作。VALUES() 函数用于获取插入语句中对应列的值,将这些值更新到已存在的记录中。

​转自https://juejin.cn/post/7463393885961437218


该文章在 2025/4/19 8:49:36 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved