无码国模产在线观看免费-无码国内精品久久人妻-无码国内精品久久综合88-无码国内精品人妻少妇-无码国内精品人妻少妇蜜桃视频-无码国语中文在线播放

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

redis rocketmq 點贊功能開發(fā)

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

一、功能設(shè)計

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

按照本項目的設(shè)計,點贊業(yè)務(wù)涉兩個個方面:

  • 要知道題目的點贊數(shù)
  • 還要知道每個人點贊的題目

點贊的業(yè)務(wù)特性:頻繁。用戶一多,時時刻刻都在進行點贊,收藏等。如果采取傳統(tǒng)的數(shù)據(jù)庫模式,交互量是非常大的,很難抗住并發(fā)問題,所以采取 redis 的方式來做。

查詢的數(shù)據(jù)交互,可以和 redis 直接來做,持久化的數(shù)據(jù),通過數(shù)據(jù)庫查詢即可,采取定時任務(wù) xxl-job 定期來刷數(shù)據(jù),將數(shù)據(jù)同步到數(shù)據(jù)庫。

記錄的時候三個關(guān)鍵信息,點贊的人,被點贊的題目,點贊的狀態(tài)。

最終的數(shù)據(jù)結(jié)構(gòu)就是 hash,string,string 類型。

hash類型用于同步數(shù)據(jù)庫:key:value([hashKey, hashVal]...)有一個總key,value分為一個個hashKey和hashVal,此處hashKey定義為subjectId:userId,hashVal為status點贊狀態(tài)

第一個string類型存題目對應(yīng)點贊數(shù)key=subjectId,value=count點贊數(shù);第二個string類型存題目對應(yīng)點贊人key=subjectId:userId,value="1"標(biāo)記點贊(該string與上面hash類似,在判斷當(dāng)前用戶是否點贊題目時處理方便)

數(shù)據(jù)庫設(shè)計:

二、基本功能開發(fā)

2.1 新增/取消點贊

直接操作redis,存hash,存題目數(shù)量+-1,存題目和點贊人的關(guān)聯(lián)

相關(guān)redisUtil:

/**
 * Hset key hashKey hashValue (hash類型存數(shù)據(jù))
 * @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對應(yīng)的value值加count(key不存在時會創(chuàng)建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(), "點贊狀態(tài)不能為空");
        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("題目點贊失敗");
    }
}

點贊狀態(tài)枚舉類

@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防腐層

/**
 * 構(gòu)建點贊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";
/**
 * 題目點贊數(shù)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) { //點贊狀態(tài)
        redisUtil.increment(countKey, 1);
        redisUtil.set(detailKey, "1"); //value用1標(biāo)記
    } else {
        Integer count = redisUtil.getInt(countKey);
        if(Objects.isNull(count) || count <= 0) { //當(dāng)數(shù)量不存在或為0時直接結(jié)束
            return;
        }
        redisUtil.increment(countKey, -1);
        redisUtil.del(detailKey);
    }
    ;
}

2.2 題目詳情增加點贊數(shù)據(jù)

此處涉及兩個功能:查詢當(dāng)前題目被點贊的數(shù)量,查詢當(dāng)前題目被當(dāng)前用戶是否點過贊

直接與reids交換,查詢key即可

subjectLiked的domain層實現(xiàn)以上兩個功能:

/**
 * 判斷當(dāng)前用戶是否點贊
 */
@Override
public Boolean isLiked(String subjectId, String userId) {
    String detailKey = SUBJECT_LIKED_DETAIL_KEY + "." + subjectId + "." + userId;
    return redisUtil.exist(detailKey);
}
/**
 * 獲取題目點贊數(shù)量
 */
@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;
}

在獲取題目詳情的返回值基礎(chǔ)上添加題目點贊數(shù)和當(dāng)前用戶是否點贊屬性,最后在domain層組裝

在subjectInfoDTO和BO中添加private Boolean liked(是否被當(dāng)前用戶點贊); private Integer likedCount(題目點贊數(shù)量);

domain層組裝:

@Override
public SubjectInfoBO querySubjectInfo(SubjectInfoBO subjectInfoBO) {
    if(log.isInfoEnabled()) {
        log.info("SubjectInfoDomainService.querySubjectInfo.subjectInfoBO:{}", JSON.toJSONString(subjectInfoBO));
    }
    //先查詢題目主表數(shù)據(jù)
    SubjectInfo subjectInfo = subjectInfoServices.queryById(subjectInfoBO.getId());
    //工廠 + 策略 查詢具體類型題目的數(shù)據(jù)
    SubejctTypeHandler handler = subjectTypeHandlerFactory.getHandler(subjectInfo.getSubjectType());
    SubjectOptionBO subjectOptionBO = handler.query(subjectInfoBO.getId());
    //將主表數(shù)據(jù)info 和 具體題目數(shù)據(jù)(答案、選項信息) 一起轉(zhuǎn)為 infoBo
    SubjectInfoBO bo = SubjectInfoBOConvert.INSTANCE.subjectOptionBoAndInfoToBo(subjectInfo, subjectOptionBO);
    //查詢標(biāo)簽id->標(biāo)簽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);
    //返回點贊數(shù)、是否點贊
    Integer likedCount = subjectLikedDomainService.getLikedCount(bo.getId().toString());
    Boolean liked = subjectLikedDomainService.isLiked(bo.getId().toString(), LoginUtil.getLoginId());
    bo.setLikedCount(likedCount);
    bo.setLiked(liked);
    return bo;
}

三、數(shù)據(jù)庫同步reids點贊數(shù)據(jù)

通過xxl-job每隔一秒向數(shù)據(jù)庫同步redis的hash點贊數(shù)據(jù)并刪除hash類型,因為間隔一秒執(zhí)行一次,所以當(dāng)并發(fā)量大時會有細(xì)微的延遲。

3.1 xxl-job執(zhí)行定時任務(wù)

@Component
@Log4j2
public class SyncLikedJob {
   @Resource
   private SubjectLikedDomainService subjectLikedDomainService;
   /**
    * 數(shù)據(jù)庫同步redis點贊數(shù)據(jù)
    * @throws Exception
    */
   @XxlJob("syncLikedJobHandler")
   public void syncLikedJobHandler() throws Exception {
       XxlJobHelper.log("syncLikedJobHandler.start"); //xxljob的日志方法會在任務(wù)調(diào)度中心顯示
       try {
           subjectLikedDomainService.syncLiked();
       } catch (Exception e) {
           XxlJobHelper.log("syncLikedJobHandler.error" + e.getMessage());
       }
   }
}

3.2 相關(guān)redisUtil

/**
 * 獲取并刪除hash類型緩存并組裝為Map
 * scan(key, ScanOptions.NONE):掃描指定key的hash表;掃描選項,NONE 表示使用默認(rèn)的掃描行為,即不限制掃描的字段數(shù)量,也不使用正則表達(dá)式匹配字段。。
 */
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()) { //檢查游標(biāo)(scan)是否還有下一個元素。
        Map.Entry<Object, Object> entry = scan.next(); //獲取游標(biāo)中的下一個元素
        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;
    }
    //批量同步數(shù)據(jù)庫
    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>

四、我的點贊

直接與數(shù)據(jù)庫交換,分頁查詢即可。因為xxl-job每隔一秒同步一次數(shù)據(jù),所以當(dāng)并發(fā)量大時,會有微小延遲。

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,轉(zhuǎn)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優(yōu)化點贊業(yè)務(wù)

之前的業(yè)務(wù)中,通過redis的hash表來保存用戶的點贊數(shù)據(jù),并配合xxl-job來定時刷到數(shù)據(jù)庫。這樣太過依賴redis和xxl-job的可靠性,數(shù)據(jù)量大時可能會丟失數(shù)據(jù),在此使用mq,每當(dāng)用戶點贊題目后,直接與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);
        //將每次的點贊消息發(fā)送到mq中直接與數(shù)據(jù)庫交互,替換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) { //點贊狀態(tài)
            redisUtil.increment(countKey, 1);
            redisUtil.set(detailKey, "1"); //value用1標(biāo)記
        } else {
            Integer count = redisUtil.getInt(countKey);
            if(Objects.isNull(count) || count <= 0) { //當(dāng)數(shù)量不存在或為0時直接結(jié)束
                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方法與數(shù)據(jù)庫交互

@Override
public void syncLikedMsg(SubjectLikedBO subjectLikedBO) {
    //同步到數(shù)據(jù)庫
    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);
}

六、點贊數(shù)據(jù)不更新BUG修復(fù)

在上述的操作中,用戶每次點贊和取消點贊都會保存到數(shù)據(jù)庫,導(dǎo)致同一個用戶id,同一個題目id,在數(shù)據(jù)庫中有點贊和未點贊兩個狀態(tài),當(dāng)用戶查詢我的點贊時,會從頭到尾遍歷數(shù)據(jù)庫status為1的題目,當(dāng)用戶先點贊后取消點贊時,題目仍在我的點贊列表中。

通過為subjectId和likedUserId建立唯一索引來保證subject_id 和 like_user_id 的組合值必須是唯一的,不能有重復(fù)記錄。

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

向表中添加一個名為unique_subject_like的唯一索引。

同時修改插入點贊數(shù)據(jù)的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: 當(dāng)插入的數(shù)據(jù)違反唯一鍵約束時,會觸發(fā)此更新操作。VALUES() 函數(shù)用于獲取插入語句中對應(yīng)列的值,將這些值更新到已存在的記錄中。

?轉(zhuǎn)自https://juejin.cn/post/7463393885961437218


該文章在 2025/4/19 8:49:36 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved

主站蜘蛛池模板: 亚洲日韩国产人成在线发布 | 四虎 影院 免费 | 一区二区视频在线观看入口 | 少妇无码av无码一区 | 中文天堂在线视频 | 加勒比heyzo高清无码中文 | 欧美日韩国产在线观看播放 | 夜色约爱网站 | 午夜国产狂喷潮在线观看 | 乱人伦人妻中文字幕无码 | 50路熟妇乱青青草免费成人福利视频 | 国产成人mv视频在线观看 | 久久久国产精品成人片 | 日本亚洲综合 | 免费国产网站 | 一区二区三区视频 | 三级免费网址 | 成人国产精品日本在线 | 久久久久久久久天天精品 | 国产成人av网站手机不卡 | 三级网国产精品 | 亚洲午夜精品AV无码少妇 | 精品人妻中文字幕乱码 | 成人三级视频在线观看完整版 | 国产精品导航一区二区 | 给我一个可以看片的免费 | 久久国产精品99久久小说 | 国产欧美成人免费观看视频 | av中文不卡在线观看 | 中文字幕欧美人妻精品一区 | 国产精品高潮呻吟久久av无码 | 亚洲日韩精品无码久久 | 日韩精品无码久久一区二区三 | 91免费视频网址完整版手机在线观看 | 精东视频影视传媒制作公司 | 熟女五十路开心久久伊人 | 97人妻熟女成人免费视频 | 久久99精品久久久久久噜噜丰满 | 亚洲一区二区在线观看不卡 | 日本一区二区三区啪啪视频 | 亚洲综合AV在线在线播放 |