feat: 集成 Spring AMQP,使用 RabbitMQ 发送小说更新消息

默认关闭,通过 spring.rabbitmq.enable 配置属性来开启
This commit is contained in:
xiongxiaoyang 2022-05-25 09:39:25 +08:00
parent a8e2e2d5c9
commit e5592b85dd
30 changed files with 259 additions and 56 deletions

View File

@ -110,6 +110,12 @@
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- MQ 相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>

View File

@ -5,7 +5,7 @@ import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.dto.UserInfoDto;
import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager;
import org.springframework.util.StringUtils;
import java.util.Objects;

View File

@ -5,8 +5,8 @@ import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.manager.AuthorInfoCacheManager;
import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.AuthorInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

View File

@ -2,7 +2,7 @@ package io.github.xxyopen.novel.core.auth;
import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.util.JwtUtils;
import io.github.xxyopen.novel.manager.UserInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.UserInfoCacheManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

View File

@ -13,11 +13,14 @@ public class CommonConsts {
*
* */
public static final Integer YES = 1;
public static final String TRUE = "true";
/**
*
* */
public static final Integer NO = 0;
public static final String FALSE = "false";
/**
* 性别常量

View File

@ -0,0 +1,44 @@
package io.github.xxyopen.novel.core.config;
import io.github.xxyopen.novel.core.constant.AmqpConsts;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* AMQP 配置类
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
@Configuration
public class AmqpConfig {
/**
* 小说信息改变交换机
*/
@Bean
public FanoutExchange bookChangeExchange() {
return new FanoutExchange(AmqpConsts.BookChangeMq.EXCHANGE_NAME);
}
/**
* Elasticsearch book 索引更新队列
*/
@Bean
public Queue esBookUpdateQueue() {
return new Queue(AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE);
}
/**
* Elasticsearch book 索引更新队列绑定到小说信息改变交换机
*/
@Bean
public Binding esBookUpdateQueueBinding() {
return BindingBuilder.bind(esBookUpdateQueue()).to(bookChangeExchange());
}
}

View File

@ -0,0 +1,33 @@
package io.github.xxyopen.novel.core.constant;
/**
* AMQP 相关常量
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
public class AmqpConsts {
/**
* 小说信息改变 MQ
* */
public static class BookChangeMq{
/**
* 小说信息改变交换机
* */
public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE";
/**
* Elasticsearch book 索引更新的队列
* */
public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE";
/**
* Redis book 缓存更新的队列
* */
public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE";
}
}

View File

@ -0,0 +1,48 @@
package io.github.xxyopen.novel.core.listener;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.IndexResponse;
import io.github.xxyopen.novel.core.constant.AmqpConsts;
import io.github.xxyopen.novel.core.constant.EsConsts;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import io.github.xxyopen.novel.dao.mapper.BookInfoMapper;
import io.github.xxyopen.novel.dto.es.EsBookDto;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* Rabbit 队列监听器
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
@Component
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true")
@RequiredArgsConstructor
@Slf4j
public class RabbitQueueListener {
private final BookInfoMapper bookInfoMapper;
private final ElasticsearchClient esClient;
/**
* 监听小说信息改变的 ES 更新队列更新最新小说信息到 ES
* */
@RabbitListener(queues = AmqpConsts.BookChangeMq.QUEUE_ES_UPDATE)
@SneakyThrows
public void updateEsBook(Long bookId) {
BookInfo bookInfo = bookInfoMapper.selectById(bookId);
IndexResponse response = esClient.index(i -> i
.index(EsConsts.BookIndex.INDEX_NAME)
.id(bookInfo.getId().toString())
.document(EsBookDto.build(bookInfo))
);
log.info("Indexed with version " + response.version());
}
}

View File

@ -18,7 +18,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.ZoneOffset;
import java.util.List;
/**
@ -60,12 +59,11 @@ public class BookToEsTask {
BulkRequest.Builder br = new BulkRequest.Builder();
for (BookInfo book : bookInfos) {
EsBookDto esBook = buildEsBook(book);
br.operations(op -> op
.index(idx -> idx
.index(EsConsts.BookIndex.INDEX_NAME)
.id(book.getId().toString())
.document(esBook)
.document(EsBookDto.build(book))
)
).timeout(Time.of(t -> t.time("10s")));
maxId = book.getId();
@ -87,26 +85,4 @@ public class BookToEsTask {
}
private EsBookDto buildEsBook(BookInfo book) {
return EsBookDto.builder()
.id(book.getId())
.categoryId(book.getCategoryId())
.categoryName(book.getCategoryName())
.bookDesc(book.getBookDesc())
.bookName(book.getBookName())
.authorId(book.getAuthorId())
.authorName(book.getAuthorName())
.bookStatus(book.getBookStatus())
.commentCount(book.getCommentCount())
.isVip(book.getIsVip())
.score(book.getScore())
.visitCount(book.getVisitCount())
.wordCount(book.getWordCount())
.workDirection(book.getWorkDirection())
.lastChapterId(book.getLastChapterId())
.lastChapterName(book.getLastChapterName())
.lastChapterUpdateTime(book.getLastChapterUpdateTime()
.toInstant(ZoneOffset.ofHours(8)).toEpochMilli())
.build();
}
}

View File

@ -1,10 +1,13 @@
package io.github.xxyopen.novel.dto.es;
import io.github.xxyopen.novel.dao.entity.BookInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.ZoneOffset;
/**
* Elasticsearch 存储小说 DTO
* @author xiongxiaoyang
@ -101,4 +104,27 @@ public class EsBookDto {
*/
private Integer isVip;
public static EsBookDto build(BookInfo bookInfo){
return EsBookDto.builder()
.id(bookInfo.getId())
.categoryId(bookInfo.getCategoryId())
.categoryName(bookInfo.getCategoryName())
.bookDesc(bookInfo.getBookDesc())
.bookName(bookInfo.getBookName())
.authorId(bookInfo.getAuthorId())
.authorName(bookInfo.getAuthorName())
.bookStatus(bookInfo.getBookStatus())
.commentCount(bookInfo.getCommentCount())
.isVip(bookInfo.getIsVip())
.score(bookInfo.getScore())
.visitCount(bookInfo.getVisitCount())
.wordCount(bookInfo.getWordCount())
.workDirection(bookInfo.getWorkDirection())
.lastChapterId(bookInfo.getLastChapterId())
.lastChapterName(bookInfo.getLastChapterName())
.lastChapterUpdateTime(bookInfo.getLastChapterUpdateTime()
.toInstant(ZoneOffset.ofHours(8)).toEpochMilli())
.build();
}
}

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import io.github.xxyopen.novel.core.constant.CacheConsts;
import io.github.xxyopen.novel.dao.entity.BookChapter;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.cache;
import io.github.xxyopen.novel.core.constant.CacheConsts;
import io.github.xxyopen.novel.dao.entity.UserInfo;

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.dao;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.github.xxyopen.novel.core.constant.DatabaseConsts;

View File

@ -0,0 +1,52 @@
package io.github.xxyopen.novel.manager.mq;
import io.github.xxyopen.novel.core.common.constant.CommonConsts;
import io.github.xxyopen.novel.core.constant.AmqpConsts;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Objects;
/**
* AMQP 消息管理类
*
* @author xiongxiaoyang
* @date 2022/5/25
*/
@Component
@RequiredArgsConstructor
public class AmqpMsgManager {
private final AmqpTemplate amqpTemplate;
@Value("${spring.amqp.enable}")
private String enableAmqp;
/**
* 发送小说信息改变消息
*/
public void sendBookChangeMsg(Long bookId) {
if (Objects.equals(enableAmqp, CommonConsts.TRUE)) {
sendAmqpMessage(amqpTemplate, AmqpConsts.BookChangeMq.EXCHANGE_NAME, null, bookId);
}
}
private void sendAmqpMessage(AmqpTemplate amqpTemplate, String exchange, String routingKey, Object message) {
// 如果在事务中则在事务执行完成后再发送否则可以直接发送
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
amqpTemplate.convertAndSend(exchange, routingKey, message);
}
});
return;
}
amqpTemplate.convertAndSend(exchange, routingKey, message);
}
}

View File

@ -1,4 +1,4 @@
package io.github.xxyopen.novel.manager;
package io.github.xxyopen.novel.manager.redis;
import io.github.xxyopen.novel.core.common.util.ImgVerifyCodeUtils;
import io.github.xxyopen.novel.core.constant.CacheConsts;

View File

@ -5,7 +5,7 @@ import io.github.xxyopen.novel.dao.entity.AuthorInfo;
import io.github.xxyopen.novel.dao.mapper.AuthorInfoMapper;
import io.github.xxyopen.novel.dto.AuthorInfoDto;
import io.github.xxyopen.novel.dto.req.AuthorRegisterReqDto;
import io.github.xxyopen.novel.manager.AuthorInfoCacheManager;
import io.github.xxyopen.novel.manager.cache.AuthorInfoCacheManager;
import io.github.xxyopen.novel.service.AuthorService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@ -15,7 +15,9 @@ import io.github.xxyopen.novel.dto.req.BookAddReqDto;
import io.github.xxyopen.novel.dto.req.ChapterAddReqDto;
import io.github.xxyopen.novel.dto.req.UserCommentReqDto;
import io.github.xxyopen.novel.dto.resp.*;
import io.github.xxyopen.novel.manager.*;
import io.github.xxyopen.novel.manager.cache.*;
import io.github.xxyopen.novel.manager.dao.UserDaoManager;
import io.github.xxyopen.novel.manager.mq.AmqpMsgManager;
import io.github.xxyopen.novel.service.BookService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -62,6 +64,8 @@ public class BookServiceImpl implements BookService {
private final UserDaoManager userDaoManager;
private final AmqpMsgManager amqpMsgManager;
private static final Integer REC_BOOK_COUNT = 4;
@Override
@ -335,6 +339,8 @@ public class BookServiceImpl implements BookService {
bookInfoMapper.updateById(newBookInfo);
// b) 刷新小说信息缓存
bookInfoCacheManager.cachePutBookInfo(dto.getBookId());
// c) 发送小说信息更新的 MQ 消息
amqpMsgManager.sendBookChangeMsg(dto.getBookId());
return RestResp.ok();
}

View File

@ -3,8 +3,8 @@ package io.github.xxyopen.novel.service.impl;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.dto.resp.HomeBookRespDto;
import io.github.xxyopen.novel.dto.resp.HomeFriendLinkRespDto;
import io.github.xxyopen.novel.manager.FriendLinkCacheManager;
import io.github.xxyopen.novel.manager.HomeBookCacheManager;
import io.github.xxyopen.novel.manager.cache.FriendLinkCacheManager;
import io.github.xxyopen.novel.manager.cache.HomeBookCacheManager;
import io.github.xxyopen.novel.service.HomeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

View File

@ -8,7 +8,7 @@ import io.github.xxyopen.novel.dao.entity.NewsInfo;
import io.github.xxyopen.novel.dao.mapper.NewsContentMapper;
import io.github.xxyopen.novel.dao.mapper.NewsInfoMapper;
import io.github.xxyopen.novel.dto.resp.NewsInfoRespDto;
import io.github.xxyopen.novel.manager.NewsCacheManager;
import io.github.xxyopen.novel.manager.cache.NewsCacheManager;
import io.github.xxyopen.novel.service.NewsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

View File

@ -6,7 +6,7 @@ import io.github.xxyopen.novel.core.common.exception.BusinessException;
import io.github.xxyopen.novel.core.common.resp.RestResp;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.dto.resp.ImgVerifyCodeRespDto;
import io.github.xxyopen.novel.manager.VerifyCodeManager;
import io.github.xxyopen.novel.manager.redis.VerifyCodeManager;
import io.github.xxyopen.novel.service.ResourceService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;

View File

@ -20,7 +20,7 @@ import io.github.xxyopen.novel.dto.req.UserRegisterReqDto;
import io.github.xxyopen.novel.dto.resp.UserInfoRespDto;
import io.github.xxyopen.novel.dto.resp.UserLoginRespDto;
import io.github.xxyopen.novel.dto.resp.UserRegisterRespDto;
import io.github.xxyopen.novel.manager.VerifyCodeManager;
import io.github.xxyopen.novel.manager.redis.VerifyCodeManager;
import io.github.xxyopen.novel.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

View File

@ -11,15 +11,9 @@ spring:
multipart:
max-file-size: 5MB
server:
port: 8888
---
spring:
datasource:
@ -30,10 +24,9 @@ spring:
activate:
on-profile: dev
---
spring:
# Redis 配置
redis:
host: 127.0.0.1
port: 6379
@ -41,6 +34,7 @@ spring:
config:
activate:
on-profile: dev
# Elasticsearch 配置
elasticsearch:
# 是否开启 elasticsearch 搜索引擎功能true-开启 false-不开启
enable: false
@ -48,6 +42,21 @@ spring:
- https://my-deployment-ce7ca3.es.us-central1.gcp.cloud.es.io:9243
username: elastic
password: qTjgYVKSuExX6tWAsDuvuvwl
amqp:
# 是否开启 Spring AMQPtrue-开启 false-不开启
enable: false
# RabbitMQ 配置
rabbitmq:
addresses: "amqp://guest:guest@47.106.243.172"
virtual-host: novel
template:
retry:
# 开启重试
enabled: true
# 最大重试次数
max-attempts: 3
# 第一次和第二次重试之间的持续时间
initial-interval: "3s"
---
spring: