基础
什么是Redis
Redis是一个使用 C 语言编写的,开源的高性能非关系型(NoSQL)的键值对数据库。
redis为什么这么快
- 纯内存操作, 比磁盘要快
- 单线程操作, 避免了频繁的线程上下文切换
- 采用了非阻塞I/O多路复用机制
- Redis 内置了多种优化过后的数据结构实现. 如跳表
redis高性能怎么实现的?
性能一般基于两个方面, 一个是计算操作, 一个是读写操作.
- 计算操作, 由于redis是基于内存的, 所以计算操作(命令执行)很快, 然后单线程执行命令比较快. 所以redis的性能瓶颈不在计算操作, 而在于网络io
- 单线程执行为啥快,那是因为单线程执行的话你就不需要加 锁来控制了,锁是很重的很耗费资源.
- 多线程的话, 也需要线程上下文切换, 这样的话性能也会降低.
- 读写操作, 无非就是网络io和磁盘io
- 磁盘io优化: rdb持久化时, 会创建一个子进程来生成 RDB 文 件,这样可以避免主线程的阻塞. 这也是一种写入时复制的思想
- 网络io优化:
- io多路复用: select epoll, 可以同时监听多个socket连接 请求
- 事件派发机制: 有很多不同性质的socket, redis有不同的 handler来处理这些socket事件, redis6.0使用多线程来处理这些handler
Redis常用的数据结构有哪些
-
5种基础数据类型:String(字符串)、List(列表)、Set(集合)、 Hash(散列)、Zset(有序集合)
-
4种特殊数据类型:HyperLogLogs(基数统计)、Bitmap(位存 储)、geo(地理位置) 、stream(消息队列)
redis每种数据类型的使用场景
- String: 最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。
- Hash: 这里 Value 存放的是结构化的对象,比较方便的就是操作 其中的某个字段. 如果单纯做对象的存储, 那么直接使用string即可, 如果需要对对象中的字段做操作, 那么用hash.
- List:
- list支持两端存取, 不能从中间取. 若从一侧存取, 则是栈. 若从 异侧存取, 则是队列.
- 使用 List 的数据结构,可以做简单的消息队列的功能。另 外,可以利用 lrange 命令,做基于 Redis 的分页功能,性能 极佳,用户体验好
- Set: 因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。我们的系统一般都是集群部署,使用 JVM 自带的 Set 比较麻烦。另外,就是利用交集、并集、差集等操作,交集就可 以计算共同喜好,共同关注, 共同好友, 共同粉丝等功能. 差集就可以实现好友推荐, 音乐推荐功能.
- Zset: 多了一个权重参数 Score,集合中的元素 能够按 Score 进行排列。可以做排行榜应用, 取 TOP(N) 操作. 也可以做优先级任务队列.
- geo: 地理位置计算
- bitmap: 可以用来做布隆过滤器, 或者统计签到次数等
- HyperLogLog: 统计页面UV
- Stream: 做一个简单的消息队列
Zset底层的数据结构
Zset类型的底层数据结构是由压缩列表或跳表实现的:
- 如果有序集合的元素个数小于128个,并且每个元素的值小于64 字节时,Redis的Zset底层会使用压缩列表
- 如果有序集合的元素不满足上面的条件,Redis会使用跳表作为 Zset类型的底层数据结构
介绍一下跳表
链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N)
于是就出现了跳表。跳表是在链表基础上改进过来的,本质是一种多层的有序链表,可以快读定位数据,实现O(log n)高效查询
redis的常见应用场景
- 缓存:通过将热点数据存储在内存中,可以极大地提高访问速 度,减轻数据库压力
- 排行榜:Redis的有序集合结构非常适合用于实现排行榜和排名 系统,可以方便地进行数据排序和排名
- 分布式锁:Redis的特性可以用来实现分布式锁,确保多个进程 或服务之间的数据操作的原子性和一致性
- 计数器: 由于Redis的原子操作和高性能,它非常适合用于实现计 数器和统计数据的存储,如网站访问量统计、点赞数统计等
- 消息队列:Redis的发布订阅功能使其做为一个轻量级的消息队列,可以用来实现发布和订阅模式. (Redis高版本新增Stream数据结构, 可以直接作为一个轻量级MQ使用)
线程模型
Redis 为什么设计成单线程的
- 多线程处理会涉及到锁,并且多线程处理会涉及到线程切换而消 耗 CPU。采用单线程,避免了不必要的上下文切换和竞争条件
- 其次 CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内 存或者网络带宽。所以redis网络io操作采用了多线程
Redis一定是单线程的吗
- Redis 单线程指的是执行命令是由一个主线程来完成的
- 但是Redis 在磁盘io(持久化操作)和网络io会使用多线程来处理, 是因为这些任务的操作都是很耗时的,如果把这些任务都放在主 线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法 处理后续的请求了
Redis 6.0 之后为什么引入了多线程
Redis 执行命令一直是单线程模型
因为Redis 的性能瓶颈有时会出现在网络 I/O 的处理上, 故在 Redis 6.0 版本之后,采用了多个 I/O 线程来处理网络请求,提高网络 I/O 的并行度 。但是对于命令的执行,Redis 仍然使用单线程来处理
内存与持久化
redis的过期策略
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字 典」保存了数据库中所有 key 的过期时间
过期策略: 定期删除+惰性删除
- 定期删除就是Redis默认每隔 100ms 就 随机抽取 一些设置了过期时间的key,检测这些key是否过期,如果过期了就将其删除
- 惰性删除是在你要获取某个key 的时候,redis会先去检测一下这 个key是否已经过期,如果没有过期则返回给你,如果已经过期 了,那么redis会删除这个key,不会返回给你
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致 大量过期 key 堆积在内存里,然后就 Out of memory 了。
怎么解决这个问题呢?Redis 内存淘汰机制
为什么Key过期不立即删除
在过期key比较多的情况下,删除过期key可能会占用相当一部分 CPU时间,在内存不紧张但CPU时间 紧张的情况下,将CPU时间用于删除和当前任务无关的过期键上, 会对服务器的响应时间和吞吐量造成影响
内存淘汰机制
内存淘汰策略允许Redis在内存资源紧张时,根据一定的策略主动删除一些键值对,以释放内存空间并保持系统的稳定性。
noeviction(不淘汰策略)
当内存不足以容纳新写入数据时,Redis 将新写入的命令返回错误。这个策略确保数据的完整性,但会导致写入操作失败。
volatile-lru(最近最少使用)
从设置了过期时间的键中选择最少使用的键进行删除。该策略优先删除最久未被访问的键,保留最常用的键。
volatile-ttl(根据过期时间优先)
从设置了过期时间的键中选择剩余时间最短的键进行删除。该策略优先删除剩余时间较短的键,以尽量保留剩余时间更长的键。
volatile-random(随机删除)
从设置了过期时间的键中随机选择一个键进行删除。
allkeys-lru(全局最近最少使用)
从所有键中选择最少使用的键进行删除。无论键是否设置了过期时间,都将参与淘汰。
allkeys-random(全局随机删除)
从所有键中随机选择一个键进行删除。
Redis持久化机制
RDB:
按照一定的时间周期策略把内存的数据以快照的形式保存到硬 盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb.
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave, 他们的区别就在于是否在「主线程」里执行:
- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操 作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会 阻塞主线程;
- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样 可以避免主线程的阻塞;执行 bgsave 过程中,Redis 依然可以 继续处理操作命令的,也就是数据是能被修改的。写时复制技术 (Copy-On-Write)
AOF:
Redis会将每一个收到的写命令追加到文件最后,类似于 MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
-
AOF保存的数据更加完整, 但是性能相比RDB稍差
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式
shappendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 appendfsync everysec #每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘 appendfsync no #让操作系统决定何时进行同步 -
为了兼顾数据和写入性能,可以用 appendfsync everysec , 让 Redis 每秒同步一次 AOF 文件,对Redis性能影响不大.
-
即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据
混合持久化:
Redis 4.0 提出混合使用 AOF 日志和内存快照,也叫混合持久化。
- 当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完 成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件 替换旧的的 AOF 文件。
- 使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数 据,后半部分是 AOF 格式的增量数据。
- 好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快
- 加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的 内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命 令,可以使得数据更少的丢失。
故一般都会用混合持久化方式.
缓存问题
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在 ,这样缓存永远不会生效,如果不断发起这样的请求,这些请求都会打到数据库,给数据库带来巨大压力。
解决方案:
- 缓存空对象:对于查询结果为空的情况,也将其缓存起来,但使用较短的过期时间,防止攻击者利用同样的 key 进行频繁攻击。
- 参数校验:在接收到请求之前进行参数校验,判断请求参数是否合法。
- 布隆过滤器:判断请求的参数是否存在于缓存或数据库中。
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问 并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:
- 合理的过期时间:设置热点数据永不过期,或者设置较长的过期时间,以免频繁失效。
- 使用互斥锁:保证同一时间只有一个线程来查询数据库,其他线程等待查询结果。
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机(同一时间,缓存大面积过期失效),导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值(让失效时间离散分布,确保Key不会在同一时间大量失效)
- 给缓存业务添加降级限流策略(比如快速失败机制,让请求尽可能打不到数据库上)
- 给业务添加多级缓存(使用本地缓存+Redis缓存,降低Redis压力)
- 利用Redis集群提高服务的可用性(主从集群、哨兵机制)
数据库和缓存双写一致性问题
数据库与缓存双写一致性问题的本质原因是:在分布式系统中,数据库和缓存是两个独立的存储系统,它们之间的操作无法保证原子性 。因此,我们只能接受"最终一致性",并设计策略将不一致时间窗口最小化。
缓存旁路模式(Cache Aside Pattern)
- 读操作:先从缓存读取数据,如果缓存未命中(缓存失效),再从数据库读取数据,然后将数据写入缓存。
- 写操作:先更新数据库,再删除缓存。
java
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
// 读操作:使用 @Cacheable 实现缓存
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
}
// 写操作:使用 @CacheEvict 实现缓存删除
@Transactional
// CacheEvict 默认在方法执行后执行(beforeInvocation = false)
@CacheEvict(value = "products", key = "#product.id") // 先更新数据库,再删除缓存
public Product updateProduct(Product product) {
return productRepository.save(product);
}
}
延时双删
先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉,从而把数据保持一致。
队列 + 重试机制
- 更新数据库数据;
- 缓存因为种种问题删除失败
- 将需要删除的key发送至消息队列
- 自己消费消息,获得需要删除的key
- 继续重试删除操作,直到成功

异步更新缓存(基于订阅binlog的同步机制)
MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
使用阿里的一款开源框架
canal,通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。MQ消息中间可以采用
RocketMQ来实现推送。

Redis的高可用-主从&Sentinel&Cluster
Redis实现高可用,在于提供多个节点,通常有三种部署模式:主从模式 ,哨兵模式 ,集群模式。
主从模式
主从模式就是,部署多台Redis节点,其中只有一台节点是主节点(master),其他的节点都是从节点(slave)。主从模式实现读写分离,只有master节点提供数据的事务性操作(增删改),slave节点只提供读操作。所有slave节点的数据都是从master节点同步过来的,该模式的结构图如下:

上图是一种简单的主从结构模式,它的slave节点都挂在master节点上,这样做的优点是slave节点与master节点的数据延迟较小。缺点是如果slave节点数量很多,master同步一次数据的时间花费较长。可以只在master节点下只挂载一个slave节点,其他节点挂载在这个salve节点上,数据同步经过传递完成,如下图所示。Redis和大部分中间件的主从模式中的数据同步都是由slave节点主动发起的,这样可以减少master的性能消耗。

高可用原理和优点:主从模式做到了数据冗余,数据拥有备份,当主节点发生故障时,可以选择一个slave节点继续提供服务(但是主从模式没有解决怎么选择slave节点作为master节点的方法,无法保证高可用)。
优点:
- 实现读写分离,降低master节点的读数据压力,提高系统的性能。写操作交给master节点,读操作交给slave节点,提供多个副本
- 配置简单,容易搭建。只需要在slave节点上维护master节点的地址信息就可实现。
缺点:
- 当master节点宕机时,由于无法选择哪一个slave节点当master节点,无法保证高可用。
- 所有的写数据的压力都集中在master节点,没有解决master节点写的压力。
哨兵模式
在主从模式中,一旦master节点发生宕机,为了保证高可用,需要找一个slave节点作为新的master节点。谁来确定宕机,选择哪一个slave节点,这些问题都没有解决。哨兵(sentinal)模式则是为了解决这些问题而产生的,它用于对主从模式中每个节点进行监控 ,当出现故障时通过投票机制,选择新的master节点,并将所有的slave节点连接到master节点,架构如下图所示。

哨兵模式有三个作用:监控、通知和自动故障转移。
- 监控(Monitoring):不断地检查master和salve是否运行正常。master存活检测、master和slave运行情况检测。
- 通知(Notification):当被监控的某个节点出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障转移(Automatic failover):断开master与slave之间的连接,选取一个salve作为master,将其他slave连接到新的master,并告知客户端新的节点地址。
选举过程:
- 每个在线的哨兵节点都可以成为领导者,每个哨兵节点会向其它哨兵发is-master-down-by-addr命令,征求判断并要求将自己设置为领导者;
- 当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;
- 如果哨兵发现自己在选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举。
优点:哨兵机制,保证高可用。能够监控各个节点运行状况,进行自动故障转移。
缺点:
- 中心化集群实现方式,基于主从模式,切换节点时,会发生数据的丢失。
- 集群里所有节点保存的都是全量数据,浪费内存空间,没有真正实现分布式存储。数据量过大时,主从同步严重影响master的性能。
- 数据写的操作都集中在master上,仍然没有解决master写数据的压力。
集群模式
哨兵模式基本已经实现了高可用,但是每个节点都存储相同的内容,很浪费内存。而且,哨兵模式没有解决master写数据的压力。为了解决这些问题,就有了集群模式,实现分布式存储,每个节点存储不同的内容。集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master宕机了,服务还可以正常地提供,架构如下图所示:

集群模式中数据通过数据分片的方式被自动分割到不同的master节点上,每个Redis集群有16384个哈希槽,进行set操作时,每个key会通过CRC16校验后再对16384取模来决定放置在哪个槽。数据在集群模式中是分开存储的,那么节点之间想要知道其他节点的状态信息,包括当前集群状态、集群中各节点负责的哈希槽、集群中各节点的master-slave状态、集群中各节点的存活状态等是通过建立TCP连接,使用gossip协议来进行集群信息传播。
故障判断方法:判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会定期的向其他节点发送ping命令,通过有没有收到回复来判断其他节点是否已经下线。具体方法是采用半数选举机制,当A节点发现目标节点疑似下线,就会向集群中的其他节点散播消息,其他节点就会向目标节点发送命令,判断目标节点是否下线。如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线。
优点:
- 无中心结构,部署简单。所有的Redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
- 可扩展性,可扩展master节点,释放单个master的写数据压力,节点可动态添加或删除。
- 能够实现自动故障转移,节点之间通过gossip协议交换状态信息,用投票机制完成slave到master的角色转换。
Redis事务
概念: 可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞。
常用命令:
- multi: 开启一个事务,multi 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中。
- exec: 执行队列中所有的命令
- discard: 中断当前事务,然后清空事务队列并放弃执行事务
- watch key1 key2 ... :监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
开启 multi 之后,命令语法导致执行错误,会放弃当前所有队列中的命令。
开启 multi 之后,命令逻辑执行错误,会主动忽略报错语句,继续执行后续命令。