一文搞懂中间件:缓存与消息队列的核心作用
一、什么是中间件?
中间件,本质上是别人已经写好的开源程序,并且这个开源程序具备特定的业务属性。
在行业里,中间件一般都是大厂为了解决特定的业务功能而研发的程序,在内部用着效果不错,然后开源出来供大家使用。
简单来说,中间件就是帮我们解决特定场景问题的 “成熟工具”,让我们不用重复造轮子,专注于业务逻辑开发。
二、中间件的常见类型
中间件的种类很多,其中最核心、最常用的主要有两大类:缓存类型和消息队列
| 类型 | 代表产品 | 核心解决问题 | 应用场景 |
|---|---|---|---|
| 缓存中间件 | Redis、Memcached | 提升数据读取速度,降低数据库压力 | 高频查询、秒杀库存、用户会话存储 |
| 消息队列中间件 | RabbitMQ、Kafka、RocketMQ | 系统解耦、异步通信、流量削峰 | 订单异步通知、日志收集、秒杀削峰 |
三、什么是缓存?
3.1 理解缓存
有一句很经典的话:性能不够,缓存来凑。
缓存存在的意义,就是为了优化我们程序的性能。
要理解缓存,我们需要先了解计算机硬件的性能差异:
- CPU:用来计算的,速度极快。
- 内存(如 16G、32G):
- 优点:读写速度特别快。
- 缺点:不具备数据持久化的能力,且单位成本高。
- 磁盘(如 500G、1T):
- 优点:数据稳定,可持久化存储。
- 缺点:读写速度特别慢。
| 硬件组件 | 核心功能 | 关键参数示例 | 优点 | 缺点 | 访问速度层级 |
|---|---|---|---|---|---|
| CPU | 数据计算与指令执行 | - | 速度极致(纳秒级) | 无存储能力,仅临时运算 | 1 级(最快) |
| 内存 | 临时数据存储与读写 | 16G、32G、64G | 读写速度极快(微秒级),适配 CPU 运算节奏 | 断电数据丢失,单位成本高,容量有限 | 2 级 |
| 磁盘 | 持久化数据存储 | 500G SSD、1T HDD | 容量大,单位成本低,断电数据不丢失 | 读写速度慢(毫秒级),磁盘 IO 是性能 |
3.2 缓存的核心工作模式
缓存的核心思想是:将高频访问数据从 “慢速磁盘” 迁移到 “高速内存”,磁盘仅作为数据持久化备份,彻底规避频繁的磁盘 IO 操作(先将数据放在内存中,磁盘做备份)。
模式 1:传统无缓存架构(性能瓶颈明显)
用户请求 ---> 应用程序(Java/Spring Boot) ---> 数据库(MySQL,存储于磁盘) ---> 返回数据至用户
问题:每次请求都需触发磁盘 IO,高频访问场景下,数据库会成为系统瓶颈,响应延迟急剧上升。
模式 2:引入缓存的优化架构(核心实战流程)
用户请求 ---> 应用程序 ---> 缓存(如Redis,存储于内存) ---> 命中则直接返回
↓(未命中)
数据库 ---> 读取数据 ---> 写入缓存 ---> 返回数据至用户
3.3 关键操作:缓存预热
缓存预热是在系统启动或低峰期,主动将高频访问数据提前查询并写入缓存的操作,是缓存架构中不可或缺的一步,简单来说就是提前将高频访问数据查询出来,放到内存中,当用户再次访问这些数据时,程序直接从内存中读取,速度得到极大提升。
(1)核心价值
- 避免系统启动初期,大量请求直接穿透缓存访问数据库,导致数据库压力骤增;
- 保证核心业务数据(如首页商品列表、热门活动信息)始终在缓存中,确保用户请求的低延迟响应。
3.4 缓存实战
(1) 实体类:User
定义用户实体,包含姓名、手机号等核心字段:
public class User {
/**
* 姓名
*/
private String name;
/**
* 手机号
*/
private String phone;
private int money;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
(2) 模拟数据源:UserList
模拟磁盘中的用户列表,提供 “根据用户名查手机号” 的基础方法:
import java.util.ArrayList;
import java.util.List;
public class UserList {
/**
* 用来模拟从磁盘中读取的用户列表
*/
public static List<User> users = new ArrayList<User>();
// 静态代码块初始化模拟数据
static {
User user = new User();
user.setName("甄聪");
user.setPhone("18733225351");
User user2 = new User();
user2.setName("张三");
user2.setPhone("10086");
users.add(user);
users.add(user2);
}
// 按照姓名查询手机号
public static String getPhoneByUserName(String userName) {
for (User user : users) {
if (user.getName().equals(userName)) {
return user.getPhone();
}
}
return null;
}
}
(3)缓存管理工具:CacheManager
实现缓存的核心操作(新增、查询、删除),基于HashMap模拟缓存容器:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 缓存管理
* 缓存中间件
*/
public class CacheManager {
/**
* 字符串类型缓存(key:用户名,value:手机号)
*/
private static Map<String, String> CACHE = new HashMap<String, String>();
/**
* 场景扩展:朋友圈点赞缓存(key:朋友圈id,value:点赞用户列表)
*/
private static Map<String, List<User>> USERLSTACAHE = new HashMap<>();
/**
* 向列表型缓存新增元素
* @param key 缓存key
* @param user 待新增用户
*/
public static void addList(String key, User user) {
List<User> users = USERLSTACAHE.getOrDefault(key, new ArrayList<>());
users.add(user);
USERLSTACAHE.put(key, users);
}
// 取消点赞(示例预留方法,可补充实现)
public static void deleteUser(String key, User user) {}
/**
* 刷新缓存(新增/更新)
* @param key 缓存key
* @param value 缓存value
*/
public static void put(String key, String value) {
CACHE.put(key, value);
}
/**
* 查询缓存
* @param key 缓存key
* @return 缓存value(未命中返回null)
*/
public static String get(String key) {
return CACHE.get(key);
}
/**
* 删除缓存
* @param key 缓存key
*/
public static void remove(String key) {
CACHE.remove(key);
}
}
(4)核心业务逻辑:Test
实现 “缓存优先查询”+“缓存预热” 的核心逻辑:
import java.util.List;
public class Test {
public static void main(String[] args) {
// 缓存预热:提前加载所有用户数据到缓存
putAllCache();
// 多次查询,验证缓存命中效果
getPhoneByUserName("甄聪");
getPhoneByUserName("甄聪");
getPhoneByUserName("甄聪");
getPhoneByUserName("张三");
}
/**
* 根据用户名查询手机号(缓存优先)
* @param name 用户名
* @return 手机号
*/
public static String getPhoneByUserName(String name) {
// 1. 先查询缓存
String phone = CacheManager.get(name);
// 2. 缓存未命中:从数据源查询,并写入缓存
if (phone == null) {
System.out.println("缓存未命中,从数据源查询:" + name);
phone = UserList.getPhoneByUserName(name);
// 写入缓存,后续查询可直接命中
CacheManager.put(name, phone);
} else {
System.out.println("缓存命中:" + name);
}
return phone;
}
/**
* 缓存预热:系统启动时加载所有用户数据到缓存
*/
public static void putAllCache() {
// 将UserList中的所有用户数据提前放入缓存
List<User> users = UserList.users;
for (User user : users) {
CacheManager.put(user.getName(), user.getPhone());
}
System.out.println("缓存预热完成,共加载" + users.size() + "条用户数据");
}
}
(5)代码执行流程
缓存命中:查询时直接从CacheManager的CACHEMap 中获取数据,无需访问数据源;
缓存未命中:先从UserList(模拟磁盘)查询数据,再将结果写入缓存,确保下次查询命中。
putAllCache() 方法在程序启动时执行,将UserList中的所有用户数据(姓名 - 手机号)提前写入缓存。
输出如下(缓存预热后所有查询均命中):
缓存预热完成,共加载2条用户数据
缓存命中:甄聪
缓存命中:甄聪
缓存命中:甄聪
缓存命中:张三
若注释putAllCache()(关闭缓存预热),首次查询 “甄聪” 会输出 “缓存未命中”,后续查询则命中。
四、什么是消息队列?
消息队列:一般我们会简称它为MQ(Message Queue)。
Message Query(MQ),消息队列中间件,很多初学者认为,MQ通过消息的发送和接受来实现程序的异步和解耦,mq主要用于异步操作,这个不是mq的真正目的,只不过是mq的应用,mq真正的目的是为了通讯。
他屏蔽了复杂的通讯协议,像常用的dubbo,http协议都是同步的。
这两种协议很难实现双端通讯,A调用B,B也可以主动调用A,而且不支持长连接。mq做的就是在这些协议上构建一个简单协议——生产者、消费者模型,mq带给我们的不是底层的通讯协议,而是更高层次的通讯模型。他定义了两个对象:发送数据的叫做生产者,接受消息的叫做消费者,我们可以无视底层的通讯协议,我们可以自己定义生产者消费者。
五、完整总结
本文从基础认知、底层逻辑、实战代码、协同场景、避坑要点五个维度,完整覆盖了缓存与消息队列两大核心中间件:
- 缓存中间件:核心是 “空间换时间”,通过内存存储高频数据,解决系统性能瓶颈,关键要点是缓存预热、缓存避坑;
- 消息队列中间件:核心是 “异步解耦 + 削峰填谷”,通过队列实现系统间通信,关键要点是模式选择、可靠性保障、幂等性处理;
- 协同价值:缓存聚焦 “读性能优化”,消息队列聚焦 “写 / 通信高可用”,二者结合可构建高性能、高可用的后端系统。
更多推荐
所有评论(0)