Java常用中间件整理讲解——Redis,RabbitMQ
整理Java后端常用的中间件,描述使用及其思想。
1:Redis
Redis是一个开源的键值存储系统,常用于缓存、消息队列等场景。Redis快的原因是内存操作、非阻塞I/O多路复用,避免了线程切换和竞争。
Redis还提供持久化的方式,以及后续版本的多线程处理IO。Redis的高可用也是重要的技术点。
1:数据结构与类型
支持多种数据结构,以及一些扩展的类型,简单介绍使用和场景,以及底层。后几种是扩展,大致描述下。
1:String
字符串,最常用的数据结构,底层是通过SDS动态字符串存储数据,自动扩容。
直接使用set,get命令默认操作的是字符串,设置键及对应的值,并可设置过期时间。
SET key value [EX seconds] [PX milliseconds] # 设置值(支持过期时间)
GET key # 获取值
INCR key # 自增计数器
APPEND key value # 追加内容
MSET key1 val1 key2 val2 # 批量设置
适用场景:缓存、计数器、分布式锁等。
分布式锁:SET lock_key uuid NX PX 10000(NX:不存在才设置,PX:过期时间)。
2:Hash
哈希,注意不要和Java的哈希搞混了,Redis默认就是键值存储,这里的Hash就是值对应多组,类似Java的Map里面套一个Map。
底层结构是压缩列表(zipList)或哈希表(dict),小数据时适用压缩列表。
使用:在语句的操作前面添加一个 H,例如HSet。
HSET key field value # 设置字段值
HGET key field # 获取字段值
HGETALL key # 获取所有字段和值
HDEL key field # 删除字段
HINCRBY key field 1 # 字段自增
适用场景:对象信息存储,或多个商品详情。
例如:HSET user:1001 name "Alice" age 30
3:List
列表,元素有序,按元素顺序排列。
底层是快速链表(压缩列表加双向链表),支持头尾高效操作。
双向链表结构,可以从两端操作数据。操作左边数据称为栈,添加L。操作右边数据称为队列,添加R。并且支持弹出和阻塞弹出。
LPUSH key value1 value2 # 左端插入(栈)
RPUSH key value1 value2 # 右端插入(队列)
LPOP key # 左端弹出
BLPOP key timeout # 阻塞式左端弹出(用于消息队列)
LRANGE key 0 -1 # 获取全部元素
可以实现消息队列场景,左端插入,右端阻塞式消费数据。以及实现朋友圈最新列表场景。
4:Set
集合,无序,元素存储顺序不固定,且元素唯一(不可重复)。
底层结构是整数集合(元素全为整数时),或哈希表。
使用:在语句前面添加 S。
SADD key member1 member2 # 添加元素
SREM key member # 删除元素
SMEMBERS key # 获取所有元素(慎用,可能阻塞)
SINTER key1 key2 # 求交集(共同好友)
SUNIONSTORE dest key1 key2 # 并集存储到新集合
可用于统计用户兴趣标签,或去重取交集并集等。
需注意交集/并集操作复杂度高(O(n)),需控制集合大小。
5:ZSet
有序集合,根据元素的 score(分值)排序,score 相同时按字典序排序。
元素唯一,但 score 可重复,支持范围查询和排行榜等。底层是通过跳跃表和哈希表实现高效排序。
使用:在Set命令的基础上,为每个键设置分值,即可实现排序功能。
ZADD key score1 member1 score2 member2 # 添加元素
ZRANGE key 0 -1 WITHSCORES # 按分值升序获取
ZREVRANGE key 0 9 # 降序取Top10
ZRANK key member # 获取排名
ZRANGEBYSCORE key min max # 按分值范围查询
是作为Set无序集合的补充,适用于不可重复但有序场景,例如用户排行榜等。
跳跃表查询时间复杂度为O(log n),适合高频读场景。
6:位图
本质上是字符串的二进制位操作。使用命令为 set/getbit,后面用法一样,指定键和值。
SETBIT key offset 1 # 设置二进制位(0或1)
GETBIT key offset # 获取位值
BITCOUNT key # 统计1的个数(日活统计)
BITOP OR dest key1 key2 # 位运算(用户行为分析)
适用于用户的签到,以及唯一状态标记等场景。
7:地理空间
基于ZSet实现,存储经纬度。使用为 GEO 开头的命令。
GEOADD key longitude latitude member # 添加地理位置
GEODIST key member1 member2 # 计算距离
GEORADIUS key lon lat radius unit # 查询附近的人
可实现附近商家,打车距离等运算,但一般会在代码中做。
8:流
Stream,特点是支持多消费者组的消息队列,类似MQ。使用为X开头的命令。
XADD key * field1 value1 # 添加消息(*自动生成ID)
XREAD COUNT 10 STREAMS key 0 # 读取消息
XGROUP CREATE key group1 0 # 创建消费者组
XACK key group1 id # 确认消息处理
可在Redis实现消息的收发机制,异步队列的处理,日志收集等。
2:持久化机制
Redis的持久化是保障数据安全的核心功能,提供了RDB(快照)、AOF(日志追加)和混合持久化三种方式,各有其不同的设计理念。
1:RDB
Redis Database,快照机制,核心原理是通过 fork 子线程生成内存数据的二进制压缩快照。
简单来说就是,将某个时间点的所有数据都存放在硬盘上,可以将快照(文件)复制到其他服务器,从而创建副本,恢复数据。如果系统故障,会丢失最后一次创建快照之后的数据。
有两种方式方式:
- 手动触发:通过 SAVE(生产禁用,阻塞主线程)命令,或 BGSAVE(后台异步生成)。
- 自动触发:通过配置文件自动触发,例如save 900 1,表示900秒内至少修改一次则触发。
其还有一个关键特点:写时复制,父进程与子进程共享内存页,仅当数据修改时复制内存页,避免全量内存拷贝。
优点:
- 二进制文件直接加载到内存,恢复速度快。
- 文件体积小,适合全量备份与灾难恢复。
- 后台异步生成快照,主线程仅在 fork 时有短暂阻塞,性能和体验较好。
缺点:
- 两次快照之间的内容可能丢失,依赖于触发频率。
- 会有大内存问题(写时复制通病),fork子线程时内存占用翻倍,对硬件有要求。
2:AOF
Append-Only File,日志追加机制,核心原理是记录所有写操作命令(如SET、HSET),以文本协议格式追加到AOF文件末尾,通过策略控制数据写入磁盘,同步数据。
首先,对文件进行写入会先存储进缓冲区,然后根据策略 appendfsync 决定同步磁盘的时机。提供了三个同步选项:
- always:每次写命令后同步(数据安全,性能最低),严重降低服务器性能。
- everysec(默认):每秒批量同步(折中方案),保证崩溃时丢失一秒左右的数据,且性能几乎无影响。
- no:由操作系统决定(性能最高,可能丢失约30秒数据),一般在AOF关闭或Redis关闭时执行。
AOF还提供了一个重写机制,目的是解决AOF文件膨胀问题,同样可通过手动触发和自动触发的方式实现。
实现原理为:fork子线程遍历内存数据,生成新的紧凑AOF文件。重写期间的新命令写入AOF重写缓冲区,重写完成后追加到新AOF文件。
重写期间的新命令同时写入AOF缓冲区和AOF重写缓冲区。若重写失败,原始AOF文件仍完整。
AOF的优点:安全性高,默认最多丢失一秒数据,并且文本可读性强,便于维护和扩展。
缺点:相同数据集下,AOF文件通常比RDB文件大数倍。且需要逐行执行命令,耗时较长。
3:混合持久化
Redis 4.0+引入,结合RDB快照与AOF增量日志的方式实现。
通过命令 aof-use-rdb-preamble yes 开启:AOF重写时先生成RDB快照,再追加重写期间的AOF日志。
文件结构为:[RDB快照数据] + [AOF增量命令],恢复数据时,优先加载RDB快照恢复基础数据,再通过AOF日志恢复增量数据。
优点:
- 兼顾RDB的快速恢复与AOF的数据安全性。
- 文件体积比纯AOF更小,恢复速度比纯AOF更快。
- 生产环境推荐方案,尤其对数据安全与恢复速度均有要求的场景。
生产上可通过监控 fork 耗时,进行持久化优化,以及定时备份文件到异地,测试宕机恢复等。
3:三大问题
缓存使用中的三个问题,场景描述及解决方式。
1:缓存穿透
这个问题和另外两个不属于同一类,可以先描述。
定义:当一个请求从缓存中取数据时,如果请求的key在缓存中不存在,就会绕过缓存,直接请求数据库。如果数据库返回null,又不会写入缓存。
这样会导致多次请求反复查询数据库,引发数据库压力。
解决:
1:使用布隆过滤器,查询前先通过布隆过滤器检查,如果返回不存在则直接拒绝请求,返回可能存在(可能误判)时,再查缓存或数据库。
2:将查询结果为空的键也缓存起来,并设置较短的过期时间。但是需避免空值过多占用内存。
3:对接口增加校验、限流等,对请求参数检验过滤。
2:缓存击穿
定义:某个热点Key在高并发场景下突然过期,大量请求直接穿透到数据库,导致数据库瞬时压力激增。
与穿透的区别是,发生问题前,缓存中有键值数据。
解决:
1:热点key设置为永不过期,在业务代码中判断是否更新时间。
2:实现互斥锁,只允许一个线程重建缓存,其他线程阻塞等待,需避免死锁。
3:热点数据预加载,监控热点key(如Redis的hotkeys),在即将过期时,主动刷新缓存避免击穿。
3:缓存雪崩
定义:大量缓存数据同时过期或缓存服务宕机,导致所有请求直接访问数据库,引发数据库崩溃。
注意与击穿的区别是:大量 Key 同时失效。
原因可能是同一时间给多个key设置相同过期时间,或集群宕机、网络中断等。
解决:
1:分散设置过期时间,例如在同一时间,给多个key设置指定范围随机数过期时间。
2:同样的将缓存设置永不过期,在业务代码中更新缓存。
3:接口处添加熔断降级配置,数据库压力过大时,直接返回提示或错误页面。
4:高可用架构
高可用是指Redis在生产环境,保证程序的稳定和正确性,其实这个思想和Mysql这些都是一样的。
1:主从复制
主从复制是 Redis 高可用的基础,通过数据冗余实现读写分离和故障恢复,说白了就是启动多个Redis不同端口服务,指定服务分别干哪些事情。
1:定义主节点( Master)和从节点(Slave)。
- 主节点负责写操作,并异步同步数据到从节点。
- 从节点负责读操作,接收主节点同步的数据。
2:全量同步,首次连接会触发。
- 从节点发送 PSYNC 命令请求同步。
- 主节点执行 BGSAVE 生成RDB快照,发送给从节点。
- 从节点清空旧数据,加载RDB文件。
- 最后,主节点会将同步期间的写命令存入缓冲区,RDB传输完成后发送缓冲区命令。
3:增量同步,断线重连等场景。
- 从节点发送PSYNC命令并携带复制偏移量(offset)。
- 主节点检查复制缓冲区是否包含该偏移量之后的命令,如果存在,则只传输缓冲区命令。如果不存在,则触发全量同步。
优点:简单易用,支持读写分离,提升读性能。
缺点:主节点单点故障,需配合哨兵或集群实现高可用。
2:哨兵机制
哨兵是 Redis 官方推荐的故障自动转移系统,用于监控主从节点并实现主节点故障转移。
核心特性是定期监控主从节点健康状态,主节点宕机时选举新的主节点,客户端通过哨兵获取到最新的主节点地址。
注意哨兵可以配置多个,并且可指定几个哨兵控制选举,称为主观下线和客观下线。
转移流程:
1:先触发主观下线,即单个哨兵认为主节点不可用。
2:进一步触发客观下线,超过半数哨兵确认主节点不可用。
3:通过 Raft 算法选举 Leader 哨兵。
4:选择新的主节点,会先过滤掉已下线的从节点,进一步选择优先级高、偏移量小、ID小的从节点。
5:进入切换流程,将选中的从节点升级为主节点,向其他从节点发送REPLICAOF命令,指向新主节点。并通知客户端更新主节点地址。
优点:自动故障转移,客户端透明切换。
缺点:写操作仍集中在单主节点,无法水平扩展写性能(类似MySQL的水平分表,治标不治本)。
3:集群架构
Redis 集群是分布式解决方案,通过分片(Sharding)实现数据水平拆分和负载均衡。即部署多个Redis服务。
核心特性:
- 哈希槽:集群将数据划分为 16384 个槽,每个节点负责部分槽(好类似G1垃圾回收)。key通过公式计算所属槽。
- 节点角色优化,主节点处理槽数据读写,从节点复制主节点数据,故障时替代主节点。
类似公司的分片机制,客户端缓存槽与节点(Redis服务)映射关系,直接请求目标节点。
故障转移需要与哨兵协作,当主节点下线时,从节点自动晋升为主节点(需多数主节点确认)。而当半数主节点或某一个槽下的主从节点均下线时,表示当前集群不可用。
扩容与缩容:
扩容流程:添加新节点,然后执行 CLUSTER MEET加入集群,再通过命令迁移槽数据。
缩容流程:迁移待下线节点的槽到其他节点,执行CLUSTER FORGET移除节点。
优点:数据分片、负载均衡、高可用性(无需额外哨兵)。
缺点:配置复杂,跨槽操作不支持(需使用Hash Tag)。
集群模式下数据一致性问题:
异步复制下可能丢失数据,需权衡一致性与性能。
同步写入(WAIT命令)强制等待从节点确认,但牺牲性能。
使用建议:数据量 < 单机内存时,选择主从+哨兵。数据量 > 单机内存时,考虑集群。
