获取视频详细信息
controller
@RequestMapping("/getVideoByVideoId")
public ResponseVO getVideoByVideoId(@NotEmpty String videoId){
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
//获取视频信息
VideoInfoPost videoInfoPost = videoInfoPostService.getVideoInfoPostByVideoId(videoId);
if(videoInfoPost == null || !videoInfoPost.getUserId().equals(tokenUserInfoDto.getUserId())){
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
//获取视频文件信息
VideoInfoFilePostQuery videoInfoFilePostQuery = new VideoInfoFilePostQuery();
videoInfoFilePostQuery.setVideoId(videoId);
videoInfoFilePostQuery.setOrderBy("file_index asc");
List<VideoInfoFilePost> videoInfoFilePostList = videoInfoFilePostService.findListByParam(videoInfoFilePostQuery);
VideoPostEditInfoVo vo = new VideoPostEditInfoVo();
vo.setVideoInfo(videoInfoPost);
vo.setVideoInfoFileList(videoInfoFilePostList);
return getSuccessResponseVO(vo);
}
修改视频互动设置
controller
@RequestMapping("/saveVideoInteraction")
public ResponseVO saveVideoInteraction(@NotEmpty String videoId, String interaction) {
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
videoInfoService.changeInteraction(videoId, tokenUserInfoDto.getUserId(), interaction);
return getSuccessResponseVO(null);
}
videoInfoService.changeInteraction(videoId, tokenUserInfoDto.getUserId(), interaction);
@Override
@Transactional(rollbackFor = Exception.class)
public void changeInteraction(String videoId, String userId, String interaction) {
VideoInfo videoInfo = new VideoInfo();
videoInfo.setInteraction(interaction);
VideoInfoQuery videoInfoQuery = new VideoInfoQuery();
videoInfoQuery.setVideoId(videoId);
videoInfoQuery.setUserId(userId);
videoInfoMapper.updateByParam(videoInfo, videoInfoQuery);
VideoInfoPost videoInfoPost = new VideoInfoPost();
videoInfoPost.setInteraction(interaction);
VideoInfoPostQuery videoInfoPostQuery = new VideoInfoPostQuery();
videoInfoPostQuery.setVideoId(videoId);
videoInfoPostQuery.setUserId(userId);
videoInfoPostMapper.updateByParam(videoInfoPost, videoInfoPostQuery);
}
删除视频
controller
//删除视频
@RequestMapping("/deleteVideo")
public ResponseVO deleteVideo(@NotEmpty String videoId) {
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
videoInfoService.deleteVideo(videoId, tokenUserInfoDto.getUserId());
return getSuccessResponseVO(null);
}
videoInfoService.deleteVideo(videoId, tokenUserInfoDto.getUserId());
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteVideo(String videoId, String userId) {
VideoInfoPost videoInfoPost = this.videoInfoPostMapper.selectByVideoId(videoId);
if (videoInfoPost == null || userId != null && !userId.equals(videoInfoPost.getUserId())) {
throw new BusinessException(ResponseCodeEnum.CODE_404);
}
this.videoInfoMapper.deleteByVideoId(videoId);
this.videoInfoPostMapper.deleteByVideoId(videoId);
/**
* 删除es信息
*/
esSearchComponent.delDoc(videoId);
/**
* 删除用户硬币
*/
SysSettingDto sysSettingDto = redisComponent.getSysSettingDto();
userInfoService.updateCoinCountInfo(videoInfoPost.getUserId(), -sysSettingDto.getPostVideoCoinCount());
executorService.execute(() -> {
VideoInfoFileQuery videoInfoFileQuery = new VideoInfoFileQuery();
videoInfoFileQuery.setVideoId(videoId);
//查询分P
List<VideoInfoFile> videoInfoFileList = this.videoInfoFileMapper.selectList(videoInfoFileQuery);
//删除分P
videoInfoFileMapper.deleteByParam(videoInfoFileQuery);
VideoInfoFilePostQuery videoInfoFilePostQuery = new VideoInfoFilePostQuery();
videoInfoFilePostQuery.setVideoId(videoId);
videoInfoFilePostMapper.deleteByParam(videoInfoFilePostQuery);
//删除弹幕
VideoDanmuQuery videoDanmuQuery = new VideoDanmuQuery();
videoDanmuQuery.setVideoId(videoId);
videoDanmuMapper.deleteByParam(videoDanmuQuery);
//删除评论
VideoCommentQuery videoCommentQuery = new VideoCommentQuery();
videoCommentQuery.setVideoId(videoId);
videoCommentMapper.deleteByParam(videoCommentQuery);
//删除文件
for (VideoInfoFile item : videoInfoFileList) {
try {
FileUtils.deleteDirectory(new File(appConfig.getProjectFolder() + item.getFilePath()));
} catch (IOException e) {
log.error("删除文件失败,文件路径:{}", item.getFilePath());
}
}
});
}
executorService.execute
以下是对 executorService.execute(() -> {...});
部分的详细解释:
1. executorService.execute(...)
executorService
是ExecutorService
接口的一个实例,负责管理后台线程池。ExecutorService
提供了一个高级的 API 来处理线程,简化了直接使用Thread
类的复杂性。ExecutorService
通常用来处理后台任务,将耗时操作从主线程中分离出来,避免主线程被阻塞,提高应用的响应性和效率。execute()
方法用来提交一个无返回值的任务到线程池执行。它会立即在后台线程中执行传入的任务,而不会等待任务完成后再继续执行其它代码。
2. (() -> {...})
- 这是一个 Lambda 表达式,它定义了一个匿名的函数(或者说任务)。在 Java 中,Lambda 表达式提供了一种更简洁的写法,允许你以更简洁的方式定义函数。
() -> {...}
代表一个没有参数的方法,它的实现部分在大括号{}
内。Lambda 表达式使得任务的定义更加简洁。
3. Lambda 内的代码块
具体的代码块如下:
VideoInfoFileQuery videoInfoFileQuery = new VideoInfoFileQuery();
videoInfoFileQuery.setVideoId(videoId);
List<VideoInfoFile> videoInfoFileList = this.videoInfoFileMapper.selectList(videoInfoFileQuery); // 获取视频相关的文件记录
// 删除分P(分段视频)
videoInfoFileMapper.deleteByParam(videoInfoFileQuery);
// 删除视频文件相关的帖子记录
VideoInfoFilePostQuery videoInfoFilePostQuery = new VideoInfoFilePostQuery();
videoInfoFilePostQuery.setVideoId(videoId);
videoInfoFilePostMapper.deleteByParam(videoInfoFilePostQuery);
// 删除视频相关的弹幕
VideoDanmuQuery videoDanmuQuery = new VideoDanmuQuery();
videoDanmuQuery.setVideoId(videoId);
videoDanmuMapper.deleteByParam(videoDanmuQuery);
// 删除视频的评论
VideoCommentQuery videoCommentQuery = new VideoCommentQuery();
videoCommentQuery.setVideoId(videoId);
videoCommentMapper.deleteByParam(videoCommentQuery);
// 删除视频文件
for (VideoInfoFile item : videoInfoFileList) {
try {
FileUtils.deleteDirectory(new File(appConfig.getProjectFolder() + item.getFilePath())); // 删除文件夹中的视频文件
} catch (IOException e) {
log.error("删除文件失败,文件路径:{}", item.getFilePath()); // 如果删除失败,记录错误日志
}
}
-
查询和删除数据库记录:
- 通过
videoInfoFileMapper.selectList(videoInfoFileQuery)
获取与视频相关的文件记录。 - 然后删除与视频相关的所有数据库记录,包括分P视频、视频文件相关的帖子、弹幕、评论等。
- 通过
-
删除文件:
- 该部分代码通过
FileUtils.deleteDirectory()
删除与视频相关的实际文件。对于每个文件,都会尝试删除,如果失败,则记录错误日志。
- 该部分代码通过
4. 为什么使用 executorService.execute()
?
使用 executorService.execute()
来异步执行后台任务的主要原因是:
-
避免阻塞主线程:数据库操作、文件删除等操作可能会非常耗时,尤其是视频文件和相关资源较多时。如果这些操作在主线程中执行,会导致用户请求被阻塞,影响系统的响应速度和用户体验。通过将这些操作放到后台线程中执行,主线程可以继续处理其他请求或响应,保持系统流畅。
-
提高性能:
ExecutorService
使用线程池管理多个线程,可以避免频繁地创建和销毁线程,提升任务执行效率。线程池中的线程在任务完成后会被复用,从而减少了线程创建的开销。 -
非阻塞 I/O 操作:删除视频文件等操作是 I/O 密集型任务,可能会导致性能瓶颈。将这些任务放到后台线程中执行,能够避免主线程的阻塞,使得其他请求可以继续处理。
5. 后台任务失败的处理
-
在代码中,删除文件的部分可能会发生
IOException
错误。为了确保即使删除文件失败也不会影响整体流程,代码中使用了try-catch
来捕获异常并记录日志:try { FileUtils.deleteDirectory(new File(appConfig.getProjectFolder() + item.getFilePath())); // 删除文件夹中的视频文件 } catch (IOException e) { log.error("删除文件失败,文件路径:{}", item.getFilePath()); // 如果删除失败,记录错误日志 }
-
这样,即使某些文件删除失败,系统会继续执行其他任务,而不会中断整个删除过程。
6. 总结
- 异步执行:
executorService.execute()
将后台任务放入线程池中异步执行,避免阻塞主线程,提升应用性能和响应速度。 - 任务内容:后台任务处理视频删除相关的所有内容,包括数据库记录和文件删除。
- 线程池管理:通过
ExecutorService
管理线程池,确保线程的高效利用。 - 错误处理:即使文件删除操作失败,程序也能正常继续执行,确保整个任务流程的健壮性。
总的来说,这段代码使用了异步处理的方式来进行视频删除的后续清理工作,从而提高了系统的响应能力和稳定性。
避免主线程阻塞
是的,在调用 executorService.execute(() -> {...})
方法之前,主线程已经返回了响应。
具体来说,这段代码的执行流程如下:
1. 接收到删除视频请求:
当用户请求删除视频时,控制器方法 deleteVideo
被触发。
@RequestMapping("/deleteVideo")
public ResponseVO deleteVideo(@NotEmpty String videoId) {
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
videoInfoService.deleteVideo(videoId, tokenUserInfoDto.getUserId());
return getSuccessResponseVO(null);
}
- 首先,
getTokenUserInfoDto()
方法获取当前用户的信息。 - 然后,调用
videoInfoService.deleteVideo(videoId, tokenUserInfoDto.getUserId())
来删除视频。
2. 删除视频的主要操作(包括数据库操作):
在 deleteVideo
方法中,删除视频的逻辑被分成了两个部分:
- 删除数据库中的记录:删除视频相关的记录、帖子、评论、弹幕等。
- 调用
executorService.execute(...)
:在执行完这些操作后,使用executorService
提交一个后台任务去异步执行其他耗时操作,比如删除视频文件、分P视频、文件夹等。
但是,重要的一点是,调用 executorService.execute(...)
的方法不会阻塞当前线程。也就是说,主线程并没有等待后台任务的完成,它只是在提交后台任务后继续执行。
3. 主线程返回响应:
主线程继续执行,调用 getSuccessResponseVO(null)
来返回一个成功的响应给前端:
return getSuccessResponseVO(null);
这个响应会迅速返回给客户端,告诉客户端视频删除操作已经被接受,并且响应是成功的。
4. 后台任务执行:
由于 executorService.execute()
是异步执行的,后台任务会在不同的线程中执行,这部分任务(删除视频文件、删除分P视频、清理相关数据等)不会影响主线程的执行。主线程完成视频删除请求的响应后,不会等待后台任务的完成。
这意味着,主线程已经完成了它的工作并返回了响应给前端,而后台的清理任务依然在后台继续进行。
关键点总结:
- 主线程返回响应:主线程在调用
executorService.execute()
后,会继续执行并尽快返回响应给客户端。这是因为executorService.execute()
不会阻塞当前线程,它将任务交给后台线程池管理。 - 后台任务:后台任务由线程池中的一个线程处理,它们执行删除视频文件、数据库记录等清理工作,但这些操作在主线程返回响应后才开始执行,并且不会影响主线程的响应速度。
因此,调用 executorService.execute(() -> {...})
后,主线程不再等待后台任务完成,而是立即返回响应给客户端。后台任务在后台独立执行。
后台线程与消息队列
executorService.execute()
和 消息队列(如 RabbitMQ、Kafka 等)在处理异步任务时有一些相似之处,但它们的设计目标和使用场景存在明显差异。下面我们来对比这两者的优缺点。
1. executorService.execute()
ExecutorService
是一个线程池,它提供了一种简单的方法来异步执行任务。它将任务提交给后台线程池,由线程池的线程来执行这些任务。
优点:
- 简单易用:
ExecutorService
提供了简单的 API,任务的提交、线程管理等都由ExecutorService
自动处理。开发者只需要关注业务逻辑的编写,不需要管理线程。 - 高效:任务提交后,线程池中的线程会尽快处理这些任务。线程池通过复用线程来避免频繁创建和销毁线程的开销。
- 适用于短期、轻量级任务:对于需要迅速处理的任务,如简单的文件删除、数据库更新等,
ExecutorService
能够高效处理。 - 无需额外组件:不需要额外的外部组件或服务,直接通过 Java 的标准库来实现,适合单机环境。
缺点:
- 线程池大小有限制:线程池的大小是有限的,可能导致当任务量过多时,新的任务无法及时执行或者线程池资源被耗尽。尽管可以调整线程池大小,但这需要提前考虑系统的负载和任务的复杂性。
- 不适合高负载、长时间运行的任务:对于需要长时间运行或高频率的任务,
ExecutorService
可能无法提供很好的可扩展性,或者线程池的管理会变得复杂。 - 缺乏持久化:如果应用崩溃或线程池中的任务执行失败,任务可能会丢失或没有被执行。任务没有持久化机制,重启后可能会丢失数据。
2. 消息队列
消息队列(如 RabbitMQ、Kafka、ActiveMQ 等)是一种异步通信机制,允许应用将任务以消息的形式放入队列中,由消费者(消费者程序或系统)异步地处理这些任务。
优点:
- 解耦与异步处理:消息队列的核心优势是它将任务生产者(发送任务的系统)和消费者(处理任务的系统)解耦。生产者发送消息,消费者可以在不同的时间、不同的机器上异步处理这些消息。这种解耦使得系统更加灵活且易于扩展。
- 高可扩展性:消息队列可以根据需要横向扩展消费者的数量,增加处理能力。比如,通过增加消费者实例来处理更高频率的消息。对于高并发和高负载的场景,消息队列表现得更为高效和稳定。
- 任务持久化与可靠性:消息队列通常提供持久化机制,消息可以存储在队列中,直到被消费。这确保了即使系统崩溃或消费方暂时不可用,消息不会丢失,可以在恢复后继续消费。
- 异步与分布式:消息队列支持跨系统、跨机器的异步消息传递,适用于分布式系统。它允许多个独立的系统或服务彼此通信,执行异步任务。
- 任务重试:消息队列通常支持消息的重试机制,即如果某个任务处理失败,消费者可以重新处理该任务。
缺点:
- 复杂度高:使用消息队列需要额外的组件和配置,增加了系统的复杂度。需要部署和管理消息队列服务,处理消息队列的监控、故障恢复等工作。
- 延迟:由于消息队列通常是基于网络通信的,任务的处理可能会有一定的延迟。相比之下,
ExecutorService
是本地线程池,任务提交后可以立即处理,延迟较低。 - 性能开销:消息队列的引入会增加系统的性能开销,特别是在低延迟和高吞吐的场景下,消息队列的通信开销可能会对系统性能产生影响。
- 任务消费不保证顺序:某些消息队列在高并发的情况下,任务的消费顺序可能不被严格保证,这对于一些需要顺序执行的任务可能是一个问题。
对比与优缺点总结
特性 | ExecutorService.execute() | 消息队列 (例如 RabbitMQ, Kafka) |
---|---|---|
任务管理 | 通过线程池管理任务。 | 通过队列管理消息,生产者和消费者解耦。 |
适用场景 | 简单的任务调度、线程池管理。适合轻量级、短期的异步任务。 | 高并发、高负载的任务调度,分布式系统中任务的解耦与异步处理。 |
任务持久化 | 不支持持久化,任务丢失可能性较大。 | 支持持久化,确保任务不丢失,适合可靠性要求高的场景。 |
扩展性 | 线程池的大小有限,处理能力固定。 | 可以横向扩展消费者,适合大规模分布式场景。 |
任务顺序 | 线程池内部顺序处理任务,但无法跨系统保证顺序。 | 消费者可能并行处理消息,可能存在任务顺序不保证的情况。 |
可靠性 | 一旦线程池中任务丢失或失败,无法恢复。 | 支持消息重试机制,任务失败时可以重试。 |
复杂性 | 配置简单,依赖少。 | 配置复杂,部署和运维工作多。 |
延迟 | 本地执行,延迟较低。 | 可能有网络延迟,处理有一定的排队时间。 |
选择时的考虑因素
-
任务的复杂性和处理量:
- 如果你的应用只需要执行简单的、局部的异步任务(例如删除视频文件、更新数据库记录等),
ExecutorService
是一个简单高效的选择。 - 如果你的系统涉及到大量的任务处理,且需要在不同系统之间进行异步、可靠的通信,或者你需要扩展系统处理能力,消息队列会更加适合。
- 如果你的应用只需要执行简单的、局部的异步任务(例如删除视频文件、更新数据库记录等),
-
系统的可靠性要求:
- 如果你需要确保任务不丢失,并且可以在系统出现故障时恢复任务,消息队列提供的持久化机制和重试机制会是一个更可靠的选择。
- 如果任务丢失不影响业务逻辑(例如后台任务可以失败重试),则使用
ExecutorService
就足够。
-
分布式系统:
- 如果你的应用是分布式的,需要跨多个服务处理任务,消息队列是更加合适的方案。它提供了一个灵活的消息传递机制,能支持跨机器、跨服务的任务处理。
- 对于单机应用或者小型系统,
ExecutorService
的简单性和高效性可能是一个更好的选择。
总结
ExecutorService.execute()
适用于较为简单、短期、轻量级的任务,尤其是在单机环境中,简单易用且高效。- 消息队列适用于大规模分布式系统、高并发任务的处理,需要持久化和高可靠性的场景。它能够提供更好的扩展性、可靠性和任务管理能力,但会增加系统的复杂性和延迟。
因此,选择使用哪种方法应根据系统的复杂性、任务量、可靠性要求和性能需求来决定。
使用消息队列来优化 executorService.execute
中的异步方法,并保证事务的一致性,是一种常见的分布式系统设计模式。消息队列能够解耦业务逻辑与异步任务的执行,使得系统更加灵活、可扩展,并且能够更好地保证事务的一致性。
优化方案概述:
-
将异步任务发送到消息队列:
- 使用消息队列将
executorService.execute
中的异步任务转换为消息队列中的消息。 - 消息队列(如 RabbitMQ、Kafka 等)可以可靠地持久化消息,保证消息不会丢失。
- 使用消息队列将
-
保证事务一致性:
- 将操作数据库的逻辑和异步任务的处理通过消息队列结合,确保业务操作的一致性。
- 使用事务性消息的模式(如消息队列的事务、消息发送确认机制等)来确保消息发送和数据库操作的原子性。
-
确保消息的处理幂等性:
- 由于消息队列的消息可能会重复消费,需要保证消息处理的幂等性。
-
实现异步事务处理的可靠性:
- 消费者在处理消息时可以通过事务管理,确保操作数据库时的数据一致性。
具体实施步骤:
1. 将异步任务转换为消息
首先,将原本通过 executorService.execute
进行的异步任务,改为发送消息到消息队列中。比如,您可以将 VideoInfoFilePost
发送到消息队列:
@Autowired
private AmqpTemplate amqpTemplate;
public void deleteVideo(String videoId, String userId) {
// 处理同步部分
VideoInfoPost videoInfoPost = this.videoInfoPostMapper.selectByVideoId(videoId);
if (videoInfoPost == null || userId != null && !userId.equals(videoInfoPost.getUserId())) {
throw new BusinessException(ResponseCodeEnum.CODE_404);
}
this.videoInfoMapper.deleteByVideoId(videoId);
this.videoInfoPostMapper.deleteByVideoId(videoId);
// 删除es信息
esSearchComponent.delDoc(videoId);
// 删除用户硬币
SysSettingDto sysSettingDto = redisComponent.getSysSettingDto();
userInfoService.updateCoinCountInfo(videoInfoPost.getUserId(), -sysSettingDto.getPostVideoCoinCount());
// 将异步操作封装成消息发送到消息队列
VideoInfoFilePost filePost = new VideoInfoFilePost();
filePost.setVideoId(videoId);
amqpTemplate.convertAndSend("directExchange", "transferFileRoutingKey", filePost);
}
2. 创建消息队列消费者
在消费者端,您可以监听这个队列,并且在消费消息时处理数据库事务:
@Component
@Slf4j
public class TransferFileQueueConsumer {
@Resource
private VideoInfoPostService videoInfoPostService;
@RabbitListener(queues = "transferFileRouting")
@Transactional(rollbackFor = Exception.class) // 确保事务的一致性
public void consumeTransferFileQueue(VideoInfoFilePost videoInfoFile) {
try {
// 处理业务逻辑,保证事务的一致性
videoInfoPostService.transferVideoFile(videoInfoFile);
} catch (Exception e) {
log.error("处理转码文件队列消息失败", e);
throw new RuntimeException("处理消息失败,事务回滚", e);
}
}
}
- 这里使用了
@Transactional
确保消息处理时的事务一致性。 - 消费者方法会在接收到消息后执行相应的操作,如果发生异常,则会触发事务回滚。
3. 事务性消息和可靠消息机制
为了确保消息队列的可靠性和事务一致性,你需要实现以下几个方面:
-
事务性消息:确保消息发送和数据库操作是原子性的。你可以使用消息队列的事务机制来确保消息发送成功和数据库操作一致。如果发送消息失败,事务会回滚。
-
消息确认机制:确保消费者确认了消息的处理才能从队列中移除该消息。如果消费者处理失败,消息可以重新消费,保证系统的可靠性。
-
消息幂等性:由于消息可能会重复发送或重复消费,你需要在消费者端确保操作的幂等性。例如,可以通过使用
UUID
等唯一标识符来确保每个任务只执行一次。
4. 配置消息队列的事务性
以 RabbitMQ 为例,你可以在发送消息时使用事务来确保消息的可靠发送:
@Autowired
private AmqpTemplate amqpTemplate;
public void sendMessageWithTransaction(VideoInfoFilePost filePost) {
// 开始事务
Channel channel = ((RabbitTemplate) amqpTemplate).getConnectionFactory().createConnection().createChannel(false);
try {
channel.txSelect(); // 开启事务
amqpTemplate.convertAndSend("directExchange", "transferFileRoutingKey", filePost);
channel.txCommit(); // 提交事务
} catch (Exception e) {
channel.txRollback(); // 回滚事务
log.error("消息发送失败,事务回滚", e);
throw new RuntimeException("消息发送失败", e);
} finally {
try {
channel.close();
} catch (IOException e) {
log.error("关闭通道失败", e);
}
}
}
这样,当发生异常时,消息发送事务会回滚。
5. 事务回调机制
你还可以使用消息队列的事务回调机制,以便在消息发送时能对回调进行处理,确保事务的执行和回滚。
6. 异常和重试机制
为确保消息处理失败时能够重新尝试,可以配置重试策略。在 RabbitMQ 中,你可以使用死信队列(DLX)来实现消息的重试,或者通过 Spring AMQP 的 SimpleMessageListenerContainer
来配置消息的重试策略。
7. 总结
通过将异步操作转换为消息队列中的消息,您可以实现以下目标:
- 通过消息队列解耦业务逻辑和异步操作,提高系统的可扩展性。
- 使用事务保证数据库操作和消息发送的原子性,确保数据一致性。
- 通过幂等性保障,确保消息消费的可靠性和系统的高可用性。
- 使用消息确认机制确保消息处理的可靠性,避免消息丢失。
这样,不仅能够优化 executorService.execute
中的异步操作,还能够通过事务保证业务操作的可靠性和一致性。
使用消息队列来实现 executorService.execute
中的异步操作是一个合理且常见的做法,尤其是在需要解耦业务逻辑和异步任务执行、保证任务的可靠性和可扩展性时。消息队列具有以下优点:
注意事项
1. 解耦业务逻辑和异步操作
- 通过将异步任务提交到消息队列,生产者和消费者之间的依赖关系可以解耦,业务逻辑(如数据库操作、文件删除等)与异步执行的任务(如文件转码、批量处理等)不再直接耦合。消费者可以独立地处理队列中的消息,不会直接影响生产者的执行流程。
2. 可靠性
- 消息队列(如 RabbitMQ)提供了可靠的消息传递机制。即使消费者处理失败,消息可以重新入队,确保任务不会丢失。结合消息队列的事务性消息机制,可以确保消息发送成功和数据库操作的一致性。
- 此外,RabbitMQ 还提供了死信队列(DLX)和消息重试等机制,能够进一步增强系统的可靠性,避免因为某些原因导致任务丢失或处理失败。
3. 提高系统的扩展性
- 通过消息队列,您可以轻松扩展消费者端的能力。例如,如果需要处理大量任务,只需要增加更多的消费者(消费者端的服务实例),而无需修改生产者端的代码或增加计算资源。
- 如果任务处理速度较慢,队列可以临时缓存任务,等待消费者处理完当前任务后再进行处理,不会导致生产者端的负担过重。
4. 事务一致性和数据安全
- 在您的场景中,业务操作涉及数据库更新(如删除视频、更新用户硬币等),这些操作需要确保一致性。在使用消息队列时,您可以在消费者端通过事务确保数据库操作的原子性。例如,您可以在消费消息时开启事务,确保数据库的更新和文件操作的一致性。
- 然而,确保事务一致性时需要特别注意,在消息处理过程中可能会发生多个步骤的操作,需要保证幂等性(即同一条消息多次消费时不会导致重复操作)。
5. 异步处理提高系统性能
- 使用消息队列后,生产者的线程不需要等待异步任务完成,减少了阻塞等待的时间。这对于需要处理大量任务的系统尤为重要,能有效提高系统的吞吐量和响应速度。
6. 支持后期扩展
- 消息队列的引入不仅能解决当前的异步需求,还能为将来的功能扩展提供支持。例如,您可以在不修改现有代码的情况下增加更多的异步任务处理模块(例如新的消费者处理不同类型的任务),极大地提升了系统的灵活性和可维护性。
7. 幂等性保障
- 消息队列中的消息可能会被重复消费,因此必须确保在处理消息时,消费操作是幂等的。比如,如果文件已经被删除或者转码过,就不需要重复执行相同的操作。通过在消费者端加入幂等性检查(例如通过唯一标识符或数据库记录标志位来判断),可以避免重复操作。
需要注意的问题
尽管消息队列在许多场景中具有很大的优势,但在实现时也有一些需要考虑的问题:
1. 消息的延迟和积压
- 消息队列引入了延迟。如果队列中积压大量消息,可能会导致消费者处理速度跟不上生产者的速度,进而影响系统的响应时间。为了避免这种情况,可以通过增加消费者实例、调整消费者处理速度等方式来扩展系统。
2. 事务管理的复杂性
- 在消息队列和数据库操作之间实现事务一致性时,可能需要额外的努力。例如,RabbitMQ 的事务机制和数据库事务是独立的,在某些情况下可能会遇到"消息发送成功,数据库操作失败"或者"数据库操作成功,消息没有被正确确认"的情况。为此,您可能需要采用最终一致性或补偿机制(如 TCC 模式)来保证系统的可靠性。
3. 确保消息的幂等性
- 消息队列中的消息可能会被重复消费,因此必须设计系统保证幂等性,即相同的消息处理多次不会产生不同的结果。例如,删除文件时,需要检查文件是否已经被删除;更新数据库时,需要检查数据是否已经更新。
4. 异常处理和失败恢复
- 处理消息时可能会出现异常,您需要设计合适的异常处理机制,确保系统在发生错误时能够进行重试或者将错误信息保存到死信队列(DLX),以便后续手动干预。
5. 性能开销
- 消息队列引入了一定的性能开销,尤其是在消息生产和消费的过程中。如果队列的消息量非常大,可能会带来系统的负载和性能问题。因此,消息队列的选择和配置需要考虑到实际的负载需求。
结论
综上所述,使用消息队列优化 executorService.execute
中的异步操作是非常合理的,特别是在需要处理异步任务、提高系统可扩展性、保证事务一致性和确保系统可靠性时。消息队列能够有效解耦、增强系统的可维护性和可扩展性,同时提供可靠的消息传递和错误恢复机制。
但是,为了确保消息队列的成功实现,您需要特别注意以下几个方面:
- 保证消息的幂等性和事务的一致性。
- 处理好消息队列的延迟和积压问题。
- 配置合理的消费者数量和消费速率,以适应实际负载。
- 设计合适的异常处理和重试机制,确保系统的健壮性。
如果能够合理地设计和配置消息队列,您的系统将具备良好的扩展性、可靠性和高可用性。