点赞业务是一个常见的社交功能,它允许用户对其他用户的内容(如帖子、评论、图片等)表示喜欢或支持。在设计点赞业务时,需要考虑以下几个方面:
一、业务需求
点赞业务需要满足以下特性:
- 通用:点赞业务在设计的时候不要与业务系统耦合,必须同时支持不同业务的点赞功能。
- 独立:点赞功能是独立系统,并且不依赖其它服务,这样才具备可迁移性。
- 并发:一些热点业务点赞会很多,所以点赞功能必须支持高并发。
- 安全:要做好并发安全控制,避免重复点赞。
二、实现思路
为了保证安全,避免重复点赞,我们需要保存每一次点赞记录。同时,因为业务方经常需要根据点赞数量排序,因此每个业务的点赞数量也需要记录下来。
三、技术架构
在实现点赞业务时,主要用到了以下技术和工具:
- 该点赞业务实现通过微服务架构、数据库存储、Redis 缓存、消息队列和定时任务等技术,实现了通用、独立、高并发和安全的点赞功能。
- 通过 Nacos 进行服务的配置管理和服务发现,确保服务的可扩展性和配置的灵活性。
- 采用 RabbitMQ 进行消息的异步传递,实现了点赞操作和点赞数更新的解耦,同时使用 Feign 实现服务间通信,方便不同服务调用点赞服务。
- 利用 Redis 优化高并发读写操作,通过定时任务定期将 Redis 中的点赞数同步到数据库,确保数据的最终一致性。
四、数据库表设计
点赞的数据结构分两部分,一是点赞记录,二是与业务关联的点赞数。点赞数与具体业务表关联在一起记录,比如互动问答的点赞,就在问答表中记录点赞数。学员笔记点赞,自然是在笔记表中记录点赞数。
点赞表设计如下:
create database tj_remark;
use tj_remark;
CREATE TABLE IF NOT EXISTS `liked_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_id` bigint NOT NULL COMMENT '用户id',
`biz_id` bigint NOT NULL COMMENT '点赞的业务id',
`biz_type` VARCHAR(16) NOT NULL COMMENT '点赞的业务类型: qa-回答 note-笔记',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_biz_user` (`biz_id`,`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='点赞记录表';
五、微服务设计:
模块创建:创建一个独立的微服务来处理点赞业务。
依赖管理:添加必要的依赖,如数据库驱动、Redis、MQ等。
配置文件:配置服务的端口、数据库连接、缓存配置、消息队列配置等。
启动类:创建启动类,配置服务的基本信息和扫描路径。
代码生成:使用代码生成工具生成基本的代码框架和数据库操作类。
六、接口设计:
- 点赞/取消点赞接口:
- 接口路径:
/like
- 请求方法:
POST
- 请求参数:
bizId
:业务ID,标识被点赞的对象bizType
:业务类型,如qa
表示问答,note
表示笔记等
- 响应:无返回值,200表示成功
- 接口路径:
- 查询是否点赞接口:
- 接口路径:
/isLiked
- 请求方法:
GET
- 请求参数:
bizId
:业务ID,标识被查询的对象bizType
:业务类型,如qa
表示问答,note
表示笔记等
- 响应:
liked
:布尔值,表示用户是否已经点赞该对象
- 接口路径:
这些接口将满足用户点赞和查询点赞状态的需求。
七、业务流程
我们先梳理一下点赞业务的几点需求:
- 用户不能重复点赞
- 点赞就新增一条点赞记录,取消点赞就删除记录
- 点赞数由具体的业务方保存,需要通知业务方更新点赞数
由于业务方的类型很多,比如互动问答、笔记、课程等,所以通知方式必须是低耦合的,这里建议使用MQ来实现。
当点赞或取消点赞后,点赞数发生变化,我们就发送MQ通知。整体业务流程如图:
新增点赞功能实现
● 逻辑说明:
○ 接收 LikeRecordFormDTO 作为请求参数,包含业务信息。
○ addLikeRecord 方法根据 recordDTO 的 liked 属性判断是点赞还是取消点赞操作,调用 liked 或 unliked 方法。
○ liked 方法先检查用户是否已点赞,若未点赞则保存点赞记录;unliked 方法则删除用户的点赞记录。
○ 操作成功后,通过 RabbitMqHelper 发送 MQ 消息,通知其他服务点赞数发生了变化,消息发送到指定的交换器和路由键,内容包含业务 id 和点赞次数。
监听点赞数变更
● 在相关业务服务(如 tj-learning)中添加 MQ 监听器,监听点赞数变更的消息并更新数据库。
● 逻辑说明:
○ 使用 @RabbitListener 监听特定交换器和路由键的消息。
○ 收到消息后,将消息中的点赞数更新到相应业务表中,如 InteractionReply 表。
查询点赞状态功能实现
● 逻辑说明:
○ 接收一个业务 id 列表作为参数。
○ 通过 lambdaQuery 查询用户对这些业务是否已点赞。
○ 最终将用户已点赞的业务 id 以集合形式返回。
暴露 Feign 接口
在 tj-api 模块中定义 RemarkClient 作为 Feign 客户端,用于其他微服务调用点赞服务:
@FeignClient(value = "remark-service", fallbackFactory = RemarkClientFallback.class)
public interface RemarkClient {
//批量查询我的点赞状态
@GetMapping("/likes/list")
Set<Long> getLikedIds(@RequestParam("bizIds") List<Long> bizIds);
}
同时定义 RemarkClientFallback 作为服务降级处理:
@Slf4j
public class RemarkClientFallback implements FallbackFactory<RemarkClient> {
@Override
public RemarkClient create(Throwable cause) {
log.error("查询点赞服务异常", cause);
return new RemarkClient() {
@Override
public Set<Long> getLikedIds(List<Long> bizIds) {
return null;
}
};
}
}
调用
在查询回复或者评论的时候远程调用点赞微服务查询是否点过赞
八、优化思路
高并发读的优化:1. 优化SQL和代码 2. 添加缓存
高并发写的优化:1. 优化SQL和代码 2. 变同步写为异步写 3. 合并写请求
- 引入 Redis 缓存 ○ 使用 Redis 存储点赞记录和点赞数,以应对高并发问题。
○ 存储点赞记录使用 Set 数据结构,存储点赞数使用 ZSet 数据结构。
○ 点赞操作(addLikeRecord)通过 Redis 的 SADD 或 SREM 命令操作点赞记录,统计点赞数使用 SCARD 命令。
- 使用定时任务 ○ 创建定时任务类 LikedTimesTask,使用 @XxlJob 注解,调用 readLikedTimesAndSendMQ 方法。
○ 该方法从 Redis 中读取点赞数,转换数据后发送 MQ 消息,更新其他服务的点赞数。- 该方法从 Redis 中读取点赞数,转换数据后发送 MQ 消息,更新其他服务的点赞数。
@Component
@Slf4j
public class LikedTimesTask {
@Autowired
private ILikedRecordService likedRecordService;
@XxlJob("checkLikedTimes")
public ReturnT<String> checkLikedTime(String param) {
log.info("开始同步点赞次数");
likedRecordService.readLikedTimesAndSendMQ();
return ReturnT.SUCCESS;
}
}