
00:00:00
外卖卷:非常划算 扫码领劵 省点小钱钱
文章发布较早,内容可能过时,阅读注意甄别。
Redis 常见的五种基本数据类型:
String(字符串)
Hash(哈希)
ID 对应一个 Hash,这个 Hash 里有 name、age、email 等字段。List(列表)
Set(集合)
ZSet / Sorted Set(有序集合)
主要有以下几个关键原因:
基于内存操作:
高效的数据结构:
单线程模型(核心处理部分):
非阻塞 I/O 多路复用:
C 语言编写:
为什么 Redis 早期设计为单线程?
Redis 早期(直到 6.0 版本之前,核心处理部分)设计为单线程,主要基于以下几点考虑:
避免并发控制开销:
内存访问速度快:
更强的可维护性:
CPU 瓶颈不是主要限制:
Redis 6.0 版本为何引入多线程?
虽然单线程有很多优点,但随着网络硬件的升级(万兆网卡普及)和 CPU 核心数的增加,网络 I/O 的处理逐渐成为了 Redis 单线程模型的瓶颈。
Redis 6.0 引入多线程的目的:
Redis 6.0 引入的多线程不是为了让数据操作(命令执行)变成多线程,而是为了分担网络 I/O 读写和解析的工作。
SET, GET, LPUSH 等),这意味着数据结构的访问和操作仍然是单线程的,这继续保留了单线程模型简单、无锁竞争的优点。总结:Redis 6.0 引入多线程是对单线程模型的一种优化和增强,旨在解决网络 I/O 瓶颈,而不是改变其核心命令执行的单线程特性。它在保持原有的简单高效优势的同时,更好地利用了现代多核 CPU 的性能。
大白话理解:
想象一下你有一本厚厚的、按字母顺序排列的电话簿。如果你要找一个人的电话,你当然可以从头开始一页一页翻(这就是普通链表)。但这太慢了!
为了加速查找,你在电话簿上加了几层目录:
当你要找一个名字时,你不是从头翻,而是先从最粗略的目录开始,快速跳到大概的范围,然后进入下一层目录,再跳到更小的范围,直到最终在最详细的目录中找到。
Redis 跳表(SkipList)的实现原理就是这个思想:
Redis 的 ZSet (有序集合) 底层就是用跳表来实现的。
数据结构:
查找过程:
插入过程:
删除过程:
跳表的优点:
为什么 Redis 不用红黑树而用跳表?
虽然红黑树的性能和跳表类似,但跳表在以下方面可能更具优势:
大白话理解:
Redis 的 Hash 类型就像一个“哈希表”或者“字典”(在编程语言中常被称为 Map 或 Dictionary)。你可以把它想象成一个箱子,这个箱子有自己的名字(Redis key),箱子里面又分了很多小格子,每个小格子都有自己的标签(field)和里面放的东西(value)。
结构:
key (顶层 Redis key) → Hash (哈希表)
field1 → value1field2 → value2field3 → value3能干啥?
最典型的应用是存储对象。
传统方式(String):存储一个用户对象,你可能需要存多个 String key:
user:1:name → Aliceuser:1:age → 30user:1:email → alice@example.comRedis Hash 方式:存储一个用户对象:
user:1 (Hash key) name → Aliceage → 30email → alice@example.comHGETALL user:1 就能获取用户所有信息,大大减少了网络开销。底层实现:
Redis 的 Hash 类型底层使用了两种数据结构来优化存储,会根据存储的数据量和字段长度自动切换:
ziplist (压缩列表):
hash-max-ziplist-entries 配置,默认 512)且所有字段和值的大小都较小(hash-max-ziplist-value 配置,默认 64 字节)时,Redis 会使用 ziplist。hashtable (哈希表):
ziplist 的使用条件时(字段数量或字段值大小超过阈值),Redis 会将底层存储从 ziplist 转换为 hashtable。优点:
大白话理解:
Redis 的 ZSet(有序集合)就像一个带分数的排行榜。每个榜单项目(member)都有一个独一无二的名字,并且有一个对应的分数(score)。Redis 会根据这个分数把它们从小到大排好。分数相同的话,就按名字的字母顺序排。
结构:
key (顶层 Redis key) → ZSet (有序集合)
member1 (score1)member2 (score2)member3 (score3)能干啥?
最典型的应用就是各种排行榜,比如游戏积分榜、商品销售榜、微博热搜榜等等。你还可以根据分数范围查询,比如找出积分在 100 到 200 之间的玩家。
底层实现:
Redis 的 ZSet 也是根据存储的数据量和元素大小,以及 ZSet 的具体操作特点,自动切换底层的数据结构:
ziplist (压缩列表):
zset-max-ziplist-entries 配置,默认 128)且每个元素的值和分数都较小(zset-max-ziplist-value 配置,默认 64 字节)时,Redis 会使用 ziplist。ziplist 中,member 和 score 是紧挨着存放的,通过有序插入保证了顺序。skiplist (跳表) + hashtable (哈希表):
ziplist 的条件时,Redis 会转换为 skiplist 和 hashtable 的组合结构。skiplist (跳表):负责根据 score 来排序和查找。它能以 O(logN) 的时间复杂度进行插入、删除、查找和范围查找(通过分数)。hashtable (哈希表):负责根据 member(元素名称)快速查找其对应的 score。这样,你可以根据元素名直接获取其分数,时间复杂度为 O(1)。member 的快速访问。总结:
Redis 巧妙地利用了多种底层数据结构,并根据数据的特性和操作的频繁程度进行自动切换,以达到在不同场景下内存占用和性能的最佳平衡。这种设计是 Redis 如此高效和灵活的关键之一。
缓存(Redis)是数据库(MySQL)的“副本”,为了保证数据不“脱节”,它们需要保持同步。
常见方案(及其优缺点):
先更新数据库,再删除缓存 (推荐)
先删除缓存,再更新数据库 (不推荐)
其他方案:
Binlog(操作日志)里。我们可以弄一个程序,专门“监听”这些 Binlog 的变化,一旦发现数据库有改动,就通过异步方式去更新或删除缓存。选择原则:
如果追求实时一致性(即时可见新数据):
如果追求最终一致性(数据迟早会一致):
总结:最常用的方法是“先更新数据库,再删除缓存”,并结合其他机制(如消息队列、延迟双删)来增强其可靠性。对于更高一致性或复杂业务,可以考虑基于 Binlog 的异步同步方案。
这三个是使用缓存时常见的“灾难”,需要我们提前预防。
缓存击穿 (Cache Breakdown)
缓存穿透 (Cache Penetration)
缓存雪崩 (Cache Avalanche)
虽然 Redis String 看起来就是简单的字符串,但它在底层并不是直接使用 C 语言的字符串(以 \0 结尾的字符数组)。Redis 为它设计了一种更聪明、更高效的结构,叫做 SDS (Simple Dynamic String),简单动态字符串。
为什么不用 C 语言字符串(char*)?
C 语言字符串有几个缺点:
\0 才能知道长度。SDS (Simple Dynamic String) 的结构:
SDS 是一个结构体,它比普通的 char* 多了一些信息:
struct sdshdr {
long len; // 已使用的字节数 (当前字符串的长度)
long alloc; // 总分配的字节数 (字符串总容量,不包括\0)
unsigned char flags; // 3 位用来表示类型
char buf[]; // 存储字符串数据,以 \0 结尾
};SDS 相对于 C 字符串的优点:
获取长度 O(1):直接通过 len 字段就可以获取字符串长度,效率非常高。
杜绝缓冲区溢出:
二进制安全:
\0 就认为字符串结束了。但 SDS 可以存任意二进制数据(图片、视频、序列化的对象),它只通过 len 字段来判断字符串的结束,而不是 \0。兼容 C 字符串函数:
len 字段,但 buf 数组仍然以 \0 结尾,这使得它可以直接作为 C 字符串传递给 C 语言标准库函数使用,增强了兼容性。总结:SDS 是 Redis 字符串类型高性能和灵活性的重要基石。它通过牺牲一点点内存空间(额外存储 len、alloc、flags 和预留空间)来换取操作的高效和安全。
大白话理解:
分布式锁就像是你在一个大办公室里(分布式系统),有一份绝无仅有的重要文件(共享资源)。为了防止多个人同时去修改这份文件导致错误,你需要一个“门卫”(分布式锁)。谁想修改文件,就得先找门卫要“钥匙”。门卫把唯一的钥匙给一个人,其他人就只能等着。等拿到钥匙的人改完文件,把钥匙还给门卫,其他人才能继续去拿钥匙。
Redis 实现分布式锁的基本原理:
Redis 提供了一些命令,可以很巧妙地实现这个“门卫”和“钥匙”的机制。最核心的命令是 SETNX (Set if Not eXists) 和 SET 命令的扩展。
实现步骤:
加锁 (Acquire Lock):
SET key value NX PX milliseconds 命令。key:锁的名称。value:一个随机字符串,用于标识当前请求(比如一个 UUID),这是为了在释放锁时识别是不是自己的锁。NX:表示“只在键不存在时才设置”。如果键已经存在(说明锁已经被别人持有),则设置失败,返回 nil。PX milliseconds:设置锁的过期时间(毫秒),这是一个非常重要的步骤,用于防止死锁。即使持有锁的客户端崩溃了,锁也能在一定时间后自动释放。执行业务逻辑:
SET 命令返回成功,表示加锁成功,客户端可以执行需要保护的业务逻辑(比如扣减库存)。释放锁 (Release Lock):
为了防止误删(即删除了别人的锁),需要先获取锁的 value,判断是不是自己设置的那个 value,如果是,才执行 DEL key 命令删除锁。
这个操作必须是原子性的! 否则可能出现:A 判断是自己的锁 → 锁过期自动释放 → B 拿到锁 → A 执行 DEL 删除了 B 的锁。
解决方案:使用 Lua 脚本 来保证“判断”和“删除”的原子性。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end大白话:“门卫,我把‘商品库存锁’还给你。你看看钥匙上是不是我自己的编号?如果是,你再收回,不是就别动。”(这个“看是不是自己的钥匙”和“收回钥匙”的动作,必须在一个步骤里完成,不能被别人打断)
大白话理解:
RedLock (Redlock Algorithm) 是 Redis 作者 Antirez 提出的一种更高级、更严格的分布式锁算法,主要是为了解决单点 Redis 分布式锁在宕机时可能出现的安全性问题。
RedLock 的核心思想:
RedLock 不依赖于单个 Redis 实例,而是依赖于 N 个独立的 Redis Master 节点(通常是奇数,如 5 个)。客户端需要从大多数(N/2 + 1 个) Redis 实例上成功获取锁,才算真正获取到锁。
RedLock 的加锁过程:
SET key value NX PX timeout。value 也是一个唯一标识符。timeout 是锁的过期时间(例如 10 秒)。(T2 - T1) 小于锁的有效时间(timeout)。DEL 命令,释放这些锁。RedLock 的释放锁过程:
客户端向所有 N 个 Redis 实例都发送 DEL 命令,无论是否在上面成功获取了锁。这是因为,即使某个实例没有成功加锁,也可能是网络问题导致,为了确保万无一失。
虽然 Redis 实现分布式锁非常方便,但也伴随着一些坑,需要注意:
死锁 (Deadlock):
锁误删 (Misdeletion):
value(比如 UUID)。在释放锁时,先检查锁的 value 是否与自己设置的 value 一致,只有一致才删除。value”和“删除锁”这两个操作必须是原子性的,否则依然可能出现误删。使用 Lua 脚本可以将这两个操作捆绑在一起,确保原子执行。非原子操作导致的问题:
SETNX 和 EXPIRE 分开执行),在执行过程中 Redis 宕机,可能导致锁没有过期时间,变成“永生锁”,从而引发死锁。SET key value NX PX milliseconds:这是 Redis 2.6.12 及以后版本提供的原子性设置锁和过期时间的命令,强烈推荐使用。锁的粒度问题:
key,精确锁定需要保护的资源。例如,扣减库存,锁的 key 可以是 product:stock:item_id。Redis 实例宕机导致锁丢失 (单点故障):
时钟跳跃问题:
总的来说:对于大多数业务场景,基于 SET key value NX PX 和 Lua 脚本的单点 Redis 分布式锁,配合过期时间、唯一标识符和续期机制(如果需要),已经能够满足需求。如果对数据一致性有极高要求,且能接受更高复杂度,可以考虑 RedLock 或其他更成熟的分布式锁框架(如 Zookeeper 实现的 Curator 锁)。
Redis 是一个内存数据库,数据都存在内存里,速度快。但内存有个缺点:服务器一关机,数据就没了。所以,为了防止数据丢失,Redis 需要把内存里的数据“备份”到硬盘上,这个过程就叫持久化。Redis 有两种主要的备份方式:
方式一:RDB (Redis Database) 快照
.rdb 文件)。SAVE 命令:阻塞 Redis 主进程,直到保存完成。生产环境慎用,因为它会阻塞所有客户端请求。BGSAVE 命令:后台异步保存。Redis 会 fork 一个子进程去执行保存操作,主进程可以继续处理请求。这是常用的手动方式。redis.conf 配置中设置策略,例如: save 900 1 (900 秒内至少有 1 次改动就保存)save 300 10 (300 秒内至少有 10 次改动就保存)save 60 10000 (60 秒内至少有 10000 次改动就保存)BGSAVE。.rdb 文件。.rdb 文件,恢复数据效率高。BGSAVE 是子进程操作,对主进程影响较小。方式二:AOF (Append Only File) 只追加文件
appendonly.aof)。redis.conf 中开启 appendonly yes。appendfsync 配置: always:每个写命令都立即同步到磁盘。最安全,但性能最低。everysec:每秒同步一次。推荐,兼顾安全性和性能。 最多丢失 1 秒数据。no:由操作系统决定何时同步。性能最高,但最不安全。SET A 1,再 SET A 2,再 DEL A)。AOF 重写就是把这些冗余命令压缩,生成一个新的、更小的 AOF 文件。BGREWRITEAOF,也可以配置自动触发(例如 auto-aof-rewrite-percentage 100 和 auto-aof-rewrite-min-size 64mb)。appendfsync 策略,可以做到最多丢失 1 秒数据(everysec)。混合持久化 (Redis 4.0+)
everysec 策略,并开启混合持久化。主从复制就像是“老师和学生”的关系。一台 Redis 服务器(Master,老师)负责处理所有写请求,并把它的数据变化实时同步给其他多台 Redis 服务器(Slave,学生)。学生们只负责读请求。这样做的目的是为了:
实现原理步骤:
建立连接:
replicaof masterip masterport),Slave 会向 Master 发送 PSYNC 命令请求同步。全量复制 (Full Resynchronization):
BGSAVE 命令,生成当前的 RDB 快照文件。增量复制 (Partial Resynchronization):
Master 宕机后的处理:
Redis 里的数据可以设置一个“保质期”(过期时间)。数据到了保质期后,就应该被删除。但 Redis 不是一到时间就立马删除,它有自己的“删除策略”,因为如果所有数据都到了时间就立即删除,会很耗费 CPU 资源。
Redis 主要采用两种策略配合使用:
策略一:惰性删除 (Lazy Deletion)
GET、HGET 等命令)某个带有过期时间的 Key 时。策略二:定期删除 (Active Deletion)
额外补充:内存淘汰策略 (Eviction Policy)
maxmemory 限制,并且有新的数据写入时。noeviction:不删除,新写入会报错。volatile-lru:从设置了过期时间的 Key 中,淘汰最近最少使用的。allkeys-lru:从所有 Key 中,淘汰最近最少使用的(不管有没有设置过期时间)。volatile-ttl:从设置了过期时间的 Key 中,淘汰即将过期的。allkeys-random:从所有 Key 中,随机淘汰。等等...总结:Redis 主要通过惰性删除(访问时删除)和定期删除(随机抽样删除)来管理过期 Key。这两种策略权衡了 CPU 资源和内存回收效率。当内存真正吃紧时,才会启动内存淘汰策略,按照配置好的规则删除 Key 来腾出空间。
热点 Key 就像一个“明星商品”或者“爆款新闻”,在短时间内有大量并发请求去访问它。在 Redis 中,这会导致:
这些都可能导致 Redis 服务响应变慢,甚至短暂宕机。
解决热点 Key 的方案:
解决热点 Key 的核心思想就是:分散压力 和 减少直接访问。
数据预热与本地缓存 (Local Cache)
热点 Key 分散存储 (Hash Tag)
product:100,现在可以变成 product:100:#1、product:100:#2 ... product:100:#N。加锁与队列限流 (针对写操作)
数据分级缓存 (多级缓存)
单机 Redis 性能再强,也有内存和并发的上限。Redis 集群就是把多个 Redis 实例组织起来,让它们像一个整体一样工作,共同存储海量数据,并处理巨大的并发请求。
核心目标:
实现原理:
Redis Cluster 采用去中心化的架构,每个节点都保存整个集群的状态信息。
数据分片:槽 (Slot)
hash_value % 16384 得到该 Key 所属的槽位。{} 包裹起来,Redis 只对 {} 中的内容计算哈希值。例如 user:{123}:name 和 user:{123}:age 都会落在同一个槽中。客户端路由:
MOVED 或 ASK 错误,告诉客户端正确的节点地址,客户端会重定向到正确的节点。高可用:主从复制 + 故障转移
节点通信:
集群的优点:
集群的缺点:
Big Key (大 Key) 指的是 Redis 中存储的 Key 对应的值非常大。这个“大”可以是:
Big Key 会带来什么问题?
GET、DEL、HGETALL、LRANGE 大范围查询)时,需要消耗大量 CPU 和内存,导致 Redis 主线程阻塞,无法响应其他请求,造成“卡顿”。如何发现 Big Key?
redis-cli --bigkeys 命令:可以扫描整个 Redis 实例,找出各种数据类型中占用内存最大的 Key。 redis-cli -h 127.0.0.1 -p 6379 --bigkeysMEMORY USAGE key 命令:针对特定 Key 查看其内存占用。如何解决 Big Key 问题?
解决 Big Key 的核心思想就是:“化整为零”,把大 Key 拆分成小 Key,或者分批处理大 Key。
拆分 Key:
user_feed:userid (List 存储用户所有动态) 拆分为 user_feed:userid:page1, user_feed:userid:page2 等多个小 List。big_object:id 拆分为多个小 Hash:big_object:id:field1, big_object:id:field2。使用合理的数据结构:
Big Key 的删除 (非阻塞删除):
DEL 命令,那样会阻塞 Redis。让 Redis 在后台慢慢删。UNLINK key [key ...]:这个命令是非阻塞的,它会把 Key 的实际删除操作放到后台线程中异步执行,主线程可以立即返回并处理其他请求。FLUSHALL ASYNC / FLUSHDB ASYNC:同样是异步清理所有 Key。限制集合类型的大小:
定期清理:
总结:发现 Big Key 是第一步,核心是将其拆小,并采用异步非阻塞的方式进行删除,以及在设计阶段就避免 Big Key 的产生。
评论