1、项目集成RabbitMQ背景说明
当前的大盘、股票等相关数据都是通过定时任务不断采集落库的,而对于大屏终端也需要高频且实时获取股票最新数据,这会导致数据库过高的负载。
思路:
-
我们可在定时任务拉取股票数据时,将最新的数据信息通过mq同步到主业务工程进行缓存处理,这样就避免了多用户从数据库反复加载股票数据导致数据库负载过高的问题,同样也提高了大屏终端服务的吞吐量;
-
使用CaffineCache本地缓存而非redis远程缓存,能提供更高效的响应速度,同时避免了与redis之间交换带来的网络I/O成本开销;
同时我们可基于主题交换机向不同的队列发送不同的股票类型数据,比如:通过stockExchange主题交换机向innerMarketQueue发送大盘数据,routingKey为inner.market;
2、采集工程向MQ同步大盘数据
stock_job工程引入amqp依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
stock_job工程定义application-mq.yml配置rabbitmq:
spring:
rabbitmq:
host: localhost # rabbitMQ的ip地址
port: 5672 # 端口
username: itcast
password: 123321
virtual-host: /
在stock_job工程定义交换机、队列、消息转化资源bean:
@Configuration
public class MqConfig {
/**
* 重新定义消息序列化的方式,改为基于json格式序列化和反序列化
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 国内大盘信息队列
* @return
*/
@Bean
public Queue innerMarketQueue(){
/*
这里的 true 参数是用于指定队列是否应该是持久的(durable)。
具体来说:
第一个参数 "innerMarketQueue" 是队列的名称。
第二个参数 true 指定该队列是否是持久的。
如果队列是持久的(durable = true),那么即使 RabbitMQ 服务器重启,该队列和其中的消息也不会丢失。这对于确保消息在系统故障或重启后仍然可用是非常重要的。
如果队列不是持久的(durable = false),那么当 RabbitMQ 服务器重启时,该队列和其中的消息都会被删除。
需要注意的是,即使队列是持久的,其中的消息默认也不是持久的。要确保消息也是持久的,你需要在发送消息时设置消息的持久性属性。
另外,要确保 RabbitMQ 正确地持久化队列和消息,还需要确保 RabbitMQ 的存储后端(如 Mnesia、RabbitMQ on Erlang、或高级存储后端)也被正确配置和启用。
*/
return new Queue("innerMarketQueue",true);
}
/**
* 定义路由股票信息的交换机
* @return
*/
@Bean
public TopicExchange innerMarketTopicExchange(){
/*
第一个 true:表示交换机是否是持久的(durable)。如果设置为 true,则交换机将在 RabbitMQ 服务器重启后继续存在。如果设置为 false,则服务器重启后交换机将被删除。
第二个 false:表示交换机是否应该自动删除(autodelete)。如果设置为 true,则当最后一个队列解绑到这个交换机上之后,该交换机将自动删除。如果设置为 false,则交换机将一直存在,直到明确地被删除。
*/
return new TopicExchange("stockExchange",true,false);
}
/**
* 绑定队列到指定交换机
* @return
*/
@Bean
public Binding bindingInnerMarketExchange(){
return BindingBuilder.bind(innerMarketQueue()).to(innerMarketTopicExchange())
.with("inner.market");
}
}
在stock_job工程com.itheima.stock.service.impl.StockTimerTaskServiceImpl#getInnerMarketInfo方法下添加最新大盘数据同步逻辑:
@Override
public void getInnerMarketInfo() {
//......
//解析的数据批量插入数据库
int count= stockMarketIndexInfoMapper.insertBatch(entities);
log.info("当前插入了:{}行数据",count);
//通知后台终端刷新本地缓存,发送的日期数据是告知对方当前更新的股票数据所在时间点
rabbitTemplate.convertAndSend("stockExchange","inner.market",new Date());
}
3、主业务后端大盘数据缓存刷新
在stock_backend工程同样集成amqp,过程与stock_job工程一致,同时主业务工程获取最新大盘数据后,并同步到本地缓存中:
工程引入CaffeineCache依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
在CommonConfig中配置本地缓存bean:
/**
* 构建缓存bean
* @return
*/
@Bean
public Cache<String,Object> caffeineCache(){
Cache<String, Object> cache = Caffeine
.newBuilder()
.maximumSize(200)//设置缓存数量上限
// .expireAfterAccess(1, TimeUnit.SECONDS)//访问1秒后删除
// .expireAfterWrite(1,TimeUnit.SECONDS)//写入1秒后删除
.initialCapacity(100)// 初始的缓存空间大小
.recordStats()//开启统计
.build();
return cache;
}
监听消息,刷新缓存:
/**
* 监听股票变化消息
*/
@Component
@Slf4j
public class MqListener {
@Autowired
private Cache<String,Object> caffeineCache;
@Autowired
private StockService stockService;
/**
*
* @param infos
* @throws Exception
*/
@RabbitListener(queues = "innerMarketQueue")
public void acceptInnerMarketInfo(Date date)throws Exception{
//获取时间毫秒差值
long diffTime= DateTime.now().getMillis()-new DateTime(date).getMillis();
//超过一分钟告警
if (diffTime>60000) {
log.error("采集国内大盘时间点:{},同步超时:{}ms",new DateTime(date).toString("yyyy-MM-dd HH:mm:ss"),diffTime);
}
//将缓存置为失效删除
caffeineCache.invalidate("innerMarketInfosKey");
//调用服务更新缓存
stockService.getNewestInnerMarketInfos();
}
}
在com.itheima.stock.service.impl.StockServiceImpl#getInnnerMarketInfos方法中添加缓存查询逻辑:
@Autowired
private Cache<String,Object> caffeineCache;
/**
* 定义获取A股大盘最新数据
* @return
*/
@Override
public R<List<InnerMarketDomain>> getInnnerMarketInfos() {
//从缓存中加载数据,如果不存在,则走补偿策略获取数据,并存入本地缓存
R<List<InnerMarketDomain>> data= (R<List<InnerMarketDomain>>) caffeineCache.get("innerMarketInfos", key->{
//如果不存在,则从数据库查询
//1.获取最新的股票交易时间点
Date lastDate = DateTimeUtil.getLastDate4Stock(DateTime.now()).toDate();
//TODO 伪造数据,后续删除
lastDate=DateTime.parse("2022-01-03 09:47:00", DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss")).toDate();
//2.获取国内大盘编码集合
List<String> innerCodes = stockInfoConfig.getInner();
//3.调用mapper查询
List<InnerMarketDomain> infos= stockMarketIndexInfoMapper.getInnerIndexByTimeAndCodes(lastDate,innerCodes);
//4.响应
return R.ok(infos);
});
return data;
}