爱收集资源网

分析点赞功能如何来实现?(附文档文档)

网络整理 2024-02-09 08:04

项目文档:

预览地址(未开发完):admire.j3code.cn/small-boss

1、分析

点赞功能我相信你们都不陌生,打开同学圈、B站、抖音、小红书等关于社交相关的功能基本都有点赞这一功能,所以本篇,俺们就来剖析一下点赞功能怎样来实现。

首先点赞肯定是对某一类数据进行点赞,如:B站就有视频点赞、评论据赞、动态点赞等。所以,根据我们常规的套路,是不是就应当有视频点赞插口,评论据赞插口,动态点赞插口等,但事实果真如此吗?

你看我这样设计,是否可行:

这样通过不同的参数,将多个不同数据的点赞插口归到一个插口当中实现,这样是不是降低了何必要的冗余代码。但骤然而至的问题也是显而易见的,原先点赞功能的流量被分散到三个或多个插口当中,而如今都归到一个插口来实现,那所有的流量就就会打到一个点赞插口当中,这对插口性能就要求很高了。

如今晓得插口如何设计了,那再来看看点赞表怎样设计?

按照插口,我们可以确定这几个数组

为何没有数据类型?

我们回想一下,插口接入数据类类型是为了适配更多的不同数据的点赞业务。而点赞的记录表有一个点赞的数据ID,就可以锁定那条数据被点赞了,所以无需数据类型ID。

其实为了确保晓得用户点赞了那条数据,所以还须要如下数组:

所以最后,我们的点在表SQL就是下边这样:

CREATE TABLE `sb_like` (
  `id` bigint(20) NOT NULL,
  `item_id` bigint(20) NOT NULL COMMENT '点赞条目id',
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `like` tinyint(1) NOT NULL COMMENT '是否点赞,true点赞,false未点赞',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un` (`item_id`,`user_id`),
  KEY `k` (`item_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

说明:

2、实现

如今依照前面的剖析,可以得出点赞恳求的简单数据流向如右图:

可以看见假如系统中的业务数据越来越多的话,点赞插口的访问量将会显得越来越大,因而弄成一个高频访问插口(其实,点赞本身就是一个高频率动作)。

所以,面对一个高频插口,俺们的点赞记录肯定是不能直接入MySQL的,所以这儿俺们的点赞数据第一步肯定是入Redis。

一个是基于c盘IO操作(MySQL),一个是基于显存操作(Redis)。

那,既然引入Redis网页点赞,肯定又会引申出一些其它问题,如:

针对这三个问题,也好解决,我把解决的大致思路花了张图,相信一图胜千言:

注:Redis的储存数据类型为hash

整理房间日记200_网页点赞_微信打字九宫格怎么设置

ok,下边就是我们的编码实现环节了,先来写点赞插口,这个特别好实现。

2.1点赞插口实现

1)controller

位置:cn.j3code.community.api.v1.controller

@Slf4j
@ResponseResult
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.WEB_V1 + "/like")
public class LikeController {
    private final LikeService likeService;
    /**
     * 点赞
     * @param request
     */
    @PostMapping("/")
    public void like(@Validated @RequestBody LikeRequest request) {
        likeService.like(request);
    }
}

LikeRequest对象

位置:cn.j3code.community.api.v1.request

@Data
public class LikeRequest {
    @NotNull(message = "条目id不为空")
    private Long itemId;
    @NotNull(message = "点赞不为空")
    private Boolean like;
    @NotNull(message = "类型不为空")
    private CommentTypeEnum type;
}

CommentTypeEnum枚举

位置:cn.j3code.config.enums

@Getter
public enum CommentTypeEnum {
    COMMODITY(1, "商品评论"),
    POST_COMMENT(2, "帖子评论"),
    POST(3, "帖子"),
    ;
    @EnumValue
    private Integer value;
    private String description;
    CommentTypeEnum(Integer value, String description) {
        this.value = value;
        this.description = description;
    }
}

2)service

位置:cn.j3code.community.service

public interface LikeService extends IService {
    void like(LikeRequest request);
}
@Slf4j
@AllArgsConstructor
@Service
public class LikeServiceImpl extends ServiceImpl
    implements LikeService {
    private final RedisTemplate redisTemplate;
    @Override
    public void like(LikeRequest request) {
        redisTemplate.opsForHash().put(
            SbUtil.getItemLikeKey(request.getType().getValue() + ":" + request.getItemId()),
            Objects.requireNonNull(SecurityUtil.getUserId(), "获取登录人信息出错!").toString(),
            request.getLike());
    }
}

是不是特别简单,只须要组装好hash结构的数据网页点赞,访问一下Redis即可。

2.2点赞数据同步数据库实现

后面我们谈到过,持久化是通过定时任务进行的,所以这儿有一点点问题就是假如在定时任务还未执行的时侯,Redis挂了,那这段时间的点赞记录将会遗失。上线的时侯,Redis的持久化功能要记得配好(AOF/RDB)。

如今,我们来剖析剖析点赞数据同步到数据库的流程:

获取对应数据类型的所有点赞key按照获取到的key,获取所有的点赞记录,数据格式为Map结合Redis点赞记录+MySQL持久化记录,估算出同一条数据的最终点赞状态和数据的点赞数目移除redis中,早已同步的数据更新业务数据点赞数目插入用户点赞记录数据

这儿,可能2、3点你们有点不好理解,没关系,我先张流程图整体来熟悉这个流程,后再来剖析大家疑虑的点:

在图中,我早已把能解释的问题都早已解释清楚了,下边就看代码实现吧!

由于点赞业务有好多,所以对应的定时任务肯定也是不同的,这儿我以本项目的贴子为例,来实现贴子数据点赞数据的同步。

1)schedule

位置:cn.j3code.community.schedule

整理房间日记200_网页点赞_微信打字九宫格怎么设置

@Slf4j
@Component
@AllArgsConstructor
public class PostLikeSchedule {
    private final PostService postService;
    /**
     * 同步帖子点赞
     */
    @DistributedLock
    @Scheduled(cron = "11 0/9 * * * ?")
    public void syncPostLike(){
        postService.syncPostLike();
    }
}

2)service

位置:cn.j3code.community.service

public interface PostService extends IService {
    void syncPostLike();
}
@Slf4j
@AllArgsConstructor
@Service
public class PostServiceImpl extends ServiceImpl
    implements PostService {
    
    private final LikeServiceImpl likeService;
    private final RedisTemplate redisTemplate;
    private final TransactionTemplate transactionTemplate;
    @Override
    public void syncPostLike() {
        // 获取帖子点赞 key
        List keys = new ArrayList<>(redisTemplate.keys(SbUtil.getItemLikeKey(CommentTypeEnum.POST.getValue() + ":*")));
        if (CollectionUtils.isEmpty(keys)) {
            return;
        }
        Set commentIdList = keys.stream().map(key -> Long.valueOf(key.substring(key.lastIndexOf(":") + 1)))
            .collect(Collectors.toSet());
        // 批量查看 redis 评论id,的点赞数据
        ItemLikeBO itemLikeBO = likeService.getItemLikeCount(new ArrayList<>(commentIdList), CommentTypeEnum.POST, Boolean.TRUE);
        Map itemLikeCount = itemLikeBO.getItemLikeCount();
        /**
         * 帖子id 对应,用户id 和 点赞状态 的 map
         */
        Map> postToUserLikeMap = itemLikeBO.getItemIdToUserLikeMap();
        // 待修改的评论的点赞数量集合
        List updatePostList = new ArrayList<>();
        // 待插入的点赞集合
        List saveLikeList = new ArrayList<>();
        postToUserLikeMap.forEach((postId, likeMap) -> {
            Post post = new Post();
            post.setId(postId);
            post.setLikeCount(itemLikeCount.get(postId));
            updatePostList.add(post);
            likeMap.forEach((key, value) -> {
                Like like = new Like();
                like.setId(SnowFlakeUtil.getId());
                like.setItemId(postId);
                like.setUserId(key);
                like.setLike(value);
                like.setCreateTime(LocalDateTime.now());
                like.setUpdateTime(LocalDateTime.now());
                saveLikeList.add(like);
            });
        });
        Map postMap = lambdaQuery()
            .in(Post::getId, postToUserLikeMap.keySet()).list()
            .stream().collect(Collectors.toMap(Post::getId, item -> item));
        // 点赞数量等于 数据库 + redis
        updatePostList.forEach(item ->
                               item.setLikeCount(item.getLikeCount() + postMap.get(item.getId()).getLikeCount()));
        String format = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        log.info(format + "-同步点赞数据:updatePostList={},saveLikeList={}",
                 JSON.toJSONString(updatePostList),
                 JSON.toJSONString(saveLikeList));
        // 修改数据库
        MyTransactionTemplate.execute(transactionTemplate, accept -> {
            CollUtil.split(updatePostList, 100).forEach(this::updateBatchById);
            CollUtil.split(saveLikeList, 100).forEach(likeService::saveOrUpdateByDuplicate);
        }, format + "-同步帖子点赞逻辑出错!");
    }
}

其中:likeService.getItemLikeCount的代码就是上图中的第3、4、5的实现,它是点赞逻辑的公用方式,后期批量点赞成步、其它类型同步对应的3、4、5逻辑都是调用该方式实现的。代码如下:

3)getItemLikeCount方式实现

位置:cn.j3code.community.service

public interface LikeService extends IService {
    /**
     *
     * @param itemIdList 条目id集合
     * @param type 条目的数据类型
     * @param redisDataRemove 统计完后,是否移除redis中的数据
     * @return
     */
    ItemLikeBO getItemLikeCount(List itemIdList, CommentTypeEnum type, Boolean redisDataRemove);
}
@Slf4j
@AllArgsConstructor
@Service
public class LikeServiceImpl extends ServiceImpl
    implements LikeService {
    private final RedisTemplate redisTemplate;
    @Override
    public ItemLikeBO getItemLikeCount(List itemIdList, CommentTypeEnum type, Boolean redisDataRemove) {
        ItemLikeBO itemLikeBO = new ItemLikeBO();
        Map result = new HashMap<>();
        itemIdList.forEach(itemId -> result.put(itemId, 0));
        // 批量查看 redis 评论id,的点赞数据
        List executePipelined = redisTemplate.executePipelined(new SessionCallback<>() {
            @Override
            public  Object execute(RedisOperations redisOperations) throws DataAccessException {
                for (Long itemId : itemIdList) {
                    redisOperations.opsForHash().entries((K) SbUtil.getItemLikeKey(type.getValue() + ":" + itemId));
                }
                return null;
            }
        });
        /**
         * 评论id 对应,用户id 和 点赞状态 的 map
         */
        Map> commentToUserLikeMap = new HashMap<>();
        for (int i = 0; i < itemIdList.size(); i++) {
            if (Objects.isNull(executePipelined.get(i))) {
                continue;
            }
            Long commentId = itemIdList.get(i);
            Map likeMap = (Map) executePipelined.get(i);
            Map lb = new HashMap<>();
            likeMap.forEach((k, v) -> lb.put(Long.valueOf(k), v));
            commentToUserLikeMap.put(commentId, lb);
            if (redisDataRemove) {
                /**
                 * 这里会有一点点问题,就是如果在获取点赞 与 删除点赞 的时间空隙之间,同一个用户又操作了同一个评论的点赞
                 * 那这将会导致数据丢失
                 * 解决方法:
                 * 1、加锁
                 * 2、改用 MQ 方式
                 */
                // 删除一下 redis 中的 点赞 记录
                redisTemplate.opsForHash().delete(
                    SbUtil.getItemLikeKey(type.getValue() + ":" + commentId),
                    likeMap.keySet().stream().map(Object::toString).toArray(Object[]::new));
            }
        }
        for (int i = 0; i < itemIdList.size(); i++) {
            Map redisUserLikeMap = commentToUserLikeMap.get(itemIdList.get(i));
            if (Objects.nonNull(redisUserLikeMap) && CollectionUtils.isNotEmpty(redisUserLikeMap.keySet())) {
                // 查询数据库中,该评论的点赞记录
                Map dbUserLikeMap = lambdaQuery()
                    .eq(Like::getItemId, itemIdList.get(i))
                    .eq(Like::getLike, Boolean.TRUE)
                    .in(Like::getUserId, redisUserLikeMap.keySet())
                    .list().stream().collect(Collectors.toMap(Like::getUserId, Like::getLike));
                // redis 与 数据库点赞记录 结合
                AtomicInteger redisLikeCount = new AtomicInteger(0);
                for (Map.Entry entry : redisUserLikeMap.entrySet()) {
                    // 数据库存在点赞,redis 取消点赞,那点赞数量减一
                    if (dbUserLikeMap.containsKey(entry.getKey()) && Boolean.FALSE.equals(entry.getValue())) {
                        redisLikeCount.set(redisLikeCount.get() - 1);
                    }
                    // 数据库存在点赞,redis 存在点赞,那 Redis 点赞状态变为 false,不记录总点赞数量中
                    if (dbUserLikeMap.containsKey(entry.getKey()) && Boolean.TRUE.equals(entry.getValue())) {
                        entry.setValue(Boolean.FALSE);
                    }
                }
                redisLikeCount.set(redisLikeCount.get() +
                                   Integer.parseInt(redisUserLikeMap.values().stream().filter(like -> like).count() + "")
                                  );
                result.put(itemIdList.get(i), redisLikeCount.get());
            }
        }
        itemLikeBO.setItemLikeCount(result);
        itemLikeBO.setItemIdToUserLikeMap(commentToUserLikeMap);
        return itemLikeBO;
    }
}

代码实现的逻辑基本和我上图中画的流程一致,但是我代码注释也写得很清楚,相信大家应当能读懂。

2.3数据查询填土点赞记录

那最后一个功能就是查询业务数据的时侯我们除了要吧MySQL中的点赞数据查下来,还要把Redis中的数据一齐查下来进行组装,最后才把数据回显到页面。

不过,这个逻辑不是很难,由于有了2.2节的公共方式的实现,所以这一步将会显得简单些。

剖析该逻辑之前,我们先来瞧瞧,页面要显示点赞的这些数据,页面如下:

ok,我们先来剖析用户点赞状态的获取流程:

将查询到的业务数据集合转为Map,初始情况下,状态都为false按照业务ID、用户ID、点赞状态(true),查询MySQL的点赞记录,将对应的数据填筑到第一步的Map中再通过管线操作,批量访问Redis,找到hashkey为业务id,属性key为用户id的点赞记录,将找到的数据直接覆盖到Map中最终,Map中的数据,就是用户是否点赞业务的标示了。

而点赞数目我就不剖析业务流程了,由于2.2节早已剖析过了就是getItemLikeCount方式的实现,只不过这儿的redisDataRemove参数为false,不须要移除redis的数据,由于这是一个查询,而不是同步。

下边,来瞧瞧我的查询业务数据列表伪代码实现:

用伪代码实现是由于查询的流程基本就是先查业务数据,之后填土点赞数据,而前一步就没必要和你们说了,我用伪代码彰显填土点赞的数据即可,相信你们都懂。

public IPage page(PostPageRequest request) {
    // 先查询 MySQL 的业务数据
    // 用户评论点赞状态
    Map itemIdToLikeMap = likeService.getItemLikeState(voiPage.getRecords().stream().map(PostVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST);
    // redis 中评论点赞数量
    Map itemIdToLikeCountMap = likeService.getItemLikeCount(voiPage.getRecords().stream().map(PostVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST, Boolean.FALSE)
        .getItemLikeCount();
    // 填充评论点赞数量及当前用户点赞状态,用户信息
    业务数据列表.forEach(postVO -> {
        设置业务数据点赞数据(业务MySQL点赞数量 + itemIdToLikeCountMap.get(postVO.getId()));
        设置业务数据用户点赞状态(itemIdToLikeMap.get(postVO.getId()));
    });
}

getItemLikeState方式实现:

public Map getItemLikeState(List itemIdList, CommentTypeEnum type) {
    Map result = new HashMap<>();
    itemIdList.forEach(itemId -> result.put(itemId, Boolean.FALSE));
    if (Objects.isNull(SecurityUtil.getUserId())) {
        // 未登录
        return result;
    }
    if (CollectionUtils.isEmpty(itemIdList)) {
        return result;
    }
    // 查看数据库中是否有用户点赞记录
    lambdaQuery()
        .eq(Like::getUserId, SecurityUtil.getUserId())
        .eq(Like::getLike, Boolean.TRUE)
        .in(Like::getItemId, itemIdList)
        .list().forEach(likeObj -> {
        if (likeObj.getLike()) {
            result.put(likeObj.getItemId(), Boolean.TRUE);
        }
    });
    // 查看 redis 用户点赞记录
    List executePipelined = redisTemplate.executePipelined(new SessionCallback<>() {
        @Override
        public  Object execute(RedisOperations redisOperations) throws DataAccessException {
            for (Long itemId : itemIdList) {
                redisOperations.opsForHash().get(
                    (K) SbUtil.getItemLikeKey(type.getValue() + ":" + itemId),
                    SecurityUtil.getUserId().toString());
            }
            return null;
        }
    });
    for (int i = 0; i < itemIdList.size(); i++) {
        if (Objects.nonNull(executePipelined.get(i))) {
            result.put(itemIdList.get(i), (Boolean) executePipelined.get(i));
        }
    }
    return result;
}

这个方式,就是我里面剖析的代码实现了。

到此,我们整个的一个业务点赞功能的实现即使完成了,其实其中肯定是有写不好的点和须要扩展的点:

假如那些你们有好的建议,可以评论里谈谈。

网页点赞