本文所有代码已在gitee开源 :freefancy

1、需求

首先说明功能需求:用户可以对动态点赞/取消点赞,用户可以查看已经点赞的动态,动态下显示点赞的数量和点赞的用户。

2、分析

传统数据库是可以实现这个需求的,动态表需要有一个字段like_num记录点赞数,另外需要一个表记录点赞,需要有的字段是用户iduser_id、动态idarticle_id、点赞状态status、创建时间gmt_create、最后修改时间gmt_modified

功能的实现也很简单不赘述。mysql应对查询可能还能应付,但点赞是一个很随意的操作,且多数为写操作,用户量一大数据库压力也会很大,可以考虑将点赞数据先写在缓存中,然后定期的写会数据库,虽有宕机丢失数据的风险,但对于点赞数据,还是可以容忍的。

因为redis有丰富的数据结构方便使用,这里就使用redis作为缓存。

这里直接贴出我的实现思路:

对于一次点赞/取消点赞,使用一个名为user_article_like_hash_${ userId}_${articleId}的hash表保存,表中保存两个字段,statustime,分别记录本次操作的结果和操作时间。

这样做相比用一个hash表保存全部的点赞/取消点赞操作,一个hash表正好对应了数据库里的一条数据,不需要将业务需要的信息编码成value,也省去了解码的操作,编码和解码的开销是很大的;这样同时也十分好扩展业务,直接加字段就可以了。

另外为了记录点赞数,我们需要article_${articleId}_like_counter来记录点赞数,点赞+1,取消点赞-1,这个值是可负的,动态的真实点赞数 = redis计数器 + mysql like_num字段。

为了满足上面的设计,还需要一些辅助的数据结构来帮助我们记录信息:集合 article_set记录article的id, 集合 user_article_like_set_${articleId}来记录对某个article点赞或者取消点赞的用户id。

举例:点赞

这里举例点赞操作怎么进行,其余的操作会在代码中体现。
用户对动态点赞,将该动态的id放入article_set,将该用户的id放入user_article_like_set_${articleId},将hash 表user_article_like_hash_${ userId}_${articleId}中keystatus 的value设置为1,keytime的value记录点赞的时间。

数据持久化

redis的数据定期写回mysql中,大致的操作是:利用article_setuser_article_like_set_${articleId}将所有的user_article_like_hash_${ userId}_${articleId}写回mysql,将article_${articleId}_like_counter与mysql中相应字段相加,最后将上述redis中的数据都删除。

代码

注:代码中使用的工具类可以在第一行的项目中找到。

服务层方法

package com.whut.idea.freefancy.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.whut.idea.freefancy.mapper.ArticleMapper;
import com.whut.idea.freefancy.mapper.UserArticleLikeMapper;
import com.whut.idea.freefancy.pojo.entity.Article;
import com.whut.idea.freefancy.pojo.entity.UserArticleLike;
import com.whut.idea.freefancy.service.UserArticleLikeService;
import com.whut.idea.freefancy.util.DateUtils;
import com.whut.idea.freefancy.util.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Set;

/**
 * @author LiMing
 * @date 2022/2/13 19:45
 */
@Service
public class UserArticleLikeServiceImpl extends ServiceImpl<UserArticleLikeMapper, UserArticleLike> implements UserArticleLikeService {

    @Autowired
    UserArticleLikeMapper userArticleLikeMapper;

    @Autowired
    ArticleMapper articleMapper;

    /**
     * 点赞标志
     */
    private final static String LIKE = "1";
    /**
     * 未点赞标志
     */
    private final static String UNLIKE = "0";

    @Override
    public void like(Long userId, Long articleId) {
        String userId_str = userId.toString();
        String articleId_str = articleId.toString();
        //article id放入article set中
        RedisUtils.sadd("article_set", articleId_str);
        //user id 放入user_article_like_set_{$article_id} 集合中,保存为article点赞的所有用户
        RedisUtils.sadd("user_article_like_set_" + articleId_str, userId_str);
        //将点赞的相关信息写入key为user_article_like_hash_{$user_id}_{$article_id}的字段中
        HashMap<String, String> map = new HashMap<>();
        map.put("status", LIKE);
        map.put("time", DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD_HH_MM_SS));
        RedisUtils.hmset("user_article_like_hash_" + userId_str + "_" + articleId_str, map);
        //article点赞计数器增1
        RedisUtils.incr("article_" + articleId_str + "_like_counter");
    }

    @Override
    public void cancelLike(Long userId, Long articleId) {
        String userId_str = userId.toString();
        String articleId_str = articleId.toString();
        //article id放入article set中
        RedisUtils.sadd("article_set", articleId_str);
        //user id 放入user_article_like_set_{$article_id} 集合中,保存为article点赞的所有用户
        RedisUtils.sadd("user_article_like_set_" + articleId_str, userId_str);
        //将点赞的相关信息写入key为user_article_like_hash_{$user_id}_{$article_id}的字段中
        HashMap<String, String> map = new HashMap<>(2);
        map.put("status", UNLIKE);
        map.put("time", DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD_HH_MM_SS));
        RedisUtils.hmset("user_article_like_hash_" + userId_str + "_" + articleId_str, map);
        //article点赞计数器减1
        RedisUtils.decr("article_" + articleId_str + "_like_counter");
    }

    @Override
    public boolean judgeLike(Long userId, Long articleId) {
        String userId_str = userId.toString();
        String articleId_str = articleId.toString();
        //先判断redis中是否记录
        String status = RedisUtils.hget("user_article_like_hash_" + userId_str + "_" + articleId_str, "status");
        if (status != null) {
            if (LIKE.equals(status)) {
                return true;
            }else if (UNLIKE.equals(status)) {
                return false;
            }
        }
        // 再判断mysql中
        QueryWrapper<UserArticleLike> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", userId).eq("article_id", articleId).eq("status", 1);
        UserArticleLike userArticleLike = userArticleLikeMapper.selectOne(queryWrapper);
        if (userArticleLike != null) {
            return true;
        }else{
            return false;
        }
    }

    @Override
    public long LikeCounter(Long articleId) {
        //article的点赞数 = mysql表 + redis计数器
        long count = 0;
        QueryWrapper<Article> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id", articleId).eq("is_deleted", 0);
        Article article = articleMapper.selectOne(queryWrapper);
        if (article != null) {
            count += article.getLikeNum();
        }
        String redisCount = RedisUtils.get("article_" + articleId.toString() + "_like_counter");
        if (redisCount != null) {
            count += Long.parseLong(redisCount);
        }
        return count;
    }

    @Override
    public void persistence() {
        Set<String> article_set = RedisUtils.smembers("article_set");
        RedisUtils.del("article_set");
        for (String articleId_str : article_set) {
            Set<String> user_set = RedisUtils.smembers("user_article_like_set_" + articleId_str);
            RedisUtils.del("user_article_like_set_" + articleId_str);
            for (String userId_str : user_set) {
                String status = RedisUtils.hget("user_article_like_hash_" + userId_str + "_" + articleId_str, "status");
                String time = RedisUtils.hget("user_article_like_hash_" + userId_str + "_" + articleId_str, "time");
                RedisUtils.del("user_article_like_hash_" + userId_str + "_" + articleId_str);
                QueryWrapper<UserArticleLike> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("user_id", Long.parseLong(userId_str)).eq("article_id", Long.parseLong(articleId_str));
                UserArticleLike userArticleLike = userArticleLikeMapper.selectOne(queryWrapper);
                if (userArticleLike != null) {
                    //更新
                    userArticleLike.setStatus(Integer.parseInt(status));
                    userArticleLike.setGmtModified(DateUtils.parseDate(time));
                    userArticleLikeMapper.updateById(userArticleLike);
                }else {
                    //新建
                    UserArticleLike like = new UserArticleLike();
                    like.setUserId(Long.parseLong(userId_str));
                    like.setArticleId(Long.parseLong(articleId_str));
                    like.setStatus(Integer.parseInt(status));
                    like.setGmtCreate(DateUtils.parseDate(time));
                    userArticleLikeMapper.insert(like);
                }
            }
            //更新点赞数
            Article article = articleMapper.selectById(Long.parseLong(articleId_str));
            long increaseCount = Long.parseLong(RedisUtils.get("article_" + articleId_str + "_like_counter"));
            if (article != null) {
                article.setLikeNum(article.getLikeNum() + increaseCount);
                articleMapper.updateById(article);
            }
            RedisUtils.del("article_" + articleId_str + "_like_counter");
        }
    }


}

控制层

package com.whut.idea.freefancy.controller;

import com.whut.idea.freefancy.common.response.ResponseJson;
import com.whut.idea.freefancy.pojo.entity.UserArticleLike;
import com.whut.idea.freefancy.service.UserArticleLikeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.constraints.NotEmpty;


/**
 * @author LiMing
 * @date 2022/2/13 19:50
 */
@Api(value = "UserArticleLikeController",tags = "点赞功能接口")
@RestController
@RequestMapping("/like")
public class UserArticleLikeController {

    @Autowired
    UserArticleLikeService userArticleLikeService;

    @ApiOperation(value = "点赞", notes = "只需要传user id 和article id")
    @PostMapping
    public Object like(@RequestBody UserArticleLike userArticleLike) {
        userArticleLikeService.like(userArticleLike.getUserId(), userArticleLike.getArticleId());
        return ResponseJson.success();
    }

    @ApiOperation(value = "取消点赞", notes = "只需要传user id 和article id")
    @PutMapping
    public Object cancelLike(@RequestBody UserArticleLike userArticleLike) {
        userArticleLikeService.cancelLike(userArticleLike.getUserId(), userArticleLike.getArticleId());
        return ResponseJson.success();
    }

    @ApiOperation(value = "根据id查询动态的点赞次数")
    @GetMapping("/likeCount/{articleId}")
    public Object getTopic(@PathVariable Long articleId) {
        long count = userArticleLikeService.LikeCounter(articleId);
        return ResponseJson.success(count);
    }

    @ApiOperation(value = "根据user是否对article点赞")
    @GetMapping("/judge/{userId}/{articleId}")
    public Object judgeLike(@PathVariable @NotEmpty Long userId, @PathVariable @NotEmpty Long articleId) {
        boolean judgeLike = userArticleLikeService.judgeLike(userId, articleId);
        return ResponseJson.success(judgeLike);
    }

}

定时任务

package com.whut.idea.freefancy.common.schedule;

import com.whut.idea.freefancy.service.UserArticleLikeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author LiMing
 * @date 2022/2/14 21:55
 */
@Component
@EnableScheduling
public class CachePersistence {

    private static final Logger LOG = LoggerFactory.getLogger(CachePersistence.class);

    @Autowired
    UserArticleLikeService userArticleLikeService;

    /**
     * 点赞信息持久化
     * 固定时间间隔2小时
     */
    @Scheduled(fixedRate = 7200000)
    private void LikePersistence() {
        userArticleLikeService.persistence();
        LOG.info("定时任务:========== 点赞信息持久化:redis ---> mysql ==========");
    }
}

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐