java基础(十二)redis 日志机制以及常见问题

1 Redis 持久化机制

1.1 持久化概述

Redis 作为基于内存的数据库,其所有读写操作均在内存中进行,因此具备极高的性能。然而,内存中的数据在 Redis 重启后会丢失。为了确保数据的持久性,Redis 实现了数据持久化机制,将数据保存到磁盘中,以便在重启后能够恢复数据。Redis 主要支持两种持久化方式:AOF(Append Only File)日志RDB(Redis Database)快照

1.2 AOF 日志持久化

1.2.1 实现原理

AOF 日志通过记录每一条写操作命令来实现持久化。每当执行一条写命令时,Redis 会将该命令以追加的方式写入 AOF 文件。当 Redis 重启时,通过重新执行 AOF 文件中的命令来恢复数据。

例如,执行命令 set name xiaolin 后,AOF 文件中会记录如下内容:

bash 复制代码
*3
$3
set
$4
name
$7
xiaolin
1.2.2 写回策略

Redis 提供了三种 AOF 日志写回硬盘的策略,可通过 appendfsync 配置项设置:

  • Always:每次写命令执行完成后,立即同步将 AOF 日志数据写回硬盘。这种策略能保证最好的数据安全性,但性能开销最大。

  • Everysec:每次写命令执行完后,先将命令写入内核缓冲区,然后每隔一秒将缓冲区内容写回硬盘。在性能和数据安全之间取得平衡,是默认推荐策略。

  • No:写命令执行完后,只写入内核缓冲区,由操作系统决定何时写回硬盘。性能最好,但数据安全性最低。

1.2.3 优缺点分析

优点

  • 数据安全性高,默认每接收到一个写命令就会追加到文件末尾,即使服务器宕机,也只会丢失最后一次写入前的数据。

  • 支持多种同步策略,可根据需要在数据安全性和性能之间进行权衡。

  • 提供 AOF 重写机制,优化文件体积,加快恢复速度。

  • 提供 redis-check-aof 工具修复损坏的 AOF 文件。

缺点

  • AOF 文件通常比 RDB 文件更大,消耗更多磁盘空间。

  • 频繁的磁盘 IO 操作(尤其是 always 策略)可能对写入性能造成影响。

  • 恢复过程可能需要重放所有操作命令,速度较慢。

1.3 RDB 快照持久化

1.3.1 实现原理

RDB 快照通过某一时刻的内存数据生成二进制快照文件来实现持久化。RDB 文件记录的是实际数据,而非操作命令,因此恢复效率较高。

Redis 提供了两个命令生成 RDB 文件:

  • save:在主线程执行,会阻塞主线程,适用于数据量小或可接受短暂阻塞的场景。

  • bgsave:创建子进程执行,避免阻塞主线程,是推荐的生产环境用法。

1.3.2 优缺点分析

优点

  • 文件体积小,备份和恢复速度快。

  • 通过子进程操作,对 Redis 服务性能影响小。

  • 适合大规模数据备份和灾难恢复。

缺点

  • 两次快照之间的数据可能丢失,可靠性不如 AOF。

  • 快照生成期间的大量写操作可能导致恢复数据不一致。

1.4 持久化策略选择建议

  • 如果数据安全性要求极高,可同时开启 AOF 和 RDB,但需要注意性能开销。

  • 如果可容忍分钟级数据丢失,可仅使用 RDB。

  • 通常建议使用 AOF 的 everysec 策略,兼顾性能和安全。

2 Redis 内存管理

2.1 过期键删除策略

Redis 采用惰性删除定期删除两种策略结合的方式管理过期键。

2.1.1 惰性删除

在访问或修改 key 前检查是否过期,若过期则删除。这种方式节省 CPU 时间,但可能导致大量过期 key 堆积。

java 复制代码
// 示例:Java 中模拟惰性删除逻辑
public Object get(String key) {
    Object value = store.get(key);
    if (value != null && isExpired(key)) {
        store.remove(key);
        return null;
    }
    return value;
}
2.1.2 定期删除

Redis 默认每秒进行 10 次过期检查(可通过 hz 配置),每次随机抽取 20 个 key 检查并删除过期 key。如果本轮删除的过期 key 数量超过 5 个(25%),则继续重复抽查过程,直到已过期 key 比例低于 25% 或超过 25ms 时间限制。

2.2 内存淘汰策略

当内存使用达到上限时,Redis 会根据配置的内存淘汰策略删除键值对。Redis 3.0 以后默认策略为 noeviction

2.2.1 淘汰策略分类
  • 不淘汰数据noeviction(默认策略),拒绝所有写入请求,只允许读和删除操作。

  • 在设置了过期时间的数据中淘汰

    • volatile-random:随机淘汰

    • volatile-ttl:优先淘汰更早过期的

    • volatile-lru:淘汰最久未使用的

    • volatile-lfu:淘汰最少使用的(Redis 4.0+)

  • 在所有数据范围内淘汰

    • allkeys-random:随机淘汰

    • allkeys-lru:淘汰最久未使用的

    • allkeys-lfu:淘汰最少使用的(Redis 4.0+)

2.2.2 策略选择建议
  • 如果数据重要性不同,建议为关键数据设置过期时间并使用 volatile-xxx 策略。

  • 如果所有数据重要性相当,可使用 allkeys-lruallkeys-lfu

  • 如果数据访问模式相对随机,可使用 allkeys-random

3 Redis 集群与高可用

3.1 主从复制

3.1.1 全量同步

当从服务器首次连接主服务器或数据差异过大时,会触发全量同步:

  1. 从服务器发送 SYNC 命令

  2. 主服务器生成 RDB 快照并发送给从服务器

  3. 从服务器加载 RDB 文件

  4. 主服务器将期间的写命令发送给从服务器

3.1.2 增量同步

基于 PSYNC 命令实现,使用运行 ID 和复制偏移量机制:

  1. 从服务器发送 psync 命令携带偏移量

  2. 主服务器判断偏移量是否在复制积压缓冲区中

  3. 如果在,发送增量数据;否则触发全量同步

优化建议 :适当增大 repl_backlog_size 可减少全量同步概率。

3.2 哨兵机制

哨兵(Sentinel)用于实现主从节点故障转移,主要功能包括监控、选主和通知。

3.2.1 故障转移流程
  1. 主观下线:单个 Sentinel 节点检测到主节点无响应

  2. 客观下线:多个 Sentinel 节点确认主节点故障

  3. 选举 Leader:Sentinel 集群选举 Leader 负责故障转移

  4. 选择新主节点:基于优先级、复制偏移量和 runid 选择新主节点

3.2.2 选举算法

Sentinel Leader 选举基于 Raft 算法,需要获得多数票(quorum 和 Sentinel 节点数/2+1 的最大值)。

3.3 Redis Cluster

Redis 集群采用分片技术,通过哈希槽(16384 个槽)将数据分布到不同节点。

3.3.1 数据分片

键值对通过 CRC16 算法计算哈希值,然后对 16384 取模确定所属哈希槽。哈希槽可以手动或自动分配到集群节点。

3.3.2 优缺点分析

优点

  • 高可用性:主从复制保证故障转移

  • 高性能:数据分片提高读写性能

  • 扩展性好:可动态增加或减少节点

缺点

  • 部署和维护复杂

  • 集群同步可能存在延迟

  • 某些操作不支持跨节点执行

4 Redis 应用场景

4.1 为什么使用 Redis

Redis 因其高性能高并发特性被广泛应用:

  • 高性能:基于内存操作,速度极快

  • 高并发:单机 QPS 可达 10w+,远高于 MySQL

4.2 常见应用场景

4.2.1 缓存

最常见的用途,将热点数据存储在内存中,减轻数据库压力。

4.2.2 排行榜

使用有序集合实现实时排行榜系统。

4.2.3 分布式锁

通过 SETNX 等命令实现分布式环境下的互斥访问。

java 复制代码
// 示例:基于 Redis 的分布式锁实现
public boolean tryLock(String key, String value, long expireTime) {
    String result = jedis.set(key, value, "NX", "PX", expireTime);
    return "OK".equals(result);
}
​
public boolean unlock(String key, String value) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                   "return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(key), 
                              Collections.singletonList(value));
    return result.equals(1L);
}
4.2.4 计数器

利用原子操作实现高性能计数器功能。

4.2.5 消息队列

使用 List 结构或 Pub/Sub 模式实现轻量级消息队列。

4.3 本地缓存 vs Redis 缓存

4.3.1 本地缓存
  • 优点:访问速度快、低延迟、减轻网络压力

  • 缺点:可扩展性有限、数据一致性难保证

4.3.2 Redis 缓存(分布式缓存)
  • 优点:可扩展性强、数据一致性高、易于维护

  • 缺点:访问速度相对慢、网络开销大

选择建议:根据数据大小、网络状况和业务特点权衡选择。

5 Redis 实践问题与解决方案

5.1 缓存一致性

5.1.1 读写策略

采用旁路缓存策略

  • 读数据:先读缓存,未命中则读数据库并写入缓存

  • 写数据:先更新数据库,再删除缓存

5.1.2 保证最终一致性
  • 删除缓存重试机制(通过消息队列)

  • 订阅 MySQL binlog(通过 Canal 中间件)

5.2 缓存异常场景

5.2.1 缓存雪崩

问题:大量缓存同时失效或 Redis 故障,导致所有请求直接访问数据库。

解决方案

  • 均匀设置过期时间(加随机数)

  • 互斥锁更新缓存

  • 后台更新缓存策略

5.2.2 缓存击穿

问题:热点数据过期,大量请求直接访问数据库。

解决方案

  • 互斥锁更新

  • 热点数据永不过期,后台异步更新

5.2.3 缓存穿透

问题:请求不存在的数据,绕过缓存直接访问数据库。

解决方案

  • 接口层校验请求参数

  • 缓存空值或默认值

  • 使用布隆过滤器

5.3 大 Key 与热 Key 问题

5.3.1 大 Key 问题

定义:通常指 value 大小 > 1MB 或集合元素数量 > 1w。

影响:内存占用高、性能下降、阻塞操作、网络拥塞等。

解决方案

  • 拆分大 Key

  • 清理不必要数据

  • 监控内存水位

  • 定期清理过期数据

5.3.2 热 Key 问题

定义:访问频率异常高的 Key。

解决方案

  • 复制热 Key 到多个分片

  • 使用读写分离架构

  • 本地缓存热 Key

5.4 秒杀场景设计

5.4.1 数据库层面解决
  • 加排他锁:select * from goods where goods_id = ? for update

  • 库存限制:update goods set stock = stock - 1 where goods_id = ? and stock > 0

5.4.2 分布式锁方案

同一时间只允许一个客户端处理同一商品的秒杀请求。

5.4.3 分布式锁+分段缓存

将库存分段存储,提高并发处理能力。

5.4.4 Redis 原子操作+异步队列
  1. 预减库存:DECR 原子操作

  2. 请求入队:异步处理秒杀请求

  3. 出队处理:生成订单,减少数据库库存

  4. 客户端轮询:检查秒杀结果

6 总结

Redis 作为高性能的内存数据库,在现代应用架构中扮演着重要角色。通过合理使用 Redis 的持久化机制、内存管理策略和集群方案,可以构建出高可用、高性能的应用系统。同时,需要注意缓存一致性、异常场景处理以及大 Key 热 Key 等实践问题,确保系统的稳定性和可靠性。

在实际应用中,应根据具体业务场景选择合适的技术方案,并结合监控和运维手段,持续优化系统性能。