狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

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

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

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

一、功能設(shè)計(jì)

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

按照本項(xiàng)目的設(shè)計(jì),點(diǎn)贊業(yè)務(wù)涉兩個(gè)個(gè)方面:

  • 要知道題目的點(diǎn)贊數(shù)
  • 還要知道每個(gè)人點(diǎn)贊的題目

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

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

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

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

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

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

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

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

2.1 新增/取消點(diǎn)贊

直接操作redis,存hash,存題目數(shù)量+-1,存題目和點(diǎn)贊人的關(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);
}
/**
 * 對(duì)指定key對(duì)應(yīng)的value值加count(key不存在時(shí)會(huì)創(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(), "點(diǎn)贊狀態(tài)不能為空");
        subjectLikedDto.setLikeUserId(LoginUtil.getLoginId());
        Preconditions.checkNotNull(subjectLikedDto.getLikeUserId(), "點(diǎn)贊人不能為空");
        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("題目點(diǎn)贊失敗");
    }
}

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

@Getter
public enum SubjectLikedStatusEnum {
    LIKED(1, "點(diǎn)贊"),
    UN_LIKED(0, "未點(diǎn)贊");
    private int code;
    private String desc;
    SubjectLikedStatusEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

domain防腐層

/**
 * 構(gòu)建點(diǎn)贊hashKey
 */
private String buildSubjectLikedKey(String subjectId, String likeUserId) {
    return subjectId + ":" + likeUserId;
}
@Resource
private RedisUtil redisUtil;
/**
 * 點(diǎn)贊hash的總key
 */
private static final String SUBJECT_LIKED_KEY = "subject.liked";
/**
 * 題目點(diǎn)贊數(shù)key前綴
 */
private static final String SUBJECT_LIKED_COUNT_KEY = "subject.liked.count";
/**
 * 題目點(diǎn)贊人key前置
 */
private static final String SUBJECT_LIKED_DETAIL_KEY = "subject.liked.detail";
/**
 * 新增/取消點(diǎn)贊
 * @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) { //點(diǎn)贊狀態(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ù)量不存在或?yàn)?時(shí)直接結(jié)束
            return;
        }
        redisUtil.increment(countKey, -1);
        redisUtil.del(detailKey);
    }
    ;
}

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

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

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

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

/**
 * 判斷當(dāng)前用戶是否點(diǎn)贊
 */
@Override
public Boolean isLiked(String subjectId, String userId) {
    String detailKey = SUBJECT_LIKED_DETAIL_KEY + "." + subjectId + "." + userId;
    return redisUtil.exist(detailKey);
}
/**
 * 獲取題目點(diǎn)贊數(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ǔ)上添加題目點(diǎn)贊數(shù)和當(dāng)前用戶是否點(diǎn)贊屬性,最后在domain層組裝

在subjectInfoDTO和BO中添加private Boolean liked(是否被當(dāng)前用戶點(diǎn)贊); private Integer likedCount(題目點(diǎn)贊數(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ù)(答案、選項(xiàng)信息) 一起轉(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);
    //返回點(diǎn)贊數(shù)、是否點(diǎn)贊
    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點(diǎn)贊數(shù)據(jù)

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

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

@Component
@Log4j2
public class SyncLikedJob {
   @Resource
   private SubjectLikedDomainService subjectLikedDomainService;
   /**
    * 數(shù)據(jù)庫同步redis點(diǎn)贊數(shù)據(jù)
    * @throws Exception
    */
   @XxlJob("syncLikedJobHandler")
   public void syncLikedJobHandler() throws Exception {
       XxlJobHelper.log("syncLikedJobHandler.start"); //xxljob的日志方法會(huì)在任務(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表;掃描選項(xiàng),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)是否還有下一個(gè)元素。
        Map.Entry<Object, Object> entry = scan.next(); //獲取游標(biāo)中的下一個(gè)元素
        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>

四、我的點(diǎn)贊

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

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("查詢點(diǎn)贊記錄失敗");
    }
}
@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)化點(diǎn)贊業(yè)務(wù)

之前的業(yè)務(wù)中,通過redis的hash表來保存用戶的點(diǎn)贊數(shù)據(jù),并配合xxl-job來定時(shí)刷到數(shù)據(jù)庫。這樣太過依賴redis和xxl-job的可靠性,數(shù)據(jù)量大時(shí)可能會(huì)丟失數(shù)據(jù),在此使用mq,每當(dāng)用戶點(diǎn)贊題目后,直接與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);
        //將每次的點(diǎn)贊消息發(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) { //點(diǎn)贊狀態(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ù)量不存在或?yàn)?時(shí)直接結(jié)束
                return;
            }
            redisUtil.increment(countKey, -1);
            redisUtil.del(detailKey);
        }
        ;
    }

mq消費(fèi)層

@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);
}

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

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

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

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

向表中添加一個(gè)名為unique_subject_like的唯一索引。

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

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


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