大数据技术栈 —— Redis与Kafka

什么是缓存

缓存原指 CPU 上的一种高速存储器,它先于内存与 CPU 交换数据,速度很快
现在泛指存储在计算机上的原始数据的复制集,便于快速访问。
在互联网技术中,缓存是系统快速响应的关键技术之一
以空间换时间的一种技术(艺术)

使用场景

用作DB缓存
在大量瞬间访问时(高并发) MySQL 单机会因为频繁 IO 而造成无法响应。 MySQL 的 InnoDB 是有行锁
将数据缓存在 Redis 中,也就是存在了内存中。
内存天然支持高并发访问。可以瞬间处理大量请求。
qps 到达 11 万 /S 读请求 8 万写 /S

session分离
将登录成功后的 Session 信息,存放在 Redis 中,这样多个服务器 (Tomcat) 可以共享 Session 信息。

分布式锁
一般讲锁是多线程的锁,是在一个进程中的
多个进程( JVM )在并发时也会产生问题,也要控制时序性
可以采用分布式锁。使用 Redis 实现 sexNX

乐观锁
同步锁和数据库中的行锁、表锁都是悲观锁
悲观锁的性能是比较低的,响应性比较差
高性能、高响应(秒杀)采用乐观锁 (CAS)
Redis 可以实现乐观锁 watch + incr

三种读写模式

Cache Aside Pattern (旁路缓存),是最经典的缓存 + 数据库读写模式。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
Read-Through (穿透读模式 / 直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入 缓存。(guavacache ) Write-Through(穿透写模式 / 直写模式):应用程序写缓存,缓存写数据库。 该种模式需要提供数据库的handler ,开发较为复杂。
Write Behind Caching Pattern
应用程序只更新缓存。
缓存通过异步的方式将数据批量或合并后更新到 DB 中
不能时时同步,甚至会丢数据

redis介绍

Redis ( Remote Dictionary Server )远程字典服务器,是用 C 语言 开发的一个 开源 的高性能 键值 ( key - value ) 内存数据库
它提供了 五种数据类型 来存储值:字符串类型、散列类型、列表类型、集合类型、有序集合类型
它是一种 NoSQL 数据存储。

上干货

淘汰策略
LRU
LRU (Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想
是 " 如果数据最近被访问过,那么将来被访问的几率也更高 " 。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。
  4. 在 Java 中可以使用 LinkHashMap (哈希链表)去实现 LRU
    Redis LRU 数据淘汰机制
    在服务器配置中保存了 lru 计数器 server.lrulock ,会定时( redis 定时程序 serverCorn() )更新,server.lrulock 的值是根据 server.unixtime 计算出来的。
    另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru 。可以想象的是,每一次访问数据的时候,会更新 redisObject.lru 。
    LRU 数据淘汰机制是这样的:在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰。
    不可能遍历 key 用当前时间 - 最近访问 越大 说明 访问间隔时间越长
    volatile-lru
    从已设置过期时间的数据集( server.db[i].expires )中挑选最近最少使用的数据淘汰
    allkeys-lru
    从数据集( server.db[i].dict )中挑选最近最少使用的数据淘汰
    LFU
    LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
    volatile-lfu
    allkeys-lfu
    random
    随机
    volatile-random
    从已设置过期时间的数据集( server.db[i].expires )中任意选择数据淘汰
    allkeys-random
    从数据集( server.db[i].dict )中任意选择数据淘汰
    ttl
    volatile-ttl
    从已设置过期时间的数据集( server.db[i].expires )中挑选将要过期的数据淘汰
    redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires 。
    TTL 数据淘汰机制:从过期时间的表中随机挑选几个键值对,取出其中 ttl 最小的键值对淘汰。
    noenviction
    禁止驱逐数据,不删除 默认
    缓存淘汰策略的选择
    allkeys-lru : 在不确定时一般采用策略。 冷热数据交换
    volatile-lru : 比 allkeys-lru 性能差 存 : 过期时间
    allkeys-random : 希望请求符合平均分布 ( 每个元素以相同的概率被访问 )
    自己控制: volatile-ttl 缓存穿透
    expire原理
    typedef struct redisDb {
    dict * dict ; -- key Value
    dict * expires ; -- key ttl
    dict * blocking_keys ;
    dict * ready_keys ;
    dict * watched_keys ;
    int id ;
    } redisDb ;
    dict 用来维护一个 Redis 数据库中包含的所有 Key-Value 键值对, expires 则用于维护一个 Redis 数据
    库中设置了失效时间的键 ( 即 key 与失效时间的映射 ) 。
    当我们使用 expire 命令设置一个 key 的失效时间时, Redis 首先到 dict 这个字典表中查找要设置的 key
    是否存在,如果存在就将这个 key 和失效时间添加到 expires 这个字典表。
    当我们使用 setex 命令向系统插入数据时, Redis 首先将 Key 和 Value 添加到 dict 这个字典表中,然后
    将 Key 和失效时间添加到 expires 这个字典表中。
    简单地总结来说就是,设置了失效时间的 key 和具体的失效时间全部都维护在 expires 这个字典表中。
    删除策略
    Redis 的数据删除有定时删除、惰性删除和主动删除三种方式。
    Redis 目前采用惰性删除 + 主动删除的方式。
    定时删除
    在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。需要创建定时器,而且消耗CPU ,一般不推荐使用。
    惰性删除
    在 key 被访问时如果发现它已经失效,那么就删除它。
    调用 expireIfNeeded 函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删
    除它。
    int expireIfNeeded ( redisDb * db , robj * key ) {
    // 获取主键的失效时间 get 当前时间 - 创建时间 >ttl
    long long when = getExpire ( db , key );
    // 假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为 -1 ),直接返回 0
    if ( when < 0 ) return 0 ;
    // 假如 Redis 服务器正在从 RDB 文件中加载数据,暂时不进行失效主键的删除,直接返回 0
    if ( server . loading ) return 0 ;
    ...
    // 如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的主键
    // 还未失效就直接返回 0
    if ( mstime () <= when ) return 0 ;
    // 如果发现主键确实已经失效了,那么首先更新关于失效主键的统计个数,然后将该主键失
    // 效的信息进行广播,最后将该主键从数据库中删除
    server . stat_expiredkeys ++ ;
    propagateExpire ( db , key );
    return dbDelete ( db , key );
    }
    主动删除
    在 redis.conf 文件中可以配置主动删除策略 , 默认是 no-enviction (不删除)
    maxmemory-policy allkeys-lru

redis持久化

Redis 有两种持久化方式: RDB 和 AOF
RDB ( Redis DataBase ),是 redis 默认的存储方式, RDB 方式是通过 快照 ( snapshotting )完成的。
触发快照的方式

  1. 符合自定义配置的快照规则
  2. 执行 save 或者 bgsave 命令
  3. 执行 flushall 命令
  4. 执行主从复制操作 ( 第一次 )
    在 redis.conf 中配置: save 多少秒内 数据变了多少
    save "" # 不使用 RDB 存储 不能主从
    save 900 1 # 表示 15 分钟( 900 秒钟)内至少 1 个键被更改则进行快照。
    save 300 10 # 表示 5 分钟( 300 秒)内至少 10 个键被更改则进行快照。
    save 60 10000 # 表示 1 分钟内至少 10000 个键被更改则进行快照。
    命令显式触发
    在客户端输入 bgsave 命令。
  5. Redis 父进程首先判断:当前是否在执行 save ,或 bgsave/bgrewriteaof ( aof 文件重写命令)的子进程,如果在执行则bgsave 命令直接返回。
  6. 父进程执行 fork (调用 OS 函数复制主进程)操作创建子进程,这个复制过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令。
  7. 父进程 fork 后, bgsave 命令返回 "Background saving started" 信息并不再阻塞父进程,并可以响应其他命令。
  8. 子进程创建 RDB 文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。(RDB 始终完整)
  9. 子进程发送信号给父进程表示完成,父进程更新统计信息。
  10. 父进程 fork 子进程后,继续工作。
    RDB 的优缺点
    优点
    RDB 是二进制压缩文件,占用空间小,便于传输(传给 slaver )
    主进程 fork 子进程,可以最大化 Redis 性能,主进程不能太大, Redis 的数据量不能太大,复制过程中主进程阻塞
    缺点
    不保证数据完整性,会丢失最后一次快照以后更改的所有数据
    AOF ( append only file )是 Redis 的另一种持久化方式。 Redis 默认情况下是不开启的。开启 AOF 持久
    化后
    Redis 将所有对数据库进行过 写入的命令(及其参数) ( RESP )记录到 AOF 文件, 以此达到记录数据
    库状态的目的,
    这样当 Redis 重启后只要按顺序回放这些命令就会恢复到原始状态了。
    AOF 会记录过程, RDB 只管结果
    AOF 持久化实现
    配置 redis.conf

可以通过修改 redis.conf 配置文件中的 appendonly 参数开启

appendonly yes

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的。

dir ./

默认的文件名是 appendonly.aof ,可以通过 appendfilename 参数修改

appendfilename appendonly.aof
AOF 原理
AOF 文件中存储的是 redis 的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:
命令传播: Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。 缓存追
加: AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务
器的 AOF 缓存中。 文件写入和保存: AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保
存条件被满足的话, fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
命令传播
当一个 Redis 客户端需要执行命令时, 它通过网络连接, 将协议文本发送给 Redis 服务器。服务器在
接到客户端的请求之后, 它会根据协议文本的内容, 选择适当的命令函数, 并将各个参数从字符串文
本转换为 Redis 字符串对象( StringObject )。每当命令函数成功执行之后, 命令参数都会被传播到
AOF 程序。
缓存追加
当命令被传播到 AOF 程序之后, 程序会根据命令以及命令的参数, 将命令从字符串对象转换回原来的
协议文本。协议文本生成之后, 它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。
redisServer 结构维持着 Redis 服务器的状态, aof_buf 域则保存着所有等待写入到 AOF 文件的协
议文本( RESP )。
文件写入和保存
每当服务器常规任务函数被执行、 或者事件处理器被执行时, aof.c/flushAppendOnlyFile 函数都会被
调用, 这个函数执行以下两个工作:
WRITE :根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE :根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
AOF 保存模式
Redis 目前支持三种 AOF 保存模式,它们分别是:
AOF_FSYNC_NO :不保存。 AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐) 以下三个小节将分别讨论这三种保存模
式。
不保存
在这种模式下, 每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。
在这种模式下, SAVE 只会在以下任意一种情况中被执行:
Redis 被关闭 AOF 功能被关闭 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执
行) 这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。
每一秒钟保存一次(推荐)
在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程( fork )调用
的, 所以它不会引起服务器主进程阻塞。
每执行一个命令保存一次
在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。
另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令
请求。
AOF 保存模式对性能和安全性的影响
对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下:

AOF 重写、触发方式、混合持久化
AOF 记录数据的变化过程,越来越大,需要重写 " 瘦身 "
Redis 可以在 AOF 体积变得过大时,自动地在后台( Fork 子进程)对 AOF 进行重写。重写后的新 AOF 文
件包含了恢复当前数据集所需的最小命令集合。 所谓的 " 重写 " 其实是一个有歧义的词语, 实际上,
AOF 重写并不需要对原有的 AOF 文件进行任何写入和读取, 它针对的是数据库中键的当前值。
举例如下:
set s1 11
set s1 22 ------- > set s1 33
set s1 33
没有优化的:
set s1 11
set s1 22
set s1 33
优化后:
set s1 33
lpush list1 1 2 3
push list1 4 5 6 -------- > list1 1 2 3 4 5 6
优化后
lpush list1 1 2 3 4 5 6
Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到(后台)子进
程里执行, 这样处理的最大好处是:
1 、子进程进行 AOF 重写期间,主进程可以继续处理命令请求。 2 、子进程带有主进程的数据副本,使
用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。
不过, 使用子进程也有一个问题需要解决: 因为子进程在进行 AOF 重写期间, 主进程还需要继续处理
命令, 而新的命令可能对现有的数据进行修改, 这会让当前数据库的数据和重写后的 AOF 文件中的数
据不一致。
为了解决这个问题, Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用,
Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,
还会追加到这个缓存中。

重写过程分析(整个重写操作是绝对安全的):
Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生
停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕, Redis 就会从旧 AOF 文件切换到
新 AOF 文件,并开始对新 AOF 文件进行追加操作。
当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:
处理命令请求。 将写命令追加到现有的 AOF 文件中。 将写命令追加到 AOF 重写缓存中。 这样一来可以保证:
RDB 和 AOF 各有优缺点, Redis 4.0 开始支持 rdb 和 aof 的混合持久化。如果把混合持久化打开, aof
rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。
RDB 的头 +AOF 的身体 ---->appendonly.aof
开启混合持久化
aof-use-rdb-preamble yes

事务

Atomicity (原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行。 Redis:一个队列中的命令 执行或不执行
Consistency (一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。 Redis: 集群中不能保证时时的一致性,只能是最终一致性
Isolation (隔离性):事务之间不会相互影响。 Redis: 命令是顺序执行的,在一个事务中,有可能被执行其他客户端的命令的
Durability (持久性):事务执行成功后必须全部写入磁盘。Redis有持久化但不保证 数据的完整性
Redis 的事务是通过 multi 、 exec 、 discard 和 watch 这四个命令来完成的。
Redis 的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
Redis 不支持回滚操作
multi :用于标记事务块的开始 ,Redis 会将后续的命令逐个放入队列中,然后使用 exec 原子化地执行这个
命令队列
exec :执行命令队列
discard :清除命令队列
watch :监视 key
unwatch :清除监视 key

rua脚本

下载地址: http://www.lua.org/download.html
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
yum -y install readline-devel ncurses-devel
tar -zxvf lua-5.3.5.tar.gz

在 src 目录下

make linux
或 make install
如果报错,说找不到 readline/readline.h, 可以通过 yum 命令安装
yum -y install readline-devel ncurses-devel
安装完以后再
make linux / make install
最后,直接输入 lua 命令即可进入 lua 的控制台
EVAL script numkeys key [key ...] arg [arg ...]
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
redis.call() :
返回值就是 redis 命令执行的返回值
如果出错,则返回错误信息,不继续执行
redis.pcall()
返回值就是 redis 命令执行的返回值
如果出错,则记录错误信息,继续执行
注意事项
在脚本中,使用 return 语句将返回值返回给客户端,如果没有 return ,则返回 nil
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 n1 zhaoyun

慢查询

在 redis.conf 中可以配置和慢查询日志相关的选项:

执行时间超过多少微秒的命令请求会被记录到日志上 0 : 全记录 <0 不记录

slowlog-log-slower-than 10000
#slowlog-max-len 存储慢查询日志条数
slowlog-max-len 128
Redis 使用列表存储慢查询日志,采用队列方式( FIFO )
config set 的方式可以临时设置, redis 重启后就无效
config set slowlog-log-slower-than 微秒 config set slowlog-max-len 条数
查看日志: slowlog get [n]
查看日志数量的 slowlog len
清除日志 slowlog reset
慢查询定位 & 处理
使用 slowlog get 可以获得执行较慢的 redis 命令,针对该命令可以进行优化:
1 、尽量使用短的 key ,对于 value 有些也可精简,能使用 int 就 int 。
2 、避免使用 keys * 、 hgetall 等全量操作。
3 、减少大 key 的存取,打散为小 key 100K 以上
4 、将 rdb 改为 aof 模式
rdb fork 子进程 数据量过大 主进程阻塞 redis 性能大幅下降
关闭持久化 , (适合于数据量较小,有固定数据源)
5 、想要一次添加多条数据的时候可以使用管道
6 、尽可能地使用哈希存储
7 、尽量限制下 redis 使用的内存大小,这样可以避免 redis 使用 swap 分区或者出现 OOM 错误
内存与硬盘的 swap

监视器

Redis 客户端通过执行 MONITOR 命令可以将自己变为一个监视器,实时地接受并打印出服务器当前处理的命令请求的相关信息。
此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令请求的信息发送给所有监视器。

redis监控平台

grafana 、 prometheus 以及 redis_exporter 。
Grafana 是一个开箱即用的可视化工具,具有功能齐全的度量仪表盘和图形编辑器,有灵活丰富的图形化选项,可以混合多种风格,支持多个数据源特点。
Prometheus 是一个开源的服务监控系统,它通过 HTTP 协议从远程的机器收集数据并存储在本地的时序数据库上。
redis_exporter 为 Prometheus 提供了 redis 指标的导出,配合 Prometheus 以及 grafana 进行可视化及监控。

缓存问题

  1. 缓存穿透 指**查询一个"不存在的数据"时,由于缓存中没有该数据(缓存未命中),所有请求都会直接穿透到数据库**,导致数据库频繁处理无效查询,长期可能压垮数据库。 例如:查询一个不存在的用户ID(如`user:999999`,实际系统中无此用户),由于缓存中没有该key,每次请求都会直接访问数据库,而数据库返回空后,缓存也不会存储(默认逻辑),后续请求会重复穿透。

  2. 缓存击穿 指**一个"热点key"(被高频访问的key)突然失效(过期或被删除),此时大量并发请求同时访问该key**,由于缓存未命中,所有请求会瞬间冲向数据库,导致数据库短时间内承受巨大压力(类似"单点突破")。 例如:某热门商品的缓存key(`goods:10086`)设置了1小时过期,1小时后恰好有10万用户同时访问该商品,缓存失效后,10万请求同时查数据库,可能直接打崩DB。

  3. 缓存雪崩 指**大量缓存key在同一时间点集中失效,或缓存服务本身故障 (如Redis集群宕机)**,导致海量请求无法被缓存拦截,全部涌向数据库,最终数据库因过载而崩溃(类似"大面积垮塌")。 例如:批量设置缓存时,所有key的过期时间均为2小时,且部署时间相同,2小时后所有key同时失效,此时若有大量用户访问,数据库将瞬间被"淹没"。 ### 解决方法 ####

  4. 缓存穿透的解决 核心思路:**拦截无效请求,避免其到达数据库**。 - **布隆过滤器预处理**: 提前将所有可能存在的有效key(如用户ID、商品ID)存入布隆过滤器(一种高效的概率性数据结构)。请求到达时,先通过布隆过滤器判断key是否"可能存在":若不存在,直接返回空(无需查缓存和DB);若可能存在,再走正常的"缓存→DB"流程。 *注:布隆过滤器有极小的误判率("不存在"被判断为"可能存在"),但可忽略,不影响整体逻辑。* - **缓存空值**: 当DB查询结果为空时(即key确实不存在),在缓存中存储一个"空值"(如`""`或`null`),并设置较短的过期时间(如5分钟)。后续相同请求会直接命中缓存的空值,避免重复访问DB。 - **接口层限流与校验**: 对高频无效请求(如恶意刷不存在的key)进行限流(如限制每秒请求数),或通过业务校验(如ID格式校验)直接拦截无效请求。 ####

  5. 缓存击穿的解决 核心思路:**避免热点key失效时的并发请求冲击DB**。 - **热点key永不过期 **: 对核心热点key(如首页商品、热门活动)不设置过期时间,通过后台异步任务定期更新缓存,避免"失效"场景。 - **互斥锁(分布式锁) **: 当缓存失效时,只有一个线程能获取锁并访问DB,其他线程等待(如自旋或休眠),直到该线程更新缓存后,其他线程再从缓存获取数据。 *示例:用Redis的`setnx`实现分布式锁,获取锁的线程查DB→更新缓存→释放锁,其他线程等待锁释放后直接读缓存。* - **热点数据预热**: 提前将热点key加载到缓存(如系统启动时或流量高峰前),并设置合理的过期时间(避免集中失效),确保缓存始终有值。 ####

  6. 缓存雪崩的解决 核心思路:**避免大量key集中失效,提升缓存服务可用性**。 - **过期时间"错开" **: 对缓存key的过期时间添加随机值(如`基础过期时间 + 随机数(0~300秒)`),避免大量key在同一时间点失效。 *示例:原计划过期时间1小时,实际设置为`3600 + random(0, 300)`秒,分散失效时间。* - **多级缓存 **: 引入"本地缓存"(如Java的Caffeine、Guava)作为Redis的补充,形成"本地缓存→Redis→DB"的多级缓存架构。当Redis中的key失效时,本地缓存可临时拦截部分请求,减少DB压力。 - **缓存服务高可用 **: 部署Redis集群(如主从+哨兵、Redis Cluster),避免单点故障。即使部分节点宕机,集群仍能提供服务,减少"缓存整体失效"的风险。 - **降级与限流 **: 当缓存服务故障(如Redis集群不可用)时,通过熔断降级机制(如Hystrix)返回兜底数据(如静态页面、默认值),或限制流向DB的请求量(如每秒最多1000次),避免DB被冲垮。 - **监控与报警**: 实时监控缓存命中率、DB压力、Redis节点状态,当指标异常(如命中率骤降、DB连接数暴涨)时及时报警,人工介入处理。

总结 :缓存穿透:针对"不存在的key",用布隆过滤器或缓存空值拦截。 - 缓存击穿:针对"热点key失效",用互斥锁或永不过期避免并发冲击。 - 缓存雪崩:针对"大量key集中失效或缓存故障",用时间错开、多级缓存、高可用架构化解风险。 三者均需结合业务场景选择合适的方案,核心目标是"保护数据库,确保系统稳定性"。

数据不一致问题

数据并发竞争

如何处理热 Key :
1 、变分布式缓存为本地缓存
发现热 key 后,把缓存数据取出后,直接加载到本地缓存中。可以采用 Ehcache 、 Guava Cache 都可
以,这样系统在访问热 key 数据时就可以直接访问自己的缓存了。(数据不要求时时一致)
2 、在每个 Redis 主节点上备份热 key 数据,这样在读取时可以采用随机读取的方式,将访问压力负载到
每个 Redis 上。
3 、利用对热点数据访问的限流熔断保护措施
每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群,
直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。(首页不行,系统友好性差)
通过系统层自己直接加限流熔断保护措施,可以很好的保护后面的缓存集群。
如何发现大 key :
1 、 redis-cli --bigkeys 命令。可以找到某个实例 5 种数据类型 (String 、 hash 、 list 、 set 、 zset) 的最大key。但如果Redis 的 key 比较多,执行该命令会比较慢
2 、获取生产 Redis 的 rdb 文件,通过 rdbtools 分析 rdb 生成 csv 文件,再导入 MySQL 或其他数据库中进行分析统计,根据size_in_bytes 统计 bigkey
大 key 的处理:
优化 big key 的原则就是 string 减少字符串长度, list 、 hash 、 set 、 zset 等减少成员数。
1 、 string 类型的 big key ,尽量不要存入 Redis 中,可以使用文档型数据库 MongoDB 或缓存到 CDN 上。如果必须用Redis 存储,最好单独存储,不要和其他的 key 一起存储。采用一主一从或多从。
2 、单个简单的 key 存储的 value 很大,可以尝试将对象分拆成几个 key-value , 使用 mget 获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis 的 IO 影响。
2 、 hash , set , zset , list 中存储过多的元素,可以将这些元素分拆。(常见)
3 、删除大 key 时不要使用 del, 因为 del 是阻塞命令,删除时会影响性能。
4 、使用 lazy delete (unlink 命令 )
删除指定的 key(s), 若 key 不存在则该 key 被跳过。但是,相比 DEL 会产生阻塞,该命令会在另一个线程中
回收内存,因此它是非阻塞的。 这也是该命令名字的由来:仅将 keys 从 key 空间中删除,真正的数据删
除会在后续异步操作。

分布式锁

乐观锁基于 CAS ( Compare And Swap )思想(比较并替换),是不具有互斥性,不会产生锁等待而消
耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用 redis 来
实现乐观锁。具体思路如下:
1 、利用 redis 的 watch 功能,监控这个 redisKey 的状态值 2 、获取 redisKey 的值 3 、创建 redis 事务 4 、
给这个 key 的值 +1 5 、然后去执行这个事务,如果 key 的值被修改过则回滚, key 不加 1

Kafka

架构模型:

性能表现:

功能特性:

典型应用场景

  • Kafka优选场景

    ✅ 日志收集与分析(如ELK)

    ✅ 实时流处理(配合Flink/Spark)

    ✅ 大数据管道(高吞吐量需求)

  • RabbitMQ优选场景

    ✅ 金融交易(需强可靠性)

    ✅ 复杂路由(如电商订单状态流转)

    ✅ 企业级异步任务(低延迟需求)

RabbitMQ集群的扩展性瓶颈

RabbitMQ集群通过镜像队列(Mirror Queue)实现高可用性,但这不是为性能扩展设计的‌3:

  • 数据全量复制‌:每个队列(Queue)的元数据和消息都会在集群所有节点上同步存储,新节点加入时必须复制所有数据,导致网络带宽和存储资源消耗剧增,无法线性提升吞吐量‌10。例如,在普通集群或镜像集群模式下,队列数据无法分布式存储,单队列容量受限于单个节点承载能力。
  • 读写压力集中‌:镜像队列仅作为备份,实际读写操作仍由主队列(Master Queue)所在节点承担,客户端请求无法被其他节点分担,扩展节点后性能提升不明显‌3。这种设计在消息堆积场景下性能显著下降,而Kafka通过分区机制分散压力,支持水平扩展。
  • 资源开销过大‌:消息同步需遍历所有节点,高并发时网络延迟和CPU负载急剧上升,限制了集群规模。例如,RabbitMQ单机并发仅支持万级QPS,而Kafka可达百万级‌1。

<!-- RabbitMQ集群架构细节 -->

Kafka的扩展性优势

Kafka采用分布式日志模型,天然支持高性能扩展:

  • 分区机制‌:数据按主题(Topic)划分为多个分区(Partition),每个分区独立存储在不同节点上,新节点加入时仅需分配部分分区,资源消耗低且能线性提升吞吐量‌1。
  • 无状态消费者‌:消费者直接读取分区数据,不与特定节点绑定,扩展消费者数量即可提升并发处理能力,避免了RabbitMQ的读写瓶颈‌5。

关键对比总结

维度 RabbitMQ Kafka
扩展性目标 高可用性优先(如故障转移) 高性能和高并发优先
数据分布 全节点复制,无法分布式存储 分区分布式存储,支持数据分片
性能影响 节点增加时资源开销大,吞吐量提升有限 节点增加可线性提升吞吐量
适用场景 强可靠性需求(如金融交易) 高吞吐量需求(如日志流处理)‌14

总之,RabbitMQ集群虽保障了高可用性,但因其数据复制机制和集中式读写设计,扩展性弱于Kafka的分区分布式架构‌13。实际选型需权衡:若需强可靠性,RabbitMQ更合适;若需处理海量数据或频繁扩展,Kafka优势显著。

常见问题

重复消费:通过幂等性设计方案;

消息堆积:扩容消费者实例,提升批量拉取数据大小,优化消费逻辑(异步/并行处理);

数据丢失:生产者设置ack=all和重试机制为最大最,每个broker的副本最小同步副本数 要>=2,消费者关闭自动提交;

消息可靠性保障:

Kafka消息可靠性保障方案需从生产端、Broker端、消费端协同实现,核心机制如下:

一、生产端保障

  1. 同步发送或异步回调
    默认异步发送可能丢失消息,可改为同步发送实时获取发送结果,或添加异步回调监听结果并重试‌1。
  2. 重试机制
    配置retries参数(如网络故障时自动重试)确保消息送达‌12。
  3. ACK确认机制
    设置acks=all:要求所有副本(ISR列表)持久化消息后才返回成功,避免单点故障丢失数据‌24。

二、Broker端保障

  1. 多副本机制(Replica)
    每个Partition配置多个副本,Leader故障时Follower自动选举为新Leader,保障高可用‌24。
  2. 持久化优化
    • 顺序读写磁盘‌:避免随机I/O性能瓶颈,提升吞吐量‌6。
    • 页缓存+零拷贝‌:减少内核态数据拷贝,加速消息读写‌6。

三、消费端保障

  1. 手动提交位移(offset)
    关闭自动提交(enable.auto.commit=false),业务处理成功后手动提交offset,避免消费丢失‌2。
  2. 幂等消费设计
    业务逻辑需支持重复消息处理(如唯一ID去重),应对可能的重复消费‌3。

关键参数配置示例

参数 推荐值 作用
生产端 acks all 所有副本持久化后确认
生产端 retries >0 发送失败自动重试
消费端 auto.commit false 关闭自动提交位移
Broker端 min.insync.replicas >=2 最小同步副本数

故障场景应对‌:

  • 网络异常‌:生产端重试机制‌1。
  • Broker宕机 ‌:副本自动切换+acks=all保障‌4。
  • 消费崩溃‌:手动提交offset避免消息遗漏‌2。

此方案通过多级冗余设计,实现消息从发送到消费的端到端可靠性‌12。实际部署需结合业务对吞吐量与可靠性的需求调整参数(如acks=1可提升吞吐但降低可靠性)‌

页存储和零拷贝原理:

Kafka的存储机制和零拷贝原理是其高性能的核心设计,分别保障数据持久化和高效传输。以下从存储原理和零拷贝技术两方面解析,并结合实际工作机制。

一、Kafka数据存储原理

Kafka采用分区(Partition)和分段(Segment)的文件存储结构,确保高吞吐和可扩展性:

  • Topic与Partition逻辑结构 ‌:Topic是逻辑消息队列,被划分为多个Partition分布在Broker节点上,每个Partition对应一个物理目录(如mytopic-0mytopic-1),支持水平扩展和数据并行处理‌4。
  • Segment文件与Index索引 ‌:每个Partition由多个Segment文件组成(后缀为.log存储数据,.index存储索引)。Segment文件按消息偏移量(offset)命名(如0000000000.log),索引文件映射offset到数据文件的物理位置,实现快速查找‌45。例如,消费时通过offset定位Segment,再结合索引定位数据条目,避免全文件扫描。
  • 副本机制保障可靠性 ‌:每个Partition配置多个副本(Replica),Leader处理读写请求,Follower异步同步数据;创建Topic时指定replication-factor(如3),确保副本分散在不同Broker,Leader故障时自动选举新Leader,防止数据丢失‌4。

二、零拷贝(Zero-Copy)原理

零拷贝通过减少数据在内核态和用户态间的冗余拷贝及上下文切换,显著提升I/O性能。在Kafka中,它优化了从磁盘到网络的传输过程:

  • 传统文件传输问题‌:传统方式需4次拷贝(磁盘→内核缓冲区→用户缓冲区→socket缓冲区→网卡)和2次上下文切换,浪费CPU资源‌1。
  • 零拷贝工作机制 ‌:利用Linux的sendfile()系统调用和DMA(直接内存访问)技术:
    1. DMA将磁盘数据直接复制到内核缓冲区(Page Cache),无需CPU干预。
    2. 文件描述符(如内存地址和偏移量)被加载到socket缓冲区。
    3. DMA将内核缓冲区的数据直接复制到网卡,仅需2次DMA拷贝(磁盘→内核→网卡),避免用户态参与‌。
      此过程减少CPU拷贝次数和上下文切换,吞吐量提升可达30%以上。
  • 好看视频-轻松有收获

三、存储与零拷贝的协同应用

在Kafka中,存储机制与零拷贝紧密结合:生产者写入数据时,Broker顺序追加到Segment文件(利用磁盘顺序写优势);消费者读取时,Broker通过零拷贝直接从Page Cache发送数据到网卡,跳过用户态拷贝。例如,消费端请求指定offset的消息,Broker先定位Segment文件,再通过零拷贝技术传输,实现高吞吐低延迟‌14。

此设计使Kafka适应高并发场景,但需合理配置参数(如Segment大小、副本数)以平衡性能和可靠性。

分区策略:

Kafka的分区策略直接影响消息的负载均衡和消费效率,主要包含生产者分区分配、消费者分区分配及Rebalance机制三部分,关键策略如下:


一、生产者分区策略(消息写入路由)

  1. 默认策略

    • Key哈希分区 ‌:若指定消息Key,则对Key哈希取模(hash(key) % partition_count),确保相同Key的消息写入同一分区‌4。
    • 轮询分区‌:未指定Key时,按分区顺序轮询写入,实现负载均衡‌4。
  2. 自定义策略

    开发者可通过实现Partitioner接口定制路由逻辑(如按业务ID分区),需配置partitioner.class参数‌4。


二、消费者分区策略(消息消费分配)

消费者组通过分区策略实现分区的负载均衡,核心策略包括:

  1. ‌**Range(范围分配)**‌

    • 规则‌:按Topic划分连续分区范围分配给消费者(如分区0-3给Consumer1,4-6给Consumer2)。
    • 问题‌:分区数无法整除消费者数时,首个消费者可能负载更高(例如7个分区3个消费者时,Consumer1分配3个分区,其余分配2个)‌36。
  2. ‌**RoundRobin(轮询分配)**‌

    • 规则‌:所有Topic的分区全局轮询分配给消费者,实现绝对均衡(如分区P0→C1、P1→C2、P2→C1)。
    • 限制‌:要求消费者组订阅相同的Topic列表‌36。
  3. ‌**Sticky(粘性分配)**‌

    • 规则‌:首次分配均衡,Rebalance时尽量保留原有分配关系,减少分区迁移开销‌36。
    • 优势‌:避免因消费者变动导致的大规模数据重新负载。

00:20 / 05:08


三、Rebalance触发条件与机制

当以下情况发生时,消费者组触发Rebalance重新分配分区:

  1. 组成员变更‌:新消费者加入或旧消费者离线‌6。
  2. 订阅变更‌:消费者组订阅的Topic或分区数量变化‌6。
  3. 协调者(Coordinator)故障‌:负责管理消费者组的Broker节点异常‌6。

执行流程‌:

  1. 所有消费者向协调者注册并提交分区策略;
  2. 协调者选举Leader消费者,汇总分配方案;
  3. 将最终方案同步至所有消费者‌6。

四、配置建议与注意事项

策略 适用场景 缺陷
Range Topic数量少且分区均匀 易导致消费者负载不均
RoundRobin 高吞吐场景,需严格均衡 要求消费者订阅相同Topic列表
Sticky 频繁消费者变动的动态环境 Kafka 0.11+版本支持

实践建议‌:

  • 优先使用Sticky策略以减少Rebalance开销;
  • 避免单Topic分区数过少导致分配倾斜;
  • 生产端按业务需求选择Key路由或轮询‌

感谢阅读!!!

相关推荐
jakeswang19 分钟前
应用缓存不止是Redis!——亿级流量系统架构设计系列
redis·分布式·后端·缓存
秋难降1 小时前
零基础学SQL(八)——事务
数据库·sql·mysql
Starry_hello world1 小时前
MySql 表的约束
数据库·笔记·mysql·有问必答
RestCloud1 小时前
ETLCloud中的数据转化规则是什么意思?怎么执行
数据库·数据仓库·etl
Aspirin_Slash1 小时前
docker-compose部署kafka with kraft 配置内网公网同时访问
kafka
一个天蝎座 白勺 程序猿2 小时前
Apache IoTDB(4):深度解析时序数据库 IoTDB 在Kubernetes 集群中的部署与实践指南
数据库·深度学习·kubernetes·apache·时序数据库·iotdb
.Shu.2 小时前
Redis zset 渐进式rehash 实现原理、触发条件、执行流程以及数据一致性保障机制【分步源码解析】
数据库·redis·缓存
悟能不能悟2 小时前
排查Redis数据倾斜引发的性能瓶颈
java·数据库·redis
切糕师学AI2 小时前
.net core web程序如何设置redis预热?
redis·.netcore