Redis
Redis缓存基础
Redis之所以能够实现高性能,主要得益于其单线程模型、内存操作、IO多路复用机制
单线程:由于所有命令都在同一个线程中顺序执行,因此不需要复杂的锁机制来保护共享资源,避免了多线程环境下的竞态条件和死锁问题。减少上下文切换开销。
基于内存操作:RAM访问时间远低于磁盘I/O。
非阻塞IO多路复用:Redis采用了基于epoll的事件驱动架构,可以在不阻塞主线程的情况下同时监听多个客户端连接的状态变化。通过异步读取和写入网络套接字,Redis可以最大限度地利用可用带宽,而不必等待每一个单独的操作完成。
常用数据类型
Redis常见数据类型:字符串,哈希,列表,集合,有序集合
String(字符串)不仅可以存储字符串, 还可以存储整数或浮点数。适用于缓存热点数据,实现各种计数器功能(如网站访问统计、点赞次数等)
# 设置指定键的值,并可选地设置过期时间(秒或毫秒) | 获取指定键的值
SET mykey "Hello" EX 10 NX
GET mykey
# 将键所存储的数值加一/减一。如果键不存在,则认为它的值是 0
INCR counter
DECR counter
# 将键所存储的数值加上/减去指定增量。同样,若键不存在则视为 0
INCRBY score 5
DECRBY score 2
底层实现: SDS (Simple Dynamic String) Redis 使用自己实现的简单动态字符串结构来表示 String 类型的数据。
对于较短的字符串(小于等于39个字节),Redis 采用 embstr 编码, 对于较长的字符串,则使用普通的 raw 编码。
String 实际上可以容纳任意二进制数据,可达 512MB。
Hash(哈希) 是一种键值对集合的数据结构,非常适合存储对象(可存取对象中的属性,并且支持原子性操作),如购物车对象
压缩列表(ziplist):为了节省内存空间,当 Hash 中的元素数量较少且单个元素大小较小时,Redis 会使用压缩列表作为底层实现。(在 Redis 7.0 及以上版本中,ziplist 被 listpack 所取代)
哈希表(hashtable):随着 Hash 中元素数量的增长或单个元素变大,Redis 会自动将压缩列表转换为更高效的哈希表结构。
# 用户 user123 添加了两个 item456 商品
HSET cart:user123 item456 2
# 查看用户 user123 的购物车物品
HGETALL cart:user123
# 查询 user123 的 item456 数量
HGET cart:user123 item456
# 用户 user123 移除了一个 item456 商品(注意确保不会出现负数)
HINCRBY cart:user123 item456 -1
# 完全删除某商品
HDEL cart:userid itemid
# 一次性清除所有商品
DEL cart:userid
# 统计购物车中商品种类数
HLEN cart:userid
# 分别统计购物车中每种商品的数量
HVALS cart:userid
哈希表是一种通过哈希函数将键映射到特定位置的数据结构,使得查找、插入和删除操作的时间复杂度接近 O(1)。
Redis 中的 List 是一种双向链表(linked list)数据结构,它可以在列表的头部和尾部高效地添加或移除元素。可利用List实现分布式的 栈和队列:
栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构
LPUSH key value # 将一个值插入到列表的头部(左端)
LPOP key # 移除并返回列表的第一个元素(左端的元素)
队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构
LPUSH key value # 将一个值插入到列表的尾部(右端)
RPOP key # 移除并返回列表的第一个元素(左端的元素)
# 阻塞队列 示例:
LPUSH key value
BRPOP key [key ...] timeout # 列表为空则阻塞连接直到等待超时或元素入列
其他应用场景:
- 任务队列:可以用来管理待处理的任务,确保任务按照一定的顺序被执行。
- 社交网络的消息流:例如 微博,可以通过 List 来保存用户发布的新鲜事。
- 缓存最近/最频繁使用的数据:例如,可以维护一个最近浏览商品的列表。
Set 是一种无序且不允许重复元素的数据结构,非常适合用于存储唯一性的数据集合。
Set 支持高效的成员添加、删除和检查操作,并且能够执行交集、并集和差集等集合运算。
Set 的底层实现是基于哈希表(hash table),这使得其成员性检查、添加和移除操作的时间复杂度接近于 O(1)。对于小集合,为了节省内存,当集合中元素的数量很少(默认 小于64 个元素)且成员为整数时,Redis 使用一种叫做 intset(整数集合) 的紧凑编码方式来表示集合。
应用场景: 去重,点赞,关注模型,抽奖, 下为一个简单的抽奖示例:
# 1. 初始化奖池
SADD raffle_pool participant1 participant2 participant3 ... participantN
# 2. (可选) 查看所有参与抽奖者
SMEMBERS raffle_pool
# 3. 抽取幸运儿
# 方法一: 使用 SPOP 直接抽取并移除
winner=$(SPOP raffle_pool)
# 方法二: 使用 SRANDMEMBER 和 SREM 组合
# winner=$(SRANDMEMBER raffle_pool) # 返回一个随机的参与者,但不移除它
# SREM raffle_pool "$winner" # 将指定的参与者从奖池中移除
# 4. 记录中奖结果
RPUSH winners "$winner" # 使用 List 记录中奖者顺序
# 或者
SADD winners_set "$winner" # 使用 Set 确保唯一性
ZSet(Sorted Set,有序集合)每个成员关联一个分数(score),使其可以根据分数进行排序
- 成员数量较少时使用压缩列表(ziplist / listpack)以节省内存
- 当成员数量或大小超过一定阈值时,则采用跳跃表(skiplist)结合哈希表(hash table)来保证高效的操作和成员唯一性
应用场景:排行榜
在 Redis 中,TYPE
和 OBJECT ENCODING
提供了关于键的不同层次的信息:
TYPE
查询这个键代表的是什么类型的数据结构(如字符串、列表、集合等)OBJECT ENCODING
展示 Redis 内部是如何实际存储这些数据结构的(使用哪种编码方式)
HSET user:1000 name "Alice"
# 查询该键的类型
TYPE user:1000 # hash
# 查询 user:1000 的底层编码方式
OBJECT ENCODING user:1000 # listpack
底层数据结构
Redis内部使用了多种底层数据结构: 哈希表, 双向链表, 整数集合, 压缩列表, 跳表
哈希表(Hash Table)
主要用于实现 Redis 的 Hash
数据类型,以及 ZSet 和 Set 内部成员唯一性的保证。
- 提供 O(1) 时间复杂度的插入、查找和删除操作。
- 使用拉链法解决哈希冲突问题。
应用场景:适合需要快速查找、添加或移除元素的场景,如缓存、会话管理等。
双向链表(Doubly Linked List): 用于实现 Redis 的 List
数据类型。
- 每个节点包含指向前驱和后继节点的指针。
- 支持两端高效地插入和删除操作。
应用场景:适用于队列、栈等先进先出(FIFO)或后进先出(LIFO)的数据处理模式。
整数集合(Intset): 在小集合且所有成员均为整数时,用作 Set
的底层实现。
- 只能存储 16 位、32 位或 64 位整数,并根据需要自动升级内部表示形式。
- 确保成员唯一性并维持有序排列。
- 高效利用内存空间。
应用场景:特别适合存储少量整数值的小型集合。
压缩列表(Ziplist / Listpack): 用于优化小集合或短序列的存储。
- 将所有元素连续地存储在一个内存块中,避免指针带来的额外开销。
- 使用变长编码表示每个元素及其长度信息。
- 允许在头部或尾部高效地插入和删除元素。
- 支持不同类型的数据(整数、字符串),并通过混合编码方式选择最合适的表示方法。
应用场景:适用于存储小集合或短序列的数据,如小型 List、Set 和 ZSet
跳跃表(Skiplist): 作为 Redis ZSet 的主要底层实现之一,确保成员按照分数排序。
- 通过多层索引加速查找、插入和删除操作,平均时间复杂度为 O(log N)。
- 新节点插入时,随机决定其层级,确保跳跃表高度合理。
- 每个节点不仅存储实际的数据项(成员及其分数),还维护指向同一层下一个节点及下一层相同位置节点的指针。
应用场景:非常适合处理有序集合中的各种操作,如排行榜、优先级队列等。
为什么选择跳表?==> 相对于红黑树实现更为简单,多线程环境中并发更友好 (内存效率更高?)
压缩列表通过紧凑存储减少内存开销,适用于小集合;跳表则通过多层索引加速查找、插入和删除操作,适用于需要高效排序和随机访问的大规模有序数据集。
持久化策略
- Redis 的持久化机制有哪些?
- 常见的持久化机制包括:
- RDB:定期将内存中的数据快照保存到磁盘。
- AOF:记录服务器执行的所有写操作命令,以便恢复数据。
- 常见的持久化机制包括:
- Redis 在生成 RDB 文件时如何处理请求?
- 生成RDB文件时,Redis会创建一个子进程来执行快照操作,父进程继续处理请求,子进程完成后将快照文件写入磁盘。
击穿/穿透/雪崩
- Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?
- 缓存击穿:某个热点Key突然大量请求,导致缓存失效,所有请求直接打到数据库。
- 缓存穿透:查询一个不存在的Key,导致每次查询都直接打到数据库。
- 缓存雪崩:大量缓存在同一时间失效,导致所有请求直接打到数据库。
- Redis 中如何保证缓存与数据库的数据一致性?
- 常见的方法包括:
- 双写:同时写入缓存和数据库。
- 失效时间:为缓存设置合理的失效时间。
- 缓存更新策略:使用更新或删除策略保持缓存和数据库一致。
- 常见的方法包括:
大Key和热点Key
- Redis 中的 Big Key 问题是什么?如何解决?
- Big Key问题:大Key占用大量内存,影响Redis性能。
- 解决方法:
- 分片:将大Key拆分成多个小Key。
- 使用更合适的数据结构:例如使用Ziplist或Hash。
- 如何解决 Redis 中的热点 key 问题?
- 热点Key问题:某些Key被频繁访问,导致性能瓶颈。
- 解决方法:
- 数据分片:将热点Key分散到多个节点。
- 缓存层:在应用层或CDN层缓存热点Key。
- 布隆过滤器:减少不必要的查询。
内存淘汰策略
- Redis 中有哪些内存淘汰策略?
- 常见的内存淘汰策略包括:
- noeviction:不淘汰数据,达到最大内存限制时返回错误。
- allkeys-lru:使用LRU算法淘汰任意键。
- volatile-lru:使用LRU算法淘汰设置了过期时间的键。
- allkeys-random:随机淘汰任意键。
- volatile-random:随机淘汰设置了过期时间的键。
- volatile-ttl:优先淘汰TTL值最小的键。
- 常见的内存淘汰策略包括:
- Redis 中的内存碎片化是什么?如何进行优化?
- 内存碎片化:由于频繁的内存分配和释放,导致内存空间分散。
- 优化方法:
- 重启Redis:定期重启Redis实例,回收内存。
- 调整内存分配策略:使用更合理的内存分配策略。
- 使用内存碎片整理工具:使用第三方工具进行内存碎片整理。
分布式锁
Redis 中如何实现分布式锁?
- 可以使用Redis的
SETNX
命令实现简单的分布式锁。 - 示例:
if (redis.setnx("lock", "value")) { // 获取锁成功,执行业务逻辑 try { // 业务逻辑 } finally { // 释放锁 redis.del("lock"); } } else { // 获取锁失败 }
- 可以使用Redis的
分布式锁在未完成逻辑前过期怎么办?
- 可以在设置锁时设置一个合理的超时时间,并在释放锁时进行双重检查。
- 示例:
long expireTime = System.currentTimeMillis() + 30000; // 30秒超时 if (redis.setnx("lock", String.valueOf(expireTime))) { // 获取锁成功 } else { String existingExpireTime = redis.get("lock"); if (existingExpireTime != null && Long.parseLong(existingExpireTime) < System.currentTimeMillis()) { // 锁已过期,尝试重新获取锁 String currentExpireTime = redis.getSet("lock", String.valueOf(expireTime)); if (currentExpireTime.equals(existingExpireTime)) { // 成功获取锁 } else { // 获取锁失败 } } else { // 获取锁失败 } }
Redis 的 Red Lock 是什么?你了解吗?
- Red Lock是一种改进的分布式锁算法,通过多个Redis实例来提高锁的可靠性和可用性。
- 示例:
RedissonClient redisson = Redisson.create(); RLock lock = redisson.getLock("myLock"); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }
Redis 实现分布式锁时可能遇到的问题有哪些?
- 常见问题包括:
- 锁过期:锁在业务逻辑未完成前过期。
- 死锁:多个节点竞争锁导致死锁。
- 网络分区:网络问题导致锁无法正常释放。
- 常见问题包括:
说说 Redisson 分布式锁的原理?
- Redisson分布式锁:基于Redis实现的分布式锁,提供了丰富的功能和更高的可靠性。
- 原理:
- 可重入锁:支持可重入,多次获取锁不会阻塞。
- 公平锁:支持公平锁,按请求顺序获取锁。
- 锁过期保护:通过WatchDog机制自动续期,防止锁过期。
- 使用示例:
RedissonClient redisson = Redisson.create(); RLock lock = redisson.getLock("myLock"); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }
性能及应用场景
- Redis 通常应用于哪些场景?
- 常见的应用场景包括:
- 缓存:作为高速缓存层,提高数据访问速度。
- 消息队列:实现消息传递和任务队列。
- 会话存储:存储用户会话信息。
- 计数器:统计网站访问量、用户行为等。
- 排行榜:实现实时排行榜功能。
- 地理位置:存储和查询地理位置信息。
- 常见的应用场景包括:
快/源码设计
Redis 为什么这么快?
- Redis之所以快,主要有以下几个原因:
- 内存存储:数据存储在内存中,访问速度快。
- 单线程模型:避免了多线程间的上下文切换开销。
- 高效的内存管理:使用自定义的数据结构和内存分配算法。
- 网络通信优化:使用多路复用IO模型,处理大量并发连接。
- Redis之所以快,主要有以下几个原因:
Redis 源码中有哪些巧妙的设计,举几个典型的例子?
- 内存管理:使用自定义的内存分配器,减少内存碎片。
- 多路复用IO:使用epoll/kqueue等高性能IO多路复用技术。
- 事件驱动:使用事件驱动模型,处理大量并发连接。
- 数据结构优化:使用Ziplist、Quicklist等优化数据结构,减少内存占用。
性能优化
- Redis 性能瓶颈时如何处理?
- 常见的性能优化方法包括:
- 增加内存:扩大Redis实例的内存容量。
- 优化数据结构:选择合适的数据结构,减少内存占用。
- 使用Pipeline:减少网络往返次数,提高性能。
- 分片:将数据分散到多个Redis实例。
- 监控和调优:使用监控工具定期检查性能,进行调优。
- 常见的性能优化方法包括:
多线程和虚拟内存
为什么 Redis 设计为单线程?6.0 版本为何引入多线程?
- 单线程设计:避免多线程间的上下文切换开销,提高性能。
- 6.0版本引入多线程:为了处理IO密集型任务,如网络IO和文件IO,提高整体性能。
Redis 的虚拟内存(VM)机制是什么?
- 虚拟内存机制:允许Redis将部分数据换出到磁盘,减少内存占用。
- 工作原理:Redis将不常用的数据换出到磁盘,当需要访问时再换入内存。
高级功能和架构
主从复制
- Redis 主从复制的实现原理是什么?
- 主从复制通过主节点将数据同步到从节点。主节点将写操作记录到replication buffer中,从节点定期请求同步数据,主节点将数据发送给从节点。
- Redis 主从复制的常见拓扑结构有哪些?
- 常见的拓扑结构包括:
- 单主单从:一个主节点和一个从节点。
- 单主多从:一个主节点和多个从节点。
- 级联复制:从节点再作为其他从节点的主节点。
- 常见的拓扑结构包括:
集群和哨兵机制
Redis 集群的实现原理是什么?
- Redis集群通过分片(sharding)技术将数据分布到多个节点上。每个节点负责一部分数据,通过哈希槽(hash slot)来确定数据的归属。
Redis 集群会出现脑裂问题吗?
- Redis集群通过哨兵机制和多数派选举来避免脑裂问题。只有超过半数的节点同意,才能进行主从切换。
在 Redis 集群中,如何根据键定位到对应的节点?
- 通过哈希槽(hash slot)来定位键。每个键通过CRC16算法计算哈希值,然后取模得到哈希槽编号,根据哈希槽编号找到对应的节点。
Redis 的哨兵机制是什么?
- 哨兵机制用于监控和管理Redis集群的高可用性。哨兵节点会定期检查主节点的健康状况,如果主节点宕机,哨兵会自动进行主从切换。
Redis事务
- Redis 支持事务吗?如何实现?
- Redis支持事务,通过
MULTI
、EXEC
、DISCARD
和WATCH
命令来实现事务。 - MULTI:开始事务。
- EXEC:执行事务。
- DISCARD:取消事务。
- WATCH:监视键的变化,如果在事务执行前键被修改,事务将被取消。
- Redis支持事务,通过
Lua脚本
- Redis 的 Lua 脚本功能是什么?如何使用?
- Redis支持使用Lua脚本执行复杂的操作。通过
EVAL
命令执行Lua脚本,可以实现原子操作。 - 示例:
EVAL "return redis.call('INCR', KEYS[1])" 1 counter
- Redis支持使用Lua脚本执行复杂的操作。通过
Pipeline管道
- Redis 的 Pipeline 功能是什么?
- Pipeline功能允许客户端一次性发送多个命令,减少网络往返次数,提高性能。
- 示例:
redis.pipelined(jedis -> { jedis.set("key1", "value1"); jedis.set("key2", "value2"); });
Redis客户端
- 你在项目中使用的 Redis 客户端是什么?
- 常见的Redis客户端包括Jedis、Lettuce、Redisson等。
- Jedis:Java客户端,轻量级,使用广泛。
- Lettuce:Java客户端,支持Netty,性能较好。
- Redisson:Java客户端,提供了丰富的高级功能,如分布式锁。