项目文档:
预览地址(未开发完):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
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
@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
代码实现的逻辑基本和我上图中画的流程一致,但是我代码注释也写得很清楚,相信大家应当能读懂。
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
这个方式,就是我里面剖析的代码实现了。
到此,我们整个的一个业务点赞功能的实现即使完成了,其实其中肯定是有写不好的点和须要扩展的点:
假如那些你们有好的建议,可以评论里谈谈。