技术栈:Springboot+RabbitMq
本文主题:1.去除Redis,使用进程内缓存;2.集中缓存管理。
目的:降低对外部组件的依赖性,进程内缓存的引入使得系统处理实时业务请求时,不直接读取外部的缓存中心或者数据库,这样避免了这些基础组件可能带来的风险,这点很重要,做系统久了你就明白这点点东西的重要性了。
我们在写系统时为了提高系统业务处理速度,在系统中大量使用Redis作为缓存使用,但是对于一些信息使用Reids是首选吗?我们还是以支付系统为例,对于对接的通道的一些配置信息、交易机构信息(银行),响应码转换信息等等,真的有必要上来就怼Redis里吗?对于这些信息每笔交易都会使用到,那么我们系统在运行时强依赖Redis,还是那句话,我们系统在运行时依赖的外界系统越少越好,依赖的越多,在后期运行过程系统性能瓶颈有很大可能发生在和这些系统交互上面,甚至依赖的外界系统异常、宕机直接导致系统不可用,这些问题我相信在很多公司发生过,笔者公司也不列外,整个项目组都在用一个Redis集群,其他系统由于对Redis使用不当,什么都往里怼,结果十几个G内存都怼满了,导致Redis不可用。同时对于稍大点公司服务一般多机房部署,跨机房访问Redis也是棘手问题,这些都是现实工作遇到的问题,所以在系统设计的时候多考虑点。
这里插入一条有意思的问题,和本博文虽然没多大关系,但是我觉得很多人都搞不清的一个东西,我们都知道Redis在RDB持久化时候会fork出一个子线程进行数据持久化,如果给Redis分配内存为10G,在持久化时候已经使用6G,那么他能fork出子线程能进行持久化吗?并且在持久化时候也是需要时间的,比如在8:00时候fork出一条子线程进行持久化,持久化时间持续5min,我们都知道,在子线程持久化时,主线程依旧提供服务,数据依旧在改动,那么我们持久出的数据时点是多少?8:00的数据还是8:05的数据?如果搞不清楚,为了不使本篇文太长,跳转到另篇博文吧(先欠着)。
不喜欢码纯技术博客,八股文,没意思,看了就忘,所以有时间就码码如何用技术来解决实际工作中遇到问题之类的博文吧,在解决实际问题时怎么设计,使用什么技术,面对相同业务场景,不同的人、公司都会有不同的解决方案,也希望能找到更好的解决方案,共同学习交流吧,未知领域自己一直闷头默默搞,不如抬头观望下。
废话不多扯了,回归本博文主题,如何将散落在系统各处的缓存做到集中管理配置并使用进程内缓存,好多人喜欢用Redis缓存也是有原因的,俩字:简单,在一个节点对数据更改,其他节点就可使用,不涉及到使用进程内缓存多节点刷新缓存的问题,使用进程内缓存我们需要解决的是如果刷新多节点缓存问题,这是我们整篇博文待解决问题,解决方式有很多,比如使用Redis、MQ,说到此处,有人可能会和我犟,这还不是使用了Redis呀,你要理解你系统接口一般会分两种,一个种是运行时接口,一种是后台接口,上者使用Redis作为缓存是在运行时接口中使用,此处如果使用Redis的话,使用其发布订阅功能,将缓存刷新任务放入Redis,其他节点订阅刷新本地缓存,这种应用是属于后台接口中使用,如果不明白我在说什么那就不要犟了。此处我们也不使用Redis,我们使用MQ。
我们现在知道了,使用进程内缓存,我们需要解决的问题是多节点缓存刷新问题,上面说了,我们使用MQ解决,使用MQ的话,我们就是要解决的是如何使一个消息被多个节点消费到,直接上结构图吧。
运营人员操作的界面,在配置完相应的配置信息后,点击对应的配置刷新即可以刷新每个节点对应配置的缓存,以此做到缓存的集中管理,原型图画的有点丑,勉强看吧....
下面我们说下运营人员点击刷新后台代码发生了什么样的数据流转,如下:已经很清楚的保姆级别流程图了,没太多技术内容,主要就是Fanout类型的交换器与匿名队列的使用。
匿名队列作用:为了使同一个应用的多节点都能消费到生产者发布同一个消息,消费端不定义队列名,让系统默认生成(默认情况下,队列名称以spring.gen-为前缀),每个节点都生成属于自己的一个队列。好多人有个误区,以为使用Fanout交换机,消息被路由到绑定的队列后,如果同一个队列有多个消费者,此消息会被所有消费者消费到,这对么?首先你要搞清楚一件东西,fanout是交换器的一种类型,是交换器层面东西,也只是消息路由到队列的一种策略,和队列有关系吗?这样看答案自然也就知道了,如果你想要多个消费者都能消费到同一个消息,那么他们就得有各自的队列,不同于kafka有消费者组的概念,此处我么就不展开了,此处不接收反驳。
设计思想如上,代码也没啥,贴几个主要关键类吧,具体实现源码就不公开了....
/**
* @author kkk
* @Description: 消费端监听
* @date 2021/11/13
*/
@Component
public class RabbitMqListener {
private Logger logger = LoggerFactory.getLogger(RabbitMqListener.class);
/**
* 缓存刷新监听
* @param messageEntity
* @param channel
* @param messageSource
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue, exchange = @Exchange(value = SystemConstant.CACHE_MANAGER_FANOUT_EXCHANGE, type = ExchangeTypes.FANOUT, durable = "true", autoDelete = "false")), containerFactory = "rabbitListenerContainerFactory")
public void processCacheRefreshListener(MessageEntity messageEntity, Channel channel, Message messageSource) {
try {
String transCode = messageEntity.getHeader().getTransCode();
FanoutMessageHandler fanoutMessageHandler = MqMessageHandlerFactory.getFanoutHandler(transCode);
if (fanoutMessageHandler != null) {
fanoutMessageHandler.handle(messageEntity);
} else {
logger.warn("MQ消息监听-({})没有找到监听处理器", transCode);
}
} catch (Exception e) {
logger.error("MQ消息监听-处理异常", e);
}
}
}
异议错误处欢迎指点。