NoSQL数据库解析:Redis


文章目录

  • 一、Reids简介
  • 二、安装Redis
    • [2.1 默认启动](#2.1 默认启动)
    • [2.2 指定配置启动](#2.2 指定配置启动)
    • [2.3 设置开机自启动](#2.3 设置开机自启动)
  • 三、Redis界面客户端
  • [四、Redis 数据结构](#四、Redis 数据结构)
    • [4.1 通用命令](#4.1 通用命令)
    • [4.2 String类型](#4.2 String类型)
    • [4.3 哈希(Hash)](#4.3 哈希(Hash))
    • [4.4 列表(List)](#4.4 列表(List))
    • [4.5 集合(Set)](#4.5 集合(Set))
    • [4.6 有序集合(sorted set)](#4.6 有序集合(sorted set))
  • 五、Redis使用
    • [5.1 Java 使用 Redis](#5.1 Java 使用 Redis)
    • [5.2 python 操作Redis](#5.2 python 操作Redis)
  • [六、 Redis 持久化](#六、 Redis 持久化)
    • [6.1 RDB 持久化(快照)](#6.1 RDB 持久化(快照))
    • [6.2 AOF 持久化(追加文件)](#6.2 AOF 持久化(追加文件))
  • 七、应用问题解决
    • [7.1 缓存穿透](#7.1 缓存穿透)
    • [7.2 缓存击穿](#7.2 缓存击穿)
    • [7.3 缓存雪崩](#7.3 缓存雪崩)

一、Reids简介

NoSQL 也称为"not only SQL"或"non-SQL",它是一种数据库设计方法,可以在关系数据库中的传统结构之外存储和查询数据,NoSQL 数据库并非采用关系数据库的典型表结构,而是将数据存储在一个数据结构中,例如 JSON 文档。由于这种非关系数据库设计不需要使用架构,因此,它提供快速可扩展性以管理通常为非结构化的大型数据集。

简单来说,NoSQL数据库通过放弃关系型数据库的某些严格限制(如固定的表结构、强一致性、Join操作),换取了极高的扩展性、灵活性和特定场景下的高性能。

灵活的数据模型 :可以存储没有固定结构的数据,非常适合处理经常变化的非结构化或半结构化数据。
高可扩展性 :多数NoSQL数据库天生支持分布式架构,通过增加更多的普通服务器(节点)来分担负载,而不是仅仅依赖提升单台服务器的硬件性能(垂直扩展)。
最终一致性 :为了在分布式环境中实现高性能和高可用性,不提供传统关系型数据库的强ACID事务保证,而是采用最终一致性模型。

不同NoSQL比对:

在NoSQL家族的众多成员中,Redis像一个身法奇快的"全能选手"。它把所有数据都放在内存里操作,因此速度极快。但它又不止于快------它还能存储列表、集合等多种结构,既能当缓存使,又能做消息管道,是现代高并发应用不可或缺的加速器。


二、安装Redis

官网:https://redis.io/

下载 : https://download.redis.io/releases/

https://github.com/redis-windows/redis-windows/releases

Redis官方推荐在生产环境中使用Linux系统进行部署,以获得最佳性能和稳定性。不过,在开发和测试环境中,它也可以运行在macOS和Windows(通过WSL2)上。以下是几种主流的安装方式.

python 复制代码
# Redis的编译依赖于gcc和make等基础开发工具。
# Ubuntu/Debian
sudo apt update
sudo apt install -y build-essential tcl
# CentOS/RHEL
sudo yum groupinstall "Development Tools"
sudo yum install -y tcl
# 下载解压
tar xzf redis-6.2.21.tar.gz
ln -s redis-6.2.21 redis
# 编译与安装
cd redis
make&&make install
# 默认安装路径 /usr/local/bin/

验证安装

2.1 默认启动

python 复制代码
# 默认启动  默认安装路径 /usr/local/bin/
redis-server

使用客户端连接测试

python 复制代码
#自带的命令行工具redis-cli连接本机Redis服务,
#并发送PING命令。如果返回PONG,则表示安装成功且服务运行正常。
redis-cli ping
# 互式命令行进行操作
redis-cli
SET name "Redis"
GET name

2.2 指定配置启动

安装文件中的配置文件名redis.conf

python 复制代码
cp redis.conf redis.conf.bak
vim redis.conf
-----------------------------------------------------------------------------------------
# 指定 Redis 监听的 IP 地址。默认是 127.0.0.1,意味着 Redis 仅接受本地连接。如果你想要远程访问,可以设置为 0.0.0.0
bind 127.0.0.1
# Redis 服务的端口号,默认是 6379
port 6379
# 是否以守护(后台)进程的方式启动,默认no
daemonize no
# 传统认证方式:所有连接使用同一个密码
requirepass yourpassword123
# redis默认有16个数据库,编号从0开始。
databases 16
# 指定Redis最大内存限制。达到内存限制时,Redis将尝试删除已到期或即将到期的Key。
maxmemory <bytes> # 512mb
---------------------------------------------------------------------
# 用 * 号获取所有配置项
CONFIG GET *

# 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice
loglevel notice
----------
# 启动
redis-server redis.conf
python 复制代码
redis-cli -a 你的密码 ping
# 1. 连接(不输入密码)
redis-cli
# 2. 进入后认证
127.0.0.1:6379> AUTH 你的密码

2.3 设置开机自启动

python 复制代码
1.#创建服务单元文件
vim /etc/systemd/system/redis.service
-----------------------------------------------------------------------------------------------
[Unit]
Description=Redis In-Memory Data Store
After=network.target
Wants=network.target

[Service]
Type=forking
# 如果redis.conf中daemonize yes,使用forking类型
# aemonize yes,则使用Type=forking;如果设置为daemonize no,则使用Type=simple
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf
ExecStop=/usr/local/bin/redis-cli shutdown
Restart=always
# 注意添加用户和组或改为root
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
# 路径为配置文件中路径需保持一致。 cat redis.conf | grep -E "^(pidfile|dir|dbfilename)" 注意目录文件权限改redis
PIDFile=/var/run/redis/redis_6379.pid
[Install]
WantedBy=multi-user.target
---------------------------------------------------------------------------------------------
python 复制代码
# 重新加载systemd配置
sudo systemctl daemon-reload
# 启用开机自启动
sudo systemctl enable redis
# 立即启动服务
sudo systemctl start redis
# 检查状态
sudo systemctl status redis
python 复制代码
# 创建 redis 用户和组
sudo groupadd redis
sudo useradd -r -g redis -s /sbin/nologin redis

# 设置目录权限
sudo mkdir -p /var/run/redis
sudo chown redis:redis /var/run/redis
sudo chown -R redis:redis /usr/app/redis
chasudo chown -R redis:redis /var/log/redis 2>/dev/null || sudo mkdir -p /var/log/redis && sudo chown redis:redis /var/log/redis

# 重新启动服务
sudo systemctl daemon-reload
sudo systemctl start redis
sudo systemctl status redis

三、Redis界面客户端

Redis Desktop Manager (RDM)

地址:https://github.com/lework/RedisDesktopManager-Windows

四、Redis 数据结构

Redis经常被称为"数据结构服务器",因为它支持丰富的数据类型


Redis 命令参考:http://doc.redisfans.com/

4.1 通用命令

python 复制代码
# 添加k-v
set k1 v1
# 删Key
del k3
#给一个Key设置有效期
expire k1 60
# 查看Key的剩余有效期
ttl k1
# 查看当前 timeout 值
config get timeout
# 查看一个命令的具体用法
help del

4.2 String类型

python 复制代码
# 同时设置一个或多个 key-value 对。
127.0.0.1:6379>mset k1 1 k2 "2" k3 x
ok
127.0.0.1:6379>mget k1 k2 k3
1) "1"
2) "2"
3) "x"
# INCR key 将 key 中储存的数字值增一。
127.0.0.1:6379> incr k1
(integer) 2
127.0.0.1:6379> incr k2
(integer) 3
127.0.0.1:6379> incr k3
(error) ERR value is not an integer or out of range
#将 key 所储存的值加上给定的增量值(increment) 
INCRBY key increment
#将 key 所储存的值加上给定的浮点增量值(increment) 。
INCRBYFLOAT key increment

4.3 哈希(Hash)

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。

Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)

python 复制代码
# 设置描述信息(name, description, likes, visitors) 到哈希表的 runoobkey 中
127.0.0.1:6379> HMSET runoobkey name "redis tutorial" description "redis basic commands for caching" likes 10 visitors 23000
OK
# 获取存储在哈希表中指定字段的值。
127.0.0.1:6379> HGET runoobkey likes
"10"
# 获取哈希表中的所有字段
127.0.0.1:6379> HKEYS runoobkey
1) "name"
2) "description"
3) "likes"
4) "visitors"

# 获取在哈希表中指定 key 的所有字段和值
127.0.0.1:6379> HGETALL runoobkey
1) "name"
2) "redis tutorial"
3) "description"
4) "redis basic commands for caching"
5) "likes"
6) "10"
7) "visitors"
8) "2300"

4.4 列表(List)

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)

一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)

python 复制代码
# LPUSH key value1 [value2]将一个或多个值插入到列表头部
27.0.0.1:6379> lpush user 1 2 3
(integer) 3
# LRANGE key start stop获取列表指定范围内的元素
127.0.0.1:6379> lrange user 0 2
1) "3"
2) "2"
3) "1"
# 	RPUSH key value1 [value2]在列表中添加一个或多个值到列表尾部
127.0.0.1:6379> rpush user 4 5 6
(integer) 6
127.0.0.1:6379> lrange user -6 -1
1) "3"
2) "2"
3) "1"
4) "4"
5) "5"
6) "6"
127.0.0.1:6379> lrange user 0 5
1) "3"
2) "2"
3) "1"
4) "4"
5) "5"
6) "6"
# 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除
127.0.0.1:6379> ltrim user 2 3
OK
127.0.0.1:6379> lrange user 0 5
1) "1"
2) "4"

4.5 集合(Set)

Redis 的 Set 是 String 类型的无序集合。

集合成员是唯一的,这就意味着集合中不能出现重复的数据。

集合对象的编码可以是intset 或者 hashtable。

Redis 中集合是通过哈希表实现的,添加,删除,查找的复杂度都是 O(1)。

集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)

python 复制代码
# 张三好友
127.0.0.1:6379> sadd zs lisi wangwu zhaoliu
(integer) 3
# 李四好友
127.0.0.1:6379> sadd ls wangwu maiz ergou
(integer) 3
# 获取集合的成员数
127.0.0.1:6379> scard zs
(integer) 3
# 返回集合中的所有成员
127.0.0.1:6379> smembers zs
1) "wangwu"
2) "lisi"
3) "zhaoliu"
# 判断 member 元素是否是集合 key 的成员
127.0.0.1:6379> sismember zs lisi
(integer) 1
# 返回给定所有集合的交集
127.0.0.1:6379> sinter zs ls
1) "wangwu"
# 返回第一个集合与其他集合之间的差异。
127.0.0.1:6379> sdiff zs ls
1) "lisi"
2) "zhaoliu"
# 返回第一个集合与其他集合之间的差异。
127.0.0.1:6379> sdiff ls zs
1) "maiz"
2) "ergou"r

4.6 有序集合(sorted set)

redis 正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复


z集合默认按分数值低到高排序

python 复制代码
#  ZADD 向 redis 的有序集合stus中添加了5个值并关联上分数
127.0.0.1:6379> zadd stus 99 lih 80 hei 66 ta 79 matao 88 lina
(integer) 5
# 移除有序集合中的一个或多个成员
127.0.0.1:6379> zrem stus hei
(integer) 1
# 返回有序集合中指定成员的索引  0开始为第一个
127.0.0.1:6379> zrank stus ta
(integer) 0
# 通过索引区间返回有序集合指定区间内的成员
127.0.0.1:6379> zrange stus 0 2
1) "ta"
2) "matao"
3) "lina"
# 返回有序集中指定区间内的成员,通过索引,分数从高到低
127.0.0.1:6379> zrevrange stus 0 2
1) "lih"
2) "lina"
3) "matao"
# 计算在有序集合中指定区间分数的成员数
127.0.0.1:6379> zcount stus 80 90
(integer) 1
# 通过索引区间返回有序集合指定区间内的成员
127.0.0.1:6379> zrangebyscore stus 80 90
1) "lina"

五、Redis使用

5.1 Java 使用 Redis

安装了 redis 服务及 Java redis 驱

jedis下载: https://github.com/redis/jedis

python 复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>7.1.0</version>
</dependency>
python 复制代码
## 未做测试 请参考
import redis.clients.jedis.Jedis;
public class RedisStringJava {
    public static void main(String[] args) {
        //连接本地的 Redis 服务
        Jedis jedis = new Jedis("localhost");
        // 如果 Redis 服务设置了密码,需要下面这行,没有就不需要
        // jedis.auth("123456");
        System.out.println("连接成功");
        //设置 redis 字符串数据
        jedis.set("runoobkey", "www.runoob.com");
        // 获取存储的数据并输出
        System.out.println("redis 存储的字符串为: "+ jedis.get("runoobkey"));
    }
}

5.2 python 操作Redis

连接到 redis 服务

python 复制代码
#!/usr/bin/env python3
"""
Redis 测试数据 - 可选择保留时间
"""
import redis

def add_test_data(redis_client, minutes=5):
    """添加测试数据,指定保留分钟数"""
    
    print(f"\n📝 添加测试数据(保留 {minutes} 分钟)...")
    
    # 列表数据 - 用户会话
    list_key = 'test:session:logs'
    redis_client.delete(list_key)
    redis_client.rpush(list_key, 
        '用户登录', 
        '查询订单', 
        '修改资料', 
        '退出登录'
    )
    redis_client.expire(list_key, minutes * 60)
    print(f"✅ 列表数据: {list_key}")
    
    # 哈希数据 - 用户信息
    hash_key = 'test:user:1001'
    redis_client.delete(hash_key)
    redis_client.hset(hash_key, mapping={
        'name': '李四',
        'age': '32',
        'email': 'lisi@example.com',
        'phone': '13800138000'
    })
    redis_client.expire(hash_key, minutes * 60)
    print(f"✅ 哈希数据: {hash_key}")
    
    # 返回所有键名
    return [list_key, hash_key]

# 连接Redis
r = redis.Redis(
    host='192.168.10.131',
    port=6379,
    password='123123',
    decode_responses=True
)

# 测试连接
r.ping()
print("✅ Redis连接成功")

# 选择保留时间
print("\n请选择数据保留时间:")
print("1. 1分钟 (快速测试)")
print("2. 5分钟 (默认)")
print("3. 10分钟")
print("4. 30分钟")
print("5. 60分钟 (1小时)")

choice = input("\n请输入选项 (1-5): ").strip()

time_map = {
    '1': 1,
    '2': 5,
    '3': 10,
    '4': 30,
    '5': 60
}

minutes = time_map.get(choice, 5)  # 默认5分钟

# 添加数据
keys = add_test_data(r, minutes)

print(f"\n✅ 数据已添加,将在 {minutes} 分钟后自动删除")
print(f"📌 你可以用以下命令验证:")
for key in keys:
    print(f"   - 查看 {key}: {r.type(key)}")

六、 Redis 持久化

Redis 作为内存数据库,数据默认存储在内存中,服务器重启或崩溃时会丢失。持久化就是为了解决这个问题,将数据保存到磁盘上。

Redis 提供两种主要持久化方式:

  • RDB(Redis Database):定期生成数据快照
  • AOF(Append Only File):记录所有写操作日志

6.1 RDB 持久化(快照)

工作原理

RDB 会在指定时间间隔 生成内存数据的二进制快照,保存到 dump.rdb 文件中

redis 通过 fork 的方式创建一个子进程来专门做持久化的动作,

python 复制代码
# redis.conf 配置
# ## SNAPSHOTTING  ##配置大概364行开始
# sed -n '364,458p' redis.conf |grep -v '^#'|grep -v '^$'
#自动触发 
save 900 1      # 900秒(15分钟)内至少有1个key变化
save 300 10     # 300秒(5分钟)内至少有10个key变化  
save 60 10000   # 60秒(1分钟)内至少有10000个key变化

# RDB文件配置
dbfilename dump.rdb      # 文件名
dir /var/lib/redis       # 保存路径
rdbcompression yes       # 是否压缩

默认配置:

python 复制代码
# 检查配置:
redis-cli CONFIG GET save
------
# 默认配置
1) "save"
2) "3600 1 300 100 60 10000"
3600 1        # 3600秒(1小时)内至少有1个key改变
300 100       # 300秒(5分钟)内至少有100个key改变  
60 10000      # 60秒内至少有10000个key改变
-------
# 监控RDB状态
redis-cli INFO Persistence
---------------------------------------------
rdb_changes_since_last_save:0 - #上次成功保存后,有多少个key发生了变化
rdb_bgsave_in_progress:0 - #是否正在执行后台RDB保存(0=否,1=是)
rdb_last_save_time:1773869532 - #最后一次成功RDB保存的Unix时间戳
rdb_last_bgsave_status:ok - #最后一次后台RDB保存的状态(ok/err)
rdb_last_bgsave_time_sec:-1 - #最后一次后台保存耗时(秒),-1表示从未执行过
rdb_current_bgsave_time_sec:-1 - #当前正在进行的后台保存已耗时,-1表示没有进行中
rdb_last_cow_size:0 - #最后一次RDB保存时的Copy-On-Write内存大小(字节)
--------------------------------------------------

save报错:

dump.db保存文件目录确保该目录存在且 redis 用户拥有写权限:

python 复制代码
vim redis.conf
--------------------------------------------------------------
# The working directory.
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
# The Append Only File will also be created inside this directory.
# Note that you must specify a directory here, not a file name.
dir ./ 
#将 dir ./ 改为 dir /var/lib/redis,并确保该目录存在且 redis 用户拥有写权限:
----------------------------------------------------------
# 手动测试
127.0.0.1:6379> CONFIG SET dir /usr/app/redis
OK
127.0.0.1:6379> CONFIG SET dbfilename dump.rdb
OK

Redis 会根据配置文件中的 dir 和 dbfilename 设置寻找 RDB 文件,

RDB 文件的恢复速度通常比 AOF 快,适合大数据集的恢复,如果同时开启了 AOF 和 RDB,Redis 会优先使用 AOF 文件恢复数据(因为 AOF 通常包含更完整的数据)。

6.2 AOF 持久化(追加文件)

AOF(Append Only File)是Redis中另一种关键的持久化机制。

将我们的写命令全部记录下来,只允许追加文件,不允许改写文件。恢复的时候,将文件中的记录全部执行一遍

redis 重启的时候,就会根据日志文件的内容将写指令按照写入顺序执行,完成数据恢复。aof 保存的是 appendonly.aof 文件

默认配置:

python 复制代码
sed -n '1238,1367p' redis.conf |grep -v '^#'|grep -v '^$'
python 复制代码
appendonly no                    # AOF开关:no=关闭,yes=开启
appendfilename "appendonly.aof"  # AOF文件名
appendfsync everysec             # 写回策略:always/everysec/no,everysec是每秒同步(推荐)
no-appendfsync-on-rewrite no     # 重写时是否暂停fsync:no=不暂停(继续同步),yes=暂停
auto-aof-rewrite-percentage 100  # AOF重写增长率:文件大小比上次重写时增长100%时触发
auto-aof-rewrite-min-size 64mb   # AOF重写最小触发尺寸:文件达到64MB才考虑重写
aof-load-truncated yes           # 加载时是否截断损坏的AOF文件:yes=允许启动并截断,no=启动失败
aof-use-rdb-preamble yes         # 混合持久化开关:yes=开启(RDB头+AOF尾),no=纯AOF

关于 aof 的配置基本上其他的都是使用默认的配置即可,我们只需要把 aof 模式打开即可

python 复制代码
appendonly yes

当 Redis 同时开启 RDB 和 AOF 时,重启后默认会优先使用 AOF 文件进行数据恢复。因为你当前的 AOF 文件是空的,如果直接重启,Redis 会加载一个空的数据集,导致你的 RDB 文件被忽略。

可以通过在线热修改 的方式来完成,避免再次重启造成服务中断。

在线开启 AOF:

python 复制代码
127.0.0.1:6379> CONFIG SET appendonly yes
OK

这个命令会立即启用 AOF,Redis 会在后台开始将当前内存中的数据写入到一个新的 AOF 文件中,这个过程不会阻塞你的服务

python 复制代码
[root@localhost redis] ll | grep -E "dump.rdb|appendonly.aof"
-rw-r--r--.  1 redis redis   298 Mar 19 03:50 appendonly.aof
-rw-r--r--.  1 redis redis   298 Mar 19 03:51 dump.rdb

持久化配置到文件

python 复制代码
# 用当前的内存配置来更新你的 redis.conf 文件,将 appendonly 重新设为 yes
CONFIG REWRITE
# 或手工修改配置文件后重启,因为appendonly以有数据了

这套流程的核心思想是:先让 Redis 以 RDB-only 模式启动来恢复数据,数据确认无误后,再在线启用 AOF,避免空 AOF 文件造成的数据丢失

RDB的快速恢复 + AOF的高安全性"的实现方式

混合持久化

同时开启 RDB 和 AOF,并设置 aof-use-rdb-preamble yes,AOF 文件前半段是 RDB 格式快照,后半段是增量命令,兼具RDB 的快速恢复和AOF 的高安全性。

七、应用问题解决

7.1 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,导致每次请求都要穿过缓存层,直接查询数据库现象.

典型场景

恶意攻击:爬虫或攻击者持续扫描不存在的用户ID(如 id = -1、id = 999999 等

解决方案

例 参数合法性校验:

在业务入口层对请求参数进行严格的格式校验,提前过滤掉明显不合法的请求

python 复制代码
public Result queryById(String id) {
    // 1. 基本格式校验
    if (id == null || id.length() == 0) {
        return Result.fail("ID不能为空");
    }
    
    // 2. 业务规则校验(例如ID必须是数字且大于0)
    if (!StringUtils.isNumeric(id) || Long.parseLong(id) <= 0) {
        return Result.fail("ID格式不正确");
    }
    
    // 3. 权限校验
    if (!checkPermission(id)) {
        return Result.fail("无权访问");
    }
    
    // 4. 正常查询流程
    // ...
}

7.2 缓存击穿

缓存击穿是指某一个热点key在缓存过期的瞬间,同时有大量并发请求访问这个key,导致所有请求都直接穿透到数据库的现象。

与缓存穿透不同,缓存击穿访问的数据在数据库中是真实存在的,只是在某个时间点缓存刚好失效了

典型场景

秒杀商品:某个爆款商品的详情页缓存过期,大量用户同时刷新

解决方案

普通热点key → 互斥锁保护

超高热点key → 逻辑过期 + 异步刷新

稳定热点key → 永不过期 + 定时刷新

例如 方案一:互斥锁(Mutex Lock):

当缓存失效时,只允许一个线程去查询数据库重建缓存,其他线程等待重建完成。

python 复制代码
public Result queryById(Long id) {
    String key = "cache:shop:" + id;
    String lockKey = "lock:shop:" + id;
    
    // 1. 从缓存查询
    String shopJson = redis.get(key);
    if (StrUtil.isNotBlank(shopJson)) {
        return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
    }
    
    // 2. 缓存未命中,尝试获取互斥锁
    String lockValue = UUID.randomUUID().toString();
    Boolean isLock = redis.setnx(lockKey, lockValue, 10); // 10秒过期
    
    if (isLock) {
        // 3. 获取锁成功,查询数据库
        try {
            Shop shop = db.query(id);
            if (shop == null) {
                redis.setex(key, "NULL", 300);
                return Result.fail("数据不存在");
            }
            redis.setex(key, JSONUtil.toJsonStr(shop), 3600);
            return Result.ok(shop);
        } finally {
            // 4. 释放锁(需要校验是否是自己的锁)
            String currentLock = redis.get(lockKey);
            if (lockValue.equals(currentLock)) {
                redis.del(lockKey);
            }
        }
    } else {
        // 5. 获取锁失败,等待一段时间后重试
        Thread.sleep(50);
        return queryById(id); // 递归重试(实际应用需设置重试次数)
    }
}

7.3 缓存雪崩

缓存雪崩是指在某一个时间段内,大量的缓存key同时失效,或者缓存层整体宕机.

如果说缓存击穿是"单点失效",那缓存雪崩就是"群体事件"。

缓存雪崩的破坏力远大于击穿和穿透,因为它往往是大规模、系统级的故障:

典型场景

批量失效:所有缓存的过期时间设置成相同值(如统一凌晨过期)

缓存服务宕机:Redis集群整体故障

网络分区:应用服务器与缓存服务器网络中断

重启恢复:缓存服务重启后,大量key需要重建

解决方案

在实际生产环境中,需要构建多层次的雪崩防御体系:

第一层:过期时间打散 + 永不过期策略

↓ 第二层:多级缓存(本地缓存)

↓ 第三层:Redis高可用(主从/集群)

↓ 第四层:数据库连接池限流

↓ 第五层:熔断降级 + 优雅降级页面

python 复制代码
@Service
public class CacheService {
    
    // 本地缓存(一级缓存)
    private Cache<String, Object> localCache = Caffeine.newBuilder()
            .maximumSize(100000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();
    
    @Autowired
    private StringRedisTemplate redisTemplate;  // Redis(二级缓存)
    
    @Autowired
    private DatabaseService databaseService;    // 数据库
    
    @Autowired
    private RateLimiter rateLimiter;            // 限流器
    
    public Object query(String key) {
        // 1. 限流前置
        if (!rateLimiter.tryAcquire()) {
            return getFallback(key);
        }
        
        // 2. 查询本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 3. 查询Redis(带随机过期时间)
        String json = redisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            value = JSONUtil.toBean(json, Object.class);
            localCache.put(key, value);  // 回填本地缓存
            return value;
        }
        
        // 4. 分布式锁保护数据库
        String lockKey = "lock:" + key;
        String lockValue = UUID.randomUUID().toString();
        Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS);
        
        if (Boolean.TRUE.equals(locked)) {
            try {
                // 双重检查
                json = redisTemplate.opsForValue().get(key);
                if (StrUtil.isNotBlank(json)) {
                    return JSONUtil.toBean(json, Object.class);
                }
                
                // 查询数据库
                value = databaseService.query(key);
                
                if (value != null) {
                    // 写入Redis(基础过期时间 + 随机偏移)
                    int baseExpire = 3600;
                    int randomOffset = ThreadLocalRandom.current().nextInt(600);
                    redisTemplate.opsForValue()
                            .set(key, JSONUtil.toJsonStr(value), baseExpire + randomOffset, TimeUnit.SECONDS);
                    
                    localCache.put(key, value);  // 写入本地缓存
                } else {
                    // 缓存空值(短过期时间)
                    redisTemplate.opsForValue()
                            .set(key, "NULL", 300, TimeUnit.SECONDS);
                }
                
                return value;
            } finally {
                // 释放锁
                String currentLock = redisTemplate.opsForValue().get(lockKey);
                if (lockValue.equals(currentLock)) {
                    redisTemplate.delete(lockKey);
                }
            }
        } else {
            // 未获取到锁,等待后重试
            Thread.sleep(50);
            return query(key);  // 递归重试(实际应用需限制次数)
        }
    }
    
    // 降级方案
    private Object getFallback(String key) {
        // 1. 尝试从本地缓存获取(即使已过期)
        Object local = localCache.getIfPresent(key);
        if (local != null) {
            return local;  // 返回可能过期的数据,优于返回空
        }
        
        // 2. 返回默认值
        return "系统繁忙,请稍后重试";
    }
}

缓存雪崩 vs 缓存击穿 vs 缓存穿透

一句话总结:缓存雪崩是大规模的灾难,需要从架构层面构建防御体系;击穿和穿透是局部问题,可以通过业务逻辑解决.


总结

Redis 不仅仅是一个缓存工具,更是一个高性能的内存数据存储系统。

本文主要内容:

在实际生产环境中,需要牢记以下几点:

  • 数据有价,持久化先行:根据数据重要性选择合适的持久化策略

  • 监控是眼睛,日志是耳朵:部署完善的监控系统(Prometheus +

    Grafana),及时发现异常

  • 容量规划要提前:预估数据增长,提前做好分片和扩容准备

  • 安全问题不放松:设置强密码、绑定内网IP、开启防火墙

  • 缓存问题防患于未然:在系统设计阶段就考虑穿透、击穿、雪崩的防护措施

Redis和Kafka

你去找 Redis 要东西(读请求),它直接从内存拿给你,快。

你往 Kafka 扔东西(写请求/事件),它先稳稳收下存好,让别的系统慢慢来拿,稳

相关推荐
小碗羊肉1 小时前
【MySQL | 第五篇】事务
数据库·mysql
dFObBIMmai1 小时前
Python Celery任务队列怎么配_实现Web后台异步任务调度处理
jvm·数据库·python
于歌8521 小时前
Oracle批处理操作方法
数据库·oracle
日取其半万世不竭1 小时前
PostgreSQL 云服务器安装配置指南:从零开始搭建生产数据库
服务器·数据库·postgresql
@小柯555m1 小时前
MySql(高级操作符--高级操作符练习(1))
数据库·sql·mysql
码农阿豪1 小时前
Python 操作金仓数据库的完全指南(下篇):SQL执行、批量操作与扩展功能
数据库·python·sql
满昕欢喜1 小时前
回顾与总结
数据库·sqlserver
DBdoctor官方1 小时前
DBdoctor v3.3.5.2发布:新增GoldenDB分布式纳管
数据库·sql·polardb·dbdoctor·goldendb
2501_901200532 小时前
mysql数据库主键类型对性能的影响_使用自增整数优于UUID
jvm·数据库·python