Redis + DB +消息队列 实现高效的文章点赞,点踩功能

需求说明

用户可点赞或踩,每赞一次,“赞”数量+1,每踩一次,“踩”数量+1,“点赞”和“点踩” 当天内二选一当天内有效
场景:用户A 点赞 文章a,文章a 点赞量+1 ,同一用户,同一文章 当天再次点击无效,赞与踩二选一,隔天再次点击有效

使用Redis 的 优缺点

优点
    1.Redis的数据存放在内存,速度快
    2.Redis计数器,完全符合此场景
    3.减轻数据库压力
    4.速度快提升用户体验
缺点:
    1.代码处理逻辑相对复杂
    2.需要单独处理持久化数据,包括使用消息队列异步持久化,或者定时任务都是可以的

表设计

文章的赞踩总记录表

CREATE TABLE `article_love_record`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `article_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章id',
  `love_type` tinyint(4) NOT NULL COMMENT '类型:1-赞;2-踩;',
  `love_times` int(11) NOT NULL DEFAULT 0 COMMENT '赞总次数',
  `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '新建时间',
  `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_article_id`(`article_id`) USING BTREE
  )ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER
 SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文章的赞踩总记录表' ROW_FORMAT = Dynamic;

文章赞踩历史记录表

CREATE TABLE `article_user_love_history_record`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `article_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章id',
  `user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id',
  `love_type` tinyint(4) NOT NULL COMMENT '类型:1-赞;2-踩;',
  `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '新建时间',
  `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER
SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文章赞踩历史记录表' ROW_FORMAT = Dynamic;

表设计说明:
使用一张历史表记录就可以实现所有统计,但添加另一张文章维度总计表的优势是,
如果缓存不存在,查询时需要查询db返回数据在放入缓存,
查询接口如果历史记录表数据庞大再使用count(*) 统计文章点赞踩的数量效率低下耗时长,用户体验差
如果有总计表只需要查询这张表即可

实现方案分析

上报用户喜好行为实现

  1. 使用redis 中Hash 计数器记录文章点赞数量统计信息,效期一个月
    key: 固定前缀+文章id
    filed:类型 1 赞 2 踩
    value:自增数量
    2)使用redis 中Hash 类型记录用户文章赞踩记录(主要用于限制当天是否可再点击)
    key: 固定前缀+文章id+yyyyMMdd
    filed:用户id
    value:用户文章点赞记录信息序列化后对象值
    注意:使用文章id维度记录缓存key的原因是相比用户维度,当天发布文章的数量更可控,设置失效时间进行淘汰
    使用用户维度记录若突然用户当日猛增,造成key值会因为用户的数量而猛增不可控,另一方面尽量减少redis的key数量
    减少空间。
    3)添加用户喜好行为信息到消息队列,异步监听排队处理持久化到数据库中
    本次功能演示中未涉及消息队列处理持久化过程,请自行处理
    主要步骤 1.添加文章喜好记录历史表 2.根据文章id累加文章的喜好总记录表

查询文章喜好行为实现

入参:文章id ,用户id
出参:文章总赞数量,文章总踩数量,当前用户是否可赞,当前用户是否可踩
1.判断当前文章缓存信息是否存在
1.1 不存在
1.1.1根据文章id 查询db文章的赞踩数(此处可以添加锁防止缓存穿透),返回并更新到缓存
1.2 存在
1.2.1根据当前文章id拼接缓存key值,查询当前文章缓存的点赞踩数量值并返回
2.用户是否可以再次点击信息处理
2.1 redis 中查询是否存在,存在则不可再次操作

java代码实现

import com.boot.redis.constant.CacheKey;
import com.boot.redis.dto.UserArticleLoveActionCacheDto;
import com.boot.redis.enums.UserArticleLoveActionEnum;
import com.boot.redis.jredis.RedisCache;
import com.boot.redis.service.UserArticleLoveActionService;
import com.boot.redis.vo.ArticleUserLoveVO;
import com.xiaoleilu.hutool.date.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 文章点赞、踩行为接口
 */
@Service
public class UserArticleLoveActionServiceImpl implements UserArticleLoveActionService {
    private Logger LOGGER = LoggerFactory.getLogger(UserArticleLoveActionServiceImpl.class);
    private static final Integer INCREMENT_ONE = 1;
    @Autowired
    private RedisCache redisCache;

    /**
     * 上报用户文章喜好行为
     *
     * @param articleId 文章id
     * @param userId    用户id
     * @param type      类型 1 点赞 2 点踩
     *                  步骤
     *                  1.使用redis 计数器,累计文章总点赞,踩数量
     *                  2.记录用户的赞踩行为到Redis (用于判断用户是否可再次点击)
     *                  3.发送消息事件,异步处理 持久化记录到DB
     */
    @Override
    public void uploadLoveAction(String articleId, String userId, int type) {
        //保存文章赞踩行为统计到缓存 (因为要定时过期,所以需要按照单个文章维度存储,方便单个key过期)
        this.doCacheLoveActionCount(articleId, String.valueOf(type), INCREMENT_ONE);
        //记录当前用户赞踩行为到缓存
        this.doCacheUserArticleLoveAction(articleId, userId, type);
        LOGGER.info("此处代替发消息到消息队列代码");
        LOGGER.info("demo 项目省略 消息处理代码以及消息消费持久化处理代码!");
    }

    /**
     * 获取文章详情赞踩数
     *
     * @param articleId 文章id
     * @param userId    用户id
     */
    @Override
    public ArticleUserLoveVO getArticleLoveDetail(String articleId, String userId) {
        //当前文章赞踩数量获取返回对象
        ArticleUserLoveVO result = new ArticleUserLoveVO();
        result.setArticleId(articleId);
        // 处理文章总赞、踩数量
        this.processArticleActionCount(result, articleId);
        //处理用户是否可以赞踩
        this.processUserCanAction(result, userId, articleId);
        return result;
    }

    /**
     * 处理文章赞、踩数量
     */
    private void processArticleActionCount(ArticleUserLoveVO result, String articleId) {
        //记录文章总点赞踩数量 缓存key
        String cacheKey = CacheKey.ARTICLE_LOVE_KEY.concat(articleId);

        //类型枚举值
        String loveType = String.valueOf(UserArticleLoveActionEnum.LOVE.getValue());
        String hateType = String.valueOf(UserArticleLoveActionEnum.HATE.getValue());
        int loveNum = 0;
        int hateNum = 0;
        //缓存是否存在
        boolean keyExists = redisCache.exists(cacheKey);
        if (keyExists) {
            //文章赞数量
            Integer loveHashNum = redisCache.getHash(cacheKey, loveType, Integer.class);
            if (null != loveHashNum) {
                loveNum = loveHashNum;
            }
            //文章踩数量
            Integer hateHashNum = redisCache.getHash(cacheKey, hateType, Integer.class);
            if (null != hateHashNum) {
                hateNum = hateHashNum;
            }
        } else {
            //不存在则db取
            //文章已赞数量
            loveNum = this.getArticleLoveNumFromDB(articleId);
            //文章已踩数量
            hateNum = this.getArticleHateNumFromDB(articleId);

            //已赞总数量 重新进入缓存
            this.doCacheLoveActionCount(articleId, loveType, loveNum);
            //已踩总数量 重新进入缓存
            this.doCacheLoveActionCount(articleId, hateType, hateNum);
        }
        result.setLoveNum(loveNum);
        result.setHateNum(hateNum);
    }

    /**
     * demo 项目
     * 仅做模拟数据库查询点赞数量
     */
    private int getArticleLoveNumFromDB(String articleId) {
        return 101;
    }

    /**
     * demo 项目
     * 仅做模拟数据库查询点踩数量
     */
    private int getArticleHateNumFromDB(String articleId) {
        return 99;
    }


    /**
     * 处理用户是否可赞、踩
     */
    private void processUserCanAction(ArticleUserLoveVO result, String userId, String articleId) {
        //判断用户是否可点击
        String userLoveCacheKey = this.getUserArticleLoveActionKey(articleId);
        //缓存中查询
        UserArticleLoveActionCacheDto loveActionCacheDto =
                redisCache.getHash(userLoveCacheKey, userId, UserArticleLoveActionCacheDto.class);

        boolean canLoveFlag = true;
        boolean canHateFlag = true;
        if (null != loveActionCacheDto) {
            canLoveFlag = false;
            canHateFlag = false;
        }
        result.setCanLoveFlag(canLoveFlag);
        result.setCanHateFlag(canHateFlag);
    }

    /**
     * 记录文章用户赞踩记录行为
     * 按照天的维度统计,零点重置
     * 缓存 hash类型 key:固定前缀+文章id+yyyyMMdd
     * filed:用户id
     * value:用户喜好记录对象信息
     * 注:按照文章维度做大key hash 存储原因
     * 文章每天平台发布的数量是可控的,但是用户访问量是不可控的,用户量某一天暴增的时候缓存key就特别多
     * redis 内存也是很宝贵的
     *
     * @param articleId 文章id
     * @param userId    人员id
     * @param type      类型
     */
    private void doCacheUserArticleLoveAction(String articleId, String userId, int type) {
        //用户文章点赞、踩缓存key =文章id+分隔符+yyyyMMdd
        String cacheKey = this.getUserArticleLoveActionKey(articleId);

        //赞踩中间对象
        UserArticleLoveActionCacheDto loveActionCacheDto = new UserArticleLoveActionCacheDto();
        loveActionCacheDto.setArticleId(articleId);
        loveActionCacheDto.setType(type);

        //保存到hash (相同文章下,不同人员进行区分)
        redisCache.setHash(cacheKey, userId, loveActionCacheDto);
        //过期时间1天(1天后用户可以再次点击)
        redisCache.expire(cacheKey, 1, TimeUnit.DAYS);
    }

    /**
     * 获取用户文章点赞、踩缓存key
     */
    private String getUserArticleLoveActionKey(String articleId) {
        String regDate = DateUtil.format(new Date(), "yyyyMMdd");
        String cacheKey = CacheKey.USER_ARTICLE_LOVE_KEY
                .concat(articleId)
                .concat("_")
                .concat(regDate);
        return cacheKey;
    }

    /**
     * 保存点赞或踩行为 到缓存
     * 并添加失效时间 30 天
     *
     * @param articleId 文章id
     * @param type      类型
     * @param increment 增量值
     */
    private void doCacheLoveActionCount(String articleId, String type, int increment) {
        //记录文章点赞踩数量 缓存key
        String cacheKey = CacheKey.ARTICLE_LOVE_KEY.concat(articleId);
        // 使用 redis, hash类型 按照类型区分的计数器
        redisCache.hashIncrBy(cacheKey, type, increment);
        //文章计数缓存保存30 天
        redisCache.expire(cacheKey, 30, TimeUnit.DAYS);
    }
}

注:redis 的配置实现 可以参考
https://blog.csdn.net/qq_38011415/article/details/82823778

Controller实现

/**
 * 用户赞踩行为事件记录controller
 */
@RestController
public class UserArticleLoveActionController {

    @Autowired
    private UserArticleLoveActionService loveActionService;

    /**
     * 上报用户文章赞、踩行为
     */
    @RequestMapping("/article/upload-love")
    public void uploadLoveAction(@RequestBody UserArticleLoveUploadBO articleLoveUploadBO) {
        String userId = articleLoveUploadBO.getUserId();
        String articleId = articleLoveUploadBO.getArticleId();
        Integer type = articleLoveUploadBO.getType();
        loveActionService.uploadLoveAction(articleId, userId, type);
    }

    /**
     * 获取文章详情赞踩数
     *
     */
    @RequestMapping("/article/love-detail")
    public ArticleUserLoveVO getArticleLoveDetail(@RequestBody UserArticleLoveUploadBO articleLoveUploadBO) {
        String userId = articleLoveUploadBO.getUserId();
        String articleId = articleLoveUploadBO.getArticleId();
        return loveActionService.getArticleLoveDetail(articleId, userId);
    }
}

猜你喜欢

转载自blog.csdn.net/qq_38011415/article/details/83512064