网站开发单子,需要做网站的公司,响应式网站开发步骤,网站上传不了图片是什么原因点赞功能是社交、电商等几乎所有的互联网项目中都广泛使用。虽然看起来简单#xff0c;不过蕴含的技术方案和手段还是比较多的。
下面将分享之前做的判题OJ系统的点赞系统的思路。
1.需求分析
点赞功能与其它功能不同#xff0c;没有复杂的原型和需求#xff0c;仅仅是一…点赞功能是社交、电商等几乎所有的互联网项目中都广泛使用。虽然看起来简单不过蕴含的技术方案和手段还是比较多的。
下面将分享之前做的判题OJ系统的点赞系统的思路。
1.需求分析
点赞功能与其它功能不同没有复杂的原型和需求仅仅是一个点赞、取消点赞的操作。所以今天我们就不需要从原型图来分析而是仅仅从这个功能的实现方案来思考。
1.1.业务需求
首先我们来分析整理一下点赞业务的需求一个通用点赞系统需要满足下列特性
通用点赞业务在设计的时候不要与业务系统耦合必须同时支持不同业务的点赞功能独立点赞功能是独立系统并且不依赖其它服务。这样才具备可迁移性。并发一些热点业务点赞会很多所以点赞功能必须支持高并发安全要做好并发安全控制避免重复点赞
1.2.实现思路
要保证安全避免重复点赞我们就必须保存每一次点赞记录。只有这样在下次用户点赞时我们才能查询数据判断是否是重复点赞。同时因为业务方经常需要根据点赞数量排序因此每个业务的点赞数量也需要记录下来。
综上点赞的基本思路如下 但问题来了我们说过点赞服务必须独立因此必须抽取为一个独立服务。多个其它微服务业务的点赞数据都有点赞系统来维护。但是问题来了 如果业务方需要根据点赞数排序就必须在数据库中维护点赞数字段。但是点赞系统无法修改其它业务服务的数据库否则就出现了业务耦合。该怎么办呢 点赞系统可以在点赞数变更时通过MQ通知业务方这样业务方就可以更新自己的点赞数量了。并且还避免了点赞系统与业务方的耦合。
于是实现思路变成了这样 2.数据结构
点赞的数据结构分两部分一是点赞记录二是与业务关联的点赞数。
点赞数是跟具体业务表关联在一起比如题目讨论区的点赞自然是在题目表中记录点赞数。问答区的话自然在问答表中记录点赞数。为什么呢我们不可能将点赞数表放在点赞系统的数据库表里因为查询题目列表不得每次用Feign调用每一条题目数据的点赞数这样是不可行的。
因此本节我们只需要实现点赞记录的表结构设计即可。
2.1.ER图
点赞记录本质就是记录谁给什么内容点了赞所以核心属性包括
点赞目标id点赞人id
不过点赞的内容多种多样为了加以区分我们还需要把点赞内的类型记录下来
点赞对象类型为了通用性
当然还有点赞时间综上对应的数据库ER图如下 2.2.表结构
由于点赞系统是独立于其它业务的这里我们需要创建一个新的数据库hjx_remark
CREATE DATABASE hjx_remark CHARACTER SET utf8mb4;然后在ER图基础上加上一些通用属性点赞记录表结构如下
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 点赞的业务类型,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)
) ENGINEInnoDB AUTO_INCREMENT8 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT点赞记录表;
点赞统计表
CREATE TABLE IF NOT EXISTS liked_stat (id bigint NOT NULL AUTO_INCREMENT COMMENT 主键id,liked_times int NOT NULL COMMENT 点赞数量,biz_id bigint NOT NULL COMMENT 点赞的业务id,biz_type VARCHAR(16) NOT NULL COMMENT 点赞的业务类型,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)
) ENGINEInnoDB AUTO_INCREMENT8 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT点赞统计表;
2.3.代码生成
略
3.实现点赞功能
从表面来看点赞功能要实现的接口就是一个点赞接口。不过仔细观察所有的点赞页面你会发现点赞按钮有灰色和点亮两种状态。
也就是说我们还需要实现查询用户点赞状态的接口这样前端才能根据点赞状态渲染不同效果。因此我们要实现的接口包括
点赞/取消点赞根据多个业务id批量查询用户是否点赞多个业务比如查询多个题目id对应的用户点赞情况
3.1.点赞或取消点赞
3.1.1.接口信息
当用户点击点赞按钮的时候第一次点击是点赞按钮会高亮第二次点击是取消点赞按钮变灰
从后台实现来看点赞就是新增一条点赞记录取消就是删除这条记录。为了方便前端交互这两个合并为一个接口即可。
因此请求参数首先要包含点赞有关的数据并且要标记是点赞还是取消
点赞的目标业务idbizId谁在点赞就是登陆用户可以不用提交点赞还是取消
除此以外我们之前说过在问答、题目讨论等功能中都会出现点赞功能所以点赞必须具备通用性。因此还需要在提交一个参数标记点赞的类型
点赞目标的类型
返回值有两种设计
方案一无返回值200就是成功页面直接把点赞数1展示给用户即可弱一致性但为了性能方案二返回点赞数量页面渲染
这里推荐使用方案一因为每次统计点赞数量也有很大的性能消耗。
综上按照Restful风格设计接口信息如下
接口说明用户可以给自己喜欢的内容点赞也可以取消点赞请求方式POST请求路径/likes请求参数格式{ bizId: 1578558664933920770, // 点赞业务id如题目id bizType: 1, // 点赞业务类型1问答区2题目讨论.. liked: true, // 是否点赞true点赞false取消 }返回值格式无
3.1.2.实体代码实现
Data
ApiModel(description 点赞记录表单实体)
public class LikeRecordFormDTO {ApiModelProperty(点赞业务id)NotNull(message 业务id不能为空)private Long bizId;ApiModelProperty(点赞业务类型)NotNull(message 业务类型不能为空)private String bizType;ApiModelProperty(是否点赞true点赞false取消点赞)NotNull(message 是否点赞不能为空)private Boolean liked;
}
/*** p* 点赞记录表 控制器* /p*/
RestController
RequiredArgsConstructor
RequestMapping(/likes)
Api(tags 点赞业务相关接口)
public class LikedRecordController {private final ILikedRecordService likedRecordService;PostMappingApiOperation(点赞或取消点赞)public void addLikeRecord(Valid RequestBody LikeRecordFormDTO recordDTO) {likedRecordService.addLikeRecord(recordDTO);}
}3.1.4.业务流程
梳理一下点赞业务的几点需求
点赞就新增一条点赞记录取消点赞就删除记录用户不能重复点赞点赞数由具体的业务方保存需要通知业务方更新点赞数
由于业务方的类型很多比如互动问答、题目问答等。所以通知方式必须是低耦合的这里建议使用MQ来实现。
当点赞或取消点赞后点赞数发生变化我们就发送MQ通知。整体业务流程如图 需要注意的是由于每次点赞的业务类型不同所以没有必要通知到所有业务方而是仅仅通知与当前点赞业务关联的业务方即可。
在RabbitMQ中利用TOPIC类型的交换机结合不同的RoutingKey可以实现通知对象的变化。我们需要让不同的业务方监听不同的RoutingKey然后发送通知时根据点赞类型不同发送不同RoutingKey 当然真实的RoutingKey不一定如图中所示这里只是做一个示意。
3.1.5.实现完整业务
首先我们需要定义一个MQ通知的消息体由于这个消息体会在各个相关微服务中使用需要定义到公用的模块中
Data
NoArgsConstructor
AllArgsConstructor
public class LikedTimesDTO {/*** 点赞的业务id*/private Long bizId;/*** 总的点赞次数*/private Integer likedTimes;
}然后是com.tianji.remark.service.impl.LikedRecordServiceImpl完整的业务逻辑
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.util.List;
import java.util.stream.Collectors;/*** p* 点赞记录表 服务实现类* /p*/
Service
RequiredArgsConstructor
public class LikedRecordServiceImpl extends ServiceImplLikedRecordMapper, LikedRecord implements ILikedRecordService {private final RabbitMqHelper mqHelper;Overridepublic void addLikeRecord(LikeRecordFormDTO recordDTO) {// 1.基于前端的参数判断是执行点赞还是取消点赞boolean success recordDTO.getLiked() ? like(recordDTO) : unlike(recordDTO);// 2.判断是否执行成功如果失败则直接结束if (!success) {return;}// 3.如果执行成功根据业务id统计点赞总数Integer likedTimes lambdaQuery().eq(LikedRecord::getBizId, recordDTO.getBizId()).count();// 4.发送MQ通知mqHelper.send(LIKE_RECORD_EXCHANGE,StringUtils.format(LIKED_TIMES_KEY_TEMPLATE, recordDTO.getBizType()),LikedTimesDTO.of(recordDTO.getBizId(), likedTimes));}private boolean unlike(LikeRecordFormDTO recordDTO) {return remove(new QueryWrapperLikedRecord().lambda().eq(LikedRecord::getUserId, UserContext.getUser()).eq(LikedRecord::getBizId, recordDTO.getBizId()));}private boolean like(LikeRecordFormDTO recordDTO) {Long userId UserContext.getUser();// 1.查询点赞记录//幂等性校验Integer count lambdaQuery().eq(LikedRecord::getUserId, userId).eq(LikedRecord::getBizId, recordDTO.getBizId()).count();// 2.判断是否存在如果已经存在直接结束if (count 0) {return false;}// 3.如果不存在直接新增LikedRecord r new LikedRecord();r.setUserId(userId);r.setBizId(recordDTO.getBizId());r.setBizType(recordDTO.getBizType());save(r);return true;}
}3.2.批量查询点赞状态
由于这个接口是供其它微服务调用实现完成接口后还需要定义对应的FeignClient
3.2.1.接口信息
这里是查询多个业务的点赞状态因此请求参数自然是业务id的集合。由于是查询当前用户的点赞状态因此无需传递用户信息。
经过筛选判断后我们把点赞过的业务id集合返回即可。
综上按照Restful来设计该接口接口信息如下
接口说明查询当前用户是否点赞了指定的业务请求方式GET请求路径/likes/list请求参数格式请求数据类型:application/x-www-form-urlencoded例如bizIds1,2,3 代表业务id集合返回值格式[ 业务id1, 业务id2, 业务id3, 业务id4 ]
3.3.2.代码
首先是tj-remark的com.hjx.remark.controller.LikedRecordController
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import java.util.List;
import java.util.Set;/*** p* 点赞记录表 控制器* /p*/
RestController
RequiredArgsConstructor
RequestMapping(/likes)
Api(tags 点赞业务相关接口)
public class LikedRecordController {private final ILikedRecordService likedRecordService;PostMappingApiOperation(点赞或取消点赞)public void addLikeRecord(Valid RequestBody LikeRecordFormDTO recordDTO) {likedRecordService.addLikeRecord(recordDTO);}GetMapping(list)ApiOperation(查询指定业务id的点赞状态)public SetLong isBizLiked(RequestParam(bizIds) ListLong bizIds){return likedRecordService.isBizLiked(bizIds);}
}对应实现类
Override
public SetLong isBizLiked(ListLong bizIds) {// 1.获取登录用户idLong userId UserContext.getUser();// 2.查询点赞状态ListLikedRecord list lambdaQuery().in(LikedRecord::getBizId, bizIds).eq(LikedRecord::getUserId, userId).list();// 3.返回结果return list.stream().map(LikedRecord::getBizId).collect(Collectors.toSet());
}3.3.3.暴露Feign接口
由于该接口是给其它微服务调用的所以必须暴露出Feign客户端并且定义好fallback降级处理
我们在api模块中定义一个客户端 其中RemarkClient如下
import com.tianji.api.client.remark.fallback.RemarkClientFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;import java.util.Set;FeignClient(value remark-service, fallbackFactory RemarkClientFallback.class)
public interface RemarkClient {GetMapping(/likes/list)SetLong isBizLiked(RequestParam(bizIds) IterableLong bizIds);
}对应的fallback逻辑
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;import java.util.Set;Slf4j
public class RemarkClientFallback implements FallbackFactoryRemarkClient {Overridepublic RemarkClient create(Throwable cause) {log.error(查询remark-service服务异常, cause);return new RemarkClient() {Overridepublic SetLong isBizLiked(IterableLong bizIds) {return CollUtils.emptySet();}};}
}如果Feign定义在api包下由于每个微服务扫描包不一致。因此其它引用api的微服务是无法通过扫描包加载到这个类的。
我们需要通过SpringBoot的自动加载机制来加载这些fallback类
由于SpringBoot会在启动时读取/META-INF/spring.factories文件我们只需要在该文件中指定了要加载
FallbackConig类
Configuration
public class FallbackConfig {Beanpublic LearningClientFallback learningClientFallback(){return new LearningClientFallback();}Beanpublic TradeClientFallback tradeClientFallback(){return new TradeClientFallback();}Beanpublic RemarkClientFallback remarkClientFallback(){return new RemarkClientFallback();}
}这样所有在其中定义的fallback类都会被加载了。
3.3.3.改造查询回复接口
开发查询点赞状态接口的目的是为了在查询用户回答和评论时能看到当前用户是否点赞了。所以我们需要改造之前实现的分页查询回答或评论的接口。
注入评价服务的Feign客户端
3.4.监听点赞变更的消息
既然点赞后会发送MQ消息通知业务服务那么每一个有关的业务服务都应该监听点赞数变更的消息更新本地的点赞数量。
例如题目讨论区我们需要再题目服务中定义MQ监视器
比如本地回答区服务执行更新自己的点赞数对应字段。
Slf4j
Component
RequiredArgsConstructor
public class LikeTimesChangeListener {private final IInteractionReplyService replyService;RabbitListener(bindings QueueBinding(value Queue(name qa.liked.times.queue, durable true),exchange Exchange(name LIKE_RECORD_EXCHANGE, type ExchangeTypes.TOPIC),key QA_LIKED_TIMES_KEY))public void listenReplyLikedTimesChange(LikedTimesDTO dto){log.debug(监听到回答或评论{}的点赞数变更:{}, dto.getBizId(), dto.getLikedTimes());InteractionReply r new InteractionReply();r.setId(dto.getBizId());r.setLikedTimes(dto.getLikedTimes());replyService.updateById(r);}
}4.点赞功能改进
虽然我们初步实现了点赞功能不过有一个非常严重的问题点赞业务包含多次数据库读写操作 更重要的是点赞操作波动较大有可能会在短时间内访问量激增。例如有人非常频繁的点赞、取消点赞。这样就会给数据库带来非常大的压力。
怎么办呢
4.1.改进思路分析
高并发写操作常见的优化手段有
优化SQL和代码变同步写为异步写合并写请求
有人可能会说我们更新业务方点赞数量的时候不就是利用MQ异步写来实现的吗
没错确实如此虽然异步写减少了业务执行时间降低了数据库写频率。不过此处更重要的是利用MQ来解耦。而且数据库的写次数没有减少压力依然很大。总的读写并没有改变
这里我们采用合并写的优化方式
需要注意的是合并写是有使用场景的必须是对中间的N次写操作不敏感的情况下。点赞业务是否符合这一需求呢
无论用户中间执行点赞、取消、再点赞、再取消多少次点赞次数发生了多少次变化业务方只关注最终的点赞结果即可点赞这个东西不要求实时性很强
用户是否点赞了业务的总点赞次数
因此点赞功能可以使用合并写方案。最终我们的点赞业务流程变成这样 合并写请求有两个关键点要考虑
数据如何缓存缓存何时写入数据库
4.1.1.点赞数据缓存
点赞记录中最两个关键信息
用户是否点赞某业务的点赞总次数
这两个信息需要分别记录也就是说我们需要在Redis中设计两种数据结构分别存储。
4.1.1.1.用户是否点赞
要知道某个用户是否点赞某个业务就必须记录业务id以及给业务点赞的所有用户id . 由于一个业务可以被很多用户点赞显然是需要一个集合来记录。而Redis中的集合类型包含四种
ListSetSortedSetHash
而要**判断用户是否点赞就是判断存在且唯一。显然Set集合是最合适的。**我们可以用业务id为Key创建Set集合将点赞的所有用户保存其中格式如下 可以使用Set集合的下列命令完成点赞功能
# 判断用户是否点赞 Redis Sismember 命令判断成员元素是否是集合的成员。
SISMEMBER bizId userId
# 点赞如果返回1则代表点赞成功返回0则代表点赞失败 SADD key member1 [member2]向集合添加一个或多个成员
SADD bizId userId
# 取消点赞就是删除一个元素
SREM bizId userId
# 统计点赞总数 获取集合的成员数
SCARD bizId由于本身具备持久化机制AOF提供的数据可靠性已经能够满足点赞业务的安全需求因此我们完全可以用Redis存储来代替数据库的点赞记录。
也就是说用户的一切点赞行为以及将来查询点赞状态我们可以都走Redis不再使用数据库查询。
如果点赞数据非常庞大达到数百亿那么该怎办呢
大多数企业根本达不到这样的规模如果真的达到也没有关系。这个时候我们可以将Redis与数据库结合。
先利用Redis来记录点赞状态并且定期的将Redis中的点赞状态持久化到数据库对于历史点赞记录比如删除的题目、或者超过2年以上的访问量较低的数据都可以从redis移除只保留在数据库中当某个记录点赞时优先去Redis查询并判断如果Redis中不存在再去查询数据库数据并缓存到Redis
4.1.1.2.点赞次数
由于点赞次数需要在业务方持久化存储到数据库因此Redis只起到缓存作用即可。
由于需要记录业务id、业务类型、点赞数三个信息
一个业务类型下包含多个业务id每个业务id对应一个点赞数。
因此我们可以把每一个业务类型作为一组使用Redis的一个key然后业务id作为键点赞数作为值。这样的键值对集合有两种结构都可以满足
Hash传统键值对集合无序SortedSet基于Hash结构并且增加了跳表。因此可排序但更占用内存
如果是从节省内存角度来考虑Hash结构无疑是最佳的选择**但是考虑到将来我们要从Redis读取点赞数然后移除避免重复处理。为了保证线程安全查询、移除操作必须具备原子性。而SortedSet则提供了几个移除并获取的功能天生具备原子性。**并且我们每隔一段时间就会将数据从Redis移除并不会占用太多内存。因此这里我们计划使用SortedSet结构。
格式如下 当用户对某个业务点赞时我们统计点赞总数并将其缓存在Redis中。这样一来在一段时间内不管有多少用户对该业务点赞热点业务数据比如某个微博大V都只在Redis中修改点赞总数无需修改数据库。
4.1.2.点赞数据入库
点赞数据写入缓存了但是这里有一个新的问题
何时把缓存的点赞数通过MQ通知到业务方持久化到业务方的数据库呢
用户何时点赞、点赞频率如何完全不确定。因此无法采用延迟检测这样的手段。怎么办
事实上这也是大多数合并写请求业务面临的问题而多数情况下我们只能通过定时任务定期将缓存的数据持久化到数据库中。
4.1.3.流程图
综上所述基于Redis做写缓存后点赞流程如下 由于需要访问Redis我们提前定义一个常量类把Redis相关的Key定义为常量
public interface RedisConstants {/*给业务点赞的用户集合的KEY前缀后缀是业务id*/String LIKE_BIZ_KEY_PREFIX likes:set:biz:;/*业务点赞数统计的KEY前缀后缀是业务类型*/String LIKES_TIMES_KEY_PREFIX likes:times:type:;
}4.2.1.点赞接口
接下来我们定义一个新的点赞业务实现类
/*** p* 点赞记录表 服务实现类* /p*/
Service
RequiredArgsConstructor
public class LikedRecordServiceRedisImpl extends ServiceImplLikedRecordMapper, LikedRecord implements ILikedRecordService {private final RabbitMqHelper mqHelper;private final StringRedisTemplate redisTemplate;Overridepublic void addLikeRecord(LikeRecordFormDTO recordDTO) {// 1.基于前端的参数判断是执行点赞还是取消点赞boolean success recordDTO.getLiked() ? like(recordDTO) : unlike(recordDTO);// 2.判断是否执行成功如果失败则直接结束if (!success) {return;}// 3.如果执行成功统计点赞总数Long likedTimes redisTemplate.opsForSet().size(RedisConstants.LIKES_BIZ_KEY_PREFIX recordDTO.getBizId());if (likedTimes null) {return;}// 4.缓存点总数到RedisredisTemplate.opsForZSet().add(RedisConstants.LIKES_TIMES_KEY_PREFIX recordDTO.getBizType(),recordDTO.getBizId().toString(),likedTimes);}private boolean unlike(LikeRecordFormDTO recordDTO) {// 1.获取用户idLong userId UserContext.getUser();// 2.获取KeyString key RedisConstants.LIKES_BIZ_KEY_PREFIX recordDTO.getBizId();// 3.执行SREM命令Long result redisTemplate.opsForSet().remove(key, userId.toString());return result ! null result 0;}private boolean like(LikeRecordFormDTO recordDTO) {// 1.获取用户idLong userId UserContext.getUser();// 2.获取KeyString key RedisConstants.LIKES_BIZ_KEY_PREFIX recordDTO.getBizId();// 3.执行SADD命令Long result redisTemplate.opsForSet().add(key, userId.toString());return result ! null result 0;}
}4.2.2.批量查询点赞状态统计
目前我们的Redis点赞记录数据结构如下 当我们判断某用户是否点赞时需要使用下面命令
# 判断用户是否点赞
SISMEMBER bizId userId需要注意的是这个命令只能判断一个用户对某一个业务的点赞状态。而我们的接口是要查询当前用户对多个业务的点赞状态。
因此我们就需要多次调用SISMEMBER命令也就需要向Redis多次发起网络请求给网络带宽带来非常大的压力影响业务性能。
那么有没有办法能够一个命令完成多个业务点赞状态判断呢
非常遗憾答案是没有只能多次执行SISMEMBER命令来判断。
不过Redis中提供了一个功能可以在一次请求中执行多个命令实现批处理效果。这个功能就是Pipeline
不要在一次批处理中传输太多命令否则单次命令占用带宽过多会导致网络阻塞
Spring提供的RedisTemplate也具备pipeline功能最终批量查询点赞状态功能实现如下
Override
public SetLong isBizLiked(ListLong bizIds) {// 1.获取登录用户idLong userId UserContext.getUser();// 2.查询点赞状态ListObject objects redisTemplate.executePipelined((RedisCallbackObject) connection - {StringRedisConnection src (StringRedisConnection) connection;for (Long bizId : bizIds) {String key RedisConstants.LIKES_BIZ_KEY_PREFIX bizId;src.sIsMember(key, userId.toString());}return null;});// 3.返回结果return IntStream.range(0, objects.size()) // 创建从0到集合size的流.filter(i - (boolean) objects.get(i)) // 遍历每个元素保留结果为true的角标i.mapToObj(bizIds::get)// 用角标i取bizIds中的对应数据就是点赞过的id.collect(Collectors.toSet());// 收集
}4.2.3.定时任务
点赞成功后会更新点赞总数并写入Redis中。而我们需要定时读取这些点赞总数的变更数据通过MQ发送给业务方。这就需要定时任务来实现了。
定时任务的实现方案有很多简单的例如
SpringTaskQuartz
还有一些依赖第三方服务的分布式任务框架
Elastic-JobXXL-Job
此处先使用简单的SpringTask来实现并测试效果。
首先在tj-remark模块的RemarkApplication启动类上添加注解 其作用就是启用Spring的定时任务功能。
然后定义一个定时任务处理器类 import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.util.List;Component
RequiredArgsConstructor
public class LikedTimesCheckTask {private static final ListString BIZ_TYPES List.of(QA, NOTE);private static final int MAX_BIZ_SIZE 30;private final ILikedRecordService recordService;Scheduled(fixedDelay 20000)public void checkLikedTimes(){for (String bizType : BIZ_TYPES) {recordService.readLikedTimesAndSendMessage(bizType, MAX_BIZ_SIZE);}}
}由于可能存在多个业务类型不能厚此薄彼只处理部分业务。所以我们会遍历多种业务类型分别处理。同时为了避免一次处理的业务过多这里设定了每次处理的业务数量为30当然这些都是可以调整的。
真正处理业务的逻辑封装到了ILikedRecordService中
Override
public void readLikedTimesAndSendMessage(String bizType, int maxBizSize) {// 1.读取并移除Redis中缓存的点赞总数String key RedisConstants.LIKES_TIMES_KEY_PREFIX bizType;SetZSetOperations.TypedTupleString tuples redisTemplate.opsForZSet().popMin(key, maxBizSize);if (CollUtils.isEmpty(tuples)) {return;}// 2.数据转换ListLikedTimesDTO list new ArrayList(tuples.size());for (ZSetOperations.TypedTupleString tuple : tuples) {String bizId tuple.getValue();Double likedTimes tuple.getScore();if (bizId null || likedTimes null) {continue;}list.add(LikedTimesDTO.of(Long.valueOf(bizId), likedTimes.intValue()));}// 3.发送MQ消息mqHelper.send(LIKE_RECORD_EXCHANGE,StringUtils.format(LIKED_TIMES_KEY_TEMPLATE, bizType),list);
}4.2.4.监听点赞数变更
需要注意的是由于在定时任务中一次最多处理20条数据这些数据就需要通过MQ一次发送到业务方也就是说MQ的消息体变成了一个集合 因此作为业务方在监听MQ消息的时候也必须接收集合格式。
我们修改类com.hjx.learning.mq.LikeTimesChangeListener import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.util.ArrayList;
import java.util.List;Slf4j
Component
RequiredArgsConstructor
public class LikeTimesChangeListener {private final IInteractionReplyService replyService;RabbitListener(bindings QueueBinding(value Queue(name qa.liked.times.queue, durable true),exchange Exchange(name LIKE_RECORD_EXCHANGE, type ExchangeTypes.TOPIC),key QA_LIKED_TIMES_KEY))public void listenReplyLikedTimesChange(ListLikedTimesDTO likedTimesDTOs){log.debug(监听到回答或评论的点赞数变更);ListInteractionReply list new ArrayList(likedTimesDTOs.size());for (LikedTimesDTO dto : likedTimesDTOs) {InteractionReply r new InteractionReply();r.setId(dto.getBizId());r.setLikedTimes(dto.getLikedTimes());list.add(r);}replyService.updateBatchById(list);}
}至此完成了整个流程