日韩欧美国产精品免费一二-日韩欧美国产精品亚洲二区-日韩欧美国产精品专区-日韩欧美国产另-日韩欧美国产免费看-日韩欧美国产免费看清风阁

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

redis rocketmq 點贊功能開發

freeflydom
2025年4月19日 8:48 本文熱度 88

一、功能設計

點贊與收藏的邏輯是一樣的,這里就選取點贊功能來做開發。

按照本項目的設計,點贊業務涉兩個個方面:

  • 要知道題目的點贊數
  • 還要知道每個人點贊的題目

點贊的業務特性:頻繁。用戶一多,時時刻刻都在進行點贊,收藏等。如果采取傳統的數據庫模式,交互量是非常大的,很難抗住并發問題,所以采取 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

主站蜘蛛池模板: 亚洲精品男女视频在线 | 国产喷水在线观看 | 亚洲午夜福利在线视频 | 欧美不卡视频一区发布 | 亚洲免费体验区 | 日韩a∨精品一区二区三区 国产亚洲视频中文字幕97精品 | 亚洲高清不卡在线观看 | 欧美激情拍拍拍 | 乳乱公伦爽到爆 | 日日夜夜 | 亚洲日韩天堂在线 | 国产产精品亚洲一区二区在线观看 | 精品国产福利在线观看91啪 | 中文字幕资源网在线观看免费 | 在线精品免费看 | 亚洲欧美国产国产一区二区三区 | 精品国产迪丽热巴在线 | 免费网站看v片在线爱的影院 | 2025亚洲精品极品色在线 | 国泰饭店| 91福利免费体验区观看区 | 99在线精品国产不卡在线观看 | 亚洲人成在 | 97视频新免费 | 日韩在线欧美精 | 欧美亚洲综合卡通另类区 | 欧美激情xxxx性bbbb | 亚洲一区不卡视频 | 亚洲人成在线观看网站播放 | 亚洲欧美日韩激情在线观 | 无人视频在线观看免费播放影院 | 国产日产免费高清欧美一区 | 国产一区二区xxx | 91欧美精品综合在线观看 | 色猫咪免费人成网站在线观看 | 免费国产高清视频 | 日本精品一区二区三区 | 亚洲无线一二三四区手机 | 亚洲欧美日本韩国在线观看 | 字幕一区在线观看视频 | 在线观看中文 |