介绍下Redis中的ZipList和他的级联更新问题

典型回答 ZipList是Redis中的一个数据结构,用来实现ZSet,他是一个压缩的数据结构,它的每个元素都是连续存储的,因此内存的使用非常紧凑。 以上是ZipList的结构。其中包含了: zlbytes:4字节,整个 ziplist 的字节数 zltail:4字节,最后一个 entry 的偏移量 zllen:2字节,entry 的数量 Entry:实际存储的数据项 zlend:1字节,结束标记 其中的Entry就是存储的数据项,他的结构是: prevlen:存储前一个 entry 的长度 前一个 entry 长度 < 254:1 字节 前一个 entry 长度 ≥ 254:5 字节(首字节固定 0xFE) encoding:内容编码(类型 + 长度) content:实际数据 ...

March 22, 2026 · 1 min · santu

介绍下Redis集群的脑裂问题?

典型回答 所谓脑裂,就像他的名字一样,大脑裂开了,一般来说就是指一个分布式系统中有两个子集,然后每个子集都有一个自己的大脑(Leader/Master)。那么整个分布式系统中就会存在多个大脑了,而且每个自己都认为自己是正常的,从而导致数据不一致或重复写入等问题。 脑裂的发生 Redis的脑裂问题可能发生在网络分区或者主节点出现问题的时候: 网络分区:网络故障或分区导致了不同子集之间的通信中断。 Master节点,哨兵和Slave节点被分割为了两个网络,Master处在一个网络中,Slave库和哨兵在另外一个网络中,此时哨兵发现和Master连不上了,就会发起主从切换,选一个新的Master,这时候就会出现两个主节点的情况。 主节点问题:集群中的主节点之间出现问题,导致不同的子集认为它们是正常的主节点。 Master节点有问题,哨兵就会开始选举新的主节点,但是在这个过程中,原来的那个Master节点又恢复了,这时候就可能会导致一部分Slave节点认为他是Master节点,而另一部分Slave新选出了一个Master 脑裂的危害 脑裂问题可能导致以下**问题**: 数据不一致:不同子集之间可能对同一数据进行不同的写入,导致数据不一致。 重复写入:在脑裂解决后,不同子集可能尝试将相同的写入操作应用到主节点上,导致数据重复。 数据丢失:新选出来的Master会向所有的实例发送slave of命令,让所有实例重新进行全量同步,而全量同步首先就会将实例上的数据先清空,所以在主从同步期间在原来那个Master上执行的命令将会被清空。 如何避免脑裂 那么如何防止脑裂的发生呢? Redis 已经提供了两个配置项可以帮我们做这个事儿,分别是 min-slaves-to-write 和 min-slaves-max-lag。 min-slaves-to-write:主库能进行数据同步的最少从库数量; min-slaves-max-lag:主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟秒数。 这两个配置项必须同时满足,不然主节点拒绝写入。在期间满足min-slaves-to-write和min-slaves-max-lag的要求,那么主节点就会被禁止写入,脑裂造成的数据丢失情况自然也就解决了。 举个例子: 假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 10s。 如果Master节点因为某些原因挂了 12s,导致哨兵判断主库客观下线,开始进行主从切换。 同时,因为原Master宕机了 12s,没有一个(min-slaves-to-write)从库能和原主库在 10s( min-slaves-max-lag) 内进行数据复制,这样一来,就因为不满足配置要求,原Master也就再也无法接收客户端请求了。 这样一来,主从切换完成后,也只有新主库能接收请求,这样就没有脑裂的发生了。 能彻底解决脑裂吗? 还是刚刚那个场景,假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 10s,并且down-after-milliseconds时间为8s,也就是说,如果8秒连不上主节点,哨兵就会进行主从切换。 但是,如果主从切换的过程需要5s时间的话,就会有问题。 Master节点宕机8s时,哨兵判断主节点客观下线,开始进行主从切换,但是这个过程一共需要5s。那如果主从切换过程中,主节点有恢复运行,即第9秒Master恢复了,而min-slaves-max-lag设置为10s那么主节点还是可写的。 那么就会导致9s~12s这期间如果有客户端写入原Master节点,那么这段时间的数据会等新的Master选出来之后,执行了slaveof之后导致丢失。 Redis脑裂可以采用min-slaves-to-write和min-slaves-max-lag合理配置尽量规避,但无法彻底解决,

March 22, 2026 · 1 min · santu

如何在 Redis Cluster 中执行 lua 脚本?

典型回答 ✅Redis Cluster 中使用事务和 lua 有什么限制? 因为 Redis Cluster 中,数据会被分片到多个节点上,跨节点的 lua 脚本是不支持的,所以就会失败。但是 Cluster 是很常见的场景,lua (以及事务)也是一个非常重要的用法,这个问题怎么解决呢? Hash Tag 在 Redis 的官方中,提到了一个 hash tags的功能: 如果我们想要执行 lua 脚本或者事务的时候,就需要确保多个相关的键应该存储在同一个节点上以便执行原子操作,**默认情况下,Redis 使用键的哈希值来决定将数据存储在哪个节点。**而Redis 中的 hashtag 就是一种可以让我们干预 hash 结果的机制。 Redis 中的 hashtag 是键名中用大括号 {} 包裹的部分。Redis 对大括号内的字符串计算哈希值,并基于这个哈希值将键分配到特定的节点。只有键名中包含大括号,且大括号内有内容时,大括号内的部分才会被用来计算哈希值。如果大括号为空或不包含任何字符,Redis 将整个键名用于哈希计算。 有了这个特性,我们就可以在设计键名时,可以将共享相同逻辑或数据集的键包含相同的 hashtag。就和我们在 MySQL 的分库分表中的基因法其实是类似的概念。 例如,如果你有多个与用户 ID 相关的键,可以使用 user:{12345}:profile 和 user:{12345}:settings 这样的命名方式,确保它们都位于同一个节点。这样他只会用{12345}进行 hash 算法,这样虽然他们是不同的key,但是分片之后的结果就可以在同一个节点上。这样就能执行事务或者 lua 脚本了。 其他方案 除了使用 Hash Tag 以外,还有一些其他的方案,也能实现,比如: 应用层处理:如果跨节点操作不可避免,可以在应用层通过分布式事务管理器或其他机制来协调多个节点的数据一致性。这通常需要复杂的逻辑和额外的开发工作。 拆分操作:尽量将需要事务处理的逻辑拆分成多个独立的、可以在单个节点上执行的小操作,从而避免跨节点事务的需求。 扩展知识 allow-cross-slot-keys 在 Redis 7.0.11中新增了一个命令:allow-cross-slot-keys ...

March 22, 2026 · 1 min · santu

如何基于Redis实现滑动窗口限流?

典型回答 滑动窗口限流是一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。它的工作方式类似于一个滑动时间窗口,在窗口内允许的操作数量是固定的,窗口会随着时间的推移不断滑动。 ✅什么是滑动窗口限流? 滑动窗口限流的主要优点是可以在时间内平滑地控制流量,而不是简单地设置固定的请求数或速率。这使得系统可以更灵活地应对突发流量或峰值流量,而不会因为固定速率的限制而浪费资源或降低系统性能。 利用Redis,我们就可以实现一个简单的滑动窗口限流的功能。因为滑动窗口和时间有关,所以很容易能想到要基于时间进行统计。 那么我们只需要在每一次有请求进来的时候,记录下请求的时间戳和请求的数据,然后在统计窗口内请求的数量时,只需要统计窗口内的被记录的数据量有多少条就行了。 在Redis中,我们可以基于ZSET来实现这个功能。假如我们限定login接口一分钟只能调用100次: 那么,我们就可以把login接口这个需要做限流的资源名作为key在redis中进行存储,然后value我们现在ZSET这种数据结构,把他的score设置为当前请求的时间戳,member的话建议用请求的详情的hash进行存储(或者UUID、MD5什么的),避免在并发时,时间戳一致出现score和memberv一样导致被zadd幂等的问题。 所以,我们实现滑动窗口限流的主要思想是:只保留在特定时间窗口内的请求记录,而丢弃窗口之外的记录。 主要步骤如下: 定义滑动窗口的时间范围,例如,窗口大小为60秒。 每次收到一个请求时,我们就定义出一个zset然后存储到redis中。 然后再通过ZREMRANGEBYSCORE命令来删除分值小于窗口起始时间戳(当前时间戳-60s)的数据。 最后,再使用ZCARD命令来获取有序集合中的成员数量,即在窗口内的请求量。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import redis.clients.jedis.Jedis; public class SlidingWindowRateLimiter { private Jedis jedis; private String key; private int limit; public boolean allowRequest(String key) { //当前时间戳 long currentTime = System.currentTimeMillis(); //窗口开始时间是当前时间减60s long windowStart = currentTime - 60 * 1000; //删除窗口开始时间之前的所有数据 jedis.zremrangeByScore(key, "-inf", String.valueOf(windowStart)); //计算总请求数 long currentRequests = jedis.zcard(key); //窗口足够则把当前请求加入 if (currentRequests < limit) { jedis.zadd(key, currentTime, String.valueOf(currentTime)); return true; } return false; } } 以上代码在高并发情况下,可能会存在原子性的问题,需要考虑加事务或者lua脚本: ...

March 22, 2026 · 2 min · santu

如何解决Redis和数据库的一致性问题?

典型回答 ✅什么情况下会出现数据库和缓存不一致的问题? 为了保证Redis和数据库的数据一致性,肯定是要缓存和数据库双写了。 一般来说,在业内有3种比较常见的具体方案: 1、先更新数据库, 再删除缓存。 2、延迟双删:先删除缓存,再更新数据库,再删除一次缓存 3、cache-aside:更新数据库,基于 binlog 监听进行缓存删除 方案 优点 缺点 适合场景 先更新数据库,后删除缓存 简单 删缓存失败,会导致数据不一致 95%一般场景都适合,尤其是并发量不大,或者对一致性要求不太高的。 延迟双删 数据一致性保证更好 延迟时间不好控制,太短了没用,太长了也会导致不一致时间长 对一致性要求高,并发量大的场景。 cache-aside 解耦、一致性有保障 复杂,需要引入新的组件 适合大厂,有完善的中间件支持,并发高,一致性要求高的场景。 扩展知识 为什么删缓存而不是更新 为了保证数据库和缓存里面的数据是一致的,很多人会在做数据更新的时候,会同时更新缓存里面的内容。但是我其实告诉大家,应该优先选择删除缓存而不是更新缓存。 首先,我们暂时抛开数据一致性的问题,单独来看看更新缓存和删除缓存的复杂的问题。 我们放到缓存中的数据,很多时候可能不只是简单的一个字符串类型的值,他还可能是一个大的JSON串,一个map类型等等。 举个例子,我们需要通过缓存进行扣减库存的时候,你可能需要从缓存中查出整个订单模型数据,把他进行反序列化之后,再解析出其中的库存字段,把他修改掉,然后再序列化,最后再更新到缓存中。 可以看到,更新缓存的动作,相比于直接删除缓存,操作过程比较的复杂,而且也容易出错。 还有就是,在数据库和缓存的一致性保证方面,删除缓存相比更新缓存要更简单一点。 在"写写并发"的场景中,如果同时更新缓存和数据库,那么很容易会出现因为并发的问题导致数据不一致的情况。如: 先写数据库,再更新缓存 W W 写数据库,更新成20 写数据库,更新成10 写缓存,更新成10 写缓存,更新成20(数据不一致) 先更新缓存,后写数据库: W W 写缓存,更新成20 写缓存,更新成10 写数据库,更新成10 写数据库,更新成20(数据不一致) 但是,如果是做缓存的删除的话,在写写并发的情况下,缓存中的数据都是要被清除的,所以就不会出现数据不一致的问题。 ...

March 22, 2026 · 1 min · santu

Redis 与 Memcached 有什么区别?

典型回答 Redis 和 Memcached 都是常见的缓存服务器,它们的主要区别包括以下几个方面: 数据结构不同:Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,而 Memcached 只支持简单的键值对存储。 持久化方式不同:Redis 支持多种持久化方式,如 RDB 和 AOF,可以将数据持久化到磁盘上;而 Memcached 不支持持久化。 数据分片方式不同:Redis 使用哈希槽分片,可以实现数据的自动分片和负载均衡;而 Memcached 只能手动分片。 处理数据的方式不同:Redis 使用单线程处理数据请求,支持事务、Lua 脚本等高级功能;而** Memcached 使用多线程处理数据请求**,只支持基本的 GET、SET 操作。 协议不同:Redis 使用自己的协议,支持多个数据库,可以使用密码进行认证;而 Memcached 使用文本协议,只支持一个默认数据库。 内存管理方式不同:Redis 的内存管理比 Memcached 更加复杂,支持更多的内存优化策略。 综上所述,Redis 和 Memcached 有着不同的设计理念和应用场景。Redis 适用于数据结构复杂、需要高级功能和数据持久化的场景;而 Memcached 则适用于简单的键值存储场景。

March 22, 2026 · 1 min · santu

Redis 使用什么协议进行通信?

典型回答 **Redis 使用自己设计的一种文本协议进行客户端与服务端之间的通信——RESP(REdis Serialization Protocol),**这种协议简单、高效,易于解析,被广泛使用。 RESP 协议基于 TCP 协议,采用请求/响应模式,每条请求由多个参数组成,以命令名称作为第一个参数。请求和响应都以行结束符(\r\n)作为分隔符,具体格式如下: 1 2 3 4 5 6 *<number of arguments>\r\n $<length of argument 1>\r\n <argument data>\r\n ... $<length of argument N>\r\n <argument data>\r\n 其中, 表示参数个数, 表示参数数据的长度, 表示参数数据。参数可以是字符串、整数、数组等数据类型。 例如,以下是一个 Redis 协议的示例请求和响应: 请求: 1 2 3 4 5 6 7 *3\r\n $3\r\n SET\r\n $5\r\n mykey\r\n $7\r\n myvalue\r\n 响应: 1 +OK\r\n 上面的请求表示向 Redis 服务器设置一个名为 “mykey” 的键,值为 “myvalue”。响应返回 “+OK” 表示操作成功。 ...

March 22, 2026 · 1 min · santu

Redis 支持哪几种数据类型?

Redis 中支持了多种数据类型,其中比较常用的有五种: 字符串(String) 哈希(Hash) 列表(List) 集合(Set) 有序集合(Sorted Set) 另外,Redis中还支持一些高级的数据类型,如:Streams、Bitmap、Geospatial以及HyperLogLog 字符串 ✅Redis为什么要自己定义SDS? 有序集合 Redis中的ZSet是怎么实现的? Streams Redis 5.0中的 Stream是什么? GEO 什么是GEO,有什么用? 扩展知识 使用场景 ✅Redis的zset实现排行榜,实现分数相同按照时间顺序排序,怎么做? ✅如何用Redis实现朋友圈点赞功能? ✅如何实现"查找附近的人"功能? ✅如何基于Redis实现滑动窗口限流?

March 22, 2026 · 1 min · santu

Redis为什么要自己定义SDS?

典型回答 Redis是一种KV的存储结构,他的key是字符串类型,值也支持字符串,所以字符串是redis中最常见的一个类型了。Redis自己本身是通过C语言实现的,但是他并没有直接使用C语言中的字符数组的方式来实现字符串,而是自己实现了一个SDS(Simple Dynamic Strings),即简单动态字符串,这是为什么呢? 首先,因为字符串在Redis中使用实在是太广泛了 ,所以对他的基本要求就有两点,第一就是要支持任意字符的存储,第二就是各种操作需要高效。 接着我们看看C语言中字符串的实现方式有什么问题呢?很多人可能都忘了,我帮大家回忆一下,C语言中,字符串是通过字符数组实现的,底层呢是开辟了一块连续的空间,依次存放字符串中的每一个字符。为了表示字符串的结束,他会在字符数组的最后一个字符处记录\0, 也就是说,在C语言中,当识别到字符数组中的\0字符的时候,就认为字符串结束了,那么这么做会带来哪些问题呢? 就是这样实现的字符串中就不能保存任意内容了,至少**\0**就不行,因为遇到他的时候就直接截断了,这肯定是接受不了的。 还有就是因为C中的字符串以**\0**作为识别字符串结束的方式,所以他的字符串长度判断、字符串追加等操作,都需要从头开始遍历,一直遍历到**\0**的时候再返回长度或者做追加。这就使得字符串相关的操作效率都很低。 那么,想要解决上面的两个问题要怎么办呢?那就是在用字符数组表示字符串的同时,在这个字符串中增加一个表示分配给该字符数组的总长度的**alloc**字段,和一个表示字符串现有长度的**len**字段。这样在获取长度的时候就不依赖**\0**了,直接返回**len**的值就行了。 还有呢,就是在做追加操作的时候,只需要判断新追加的部分的len加上已有的len是否大于alloc,如果超过就重新再申请新空间,如果没超过,就直接进行追加就行了。 还有很多其他操作,比如复制、比较等都可以使用类似的思想高效的操作。

March 22, 2026 · 1 min · santu

Redis为什么被设计成是单线程的?

典型回答 我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。 所以说,Redis中并不是没有多线程模型的,早在Redis 4.0的时候就已经针对部分命令做了多线程化。 一个计算机程序在执行的过程中,主要需要进行两种操作分别是读写操作和计算操作。 其中读写操作主要是涉及到的就是I/O操作,其中包括网络I/O和磁盘I/O。计算操作主要涉及到CPU。 而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。 之所以Redis没有用多线程处理IO操作,主要是因为,Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈。 扩展知识 Redis为什么最开始被设计成单线程的? Redis作为一个成熟的分布式缓存框架,它由很多个模块组成,如网络请求模块、索引模块、存储模块、高可用集群支撑模块、数据操作模块等。 很多人说Redis是单线程的,就认为Redis中所有模块的操作都是单线程的,其实这是不对的。 我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。 所以说,Redis中并不是没有多线程模型的,早在Redis 4.0的时候就已经针对部分命令做了多线程化。 那么,为什么网络操作模块和数据存储模块最初并没有使用多线程呢? 这个问题的答案比较简单!因为:“没必要!” 为什么没必要呢?我们先来说一下,什么情况下要使用多线程? 多线程适用场景 一个计算机程序在执行的过程中,主要需要进行两种操作分别是读写操作和计算操作。 其中读写操作主要是涉及到的就是I/O操作,其中包括网络I/O和磁盘I/O。计算操作主要涉及到CPU。 而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。 那么,Redis需不需要通过多线程的方式来提升提升I/O的利用率和CPU的利用率呢? 首先,我们可以肯定的说,Redis不需要提升CPU利用率,因为Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈。 网上很多人都在说“CPU不是Redis的瓶颈”,但是这句话怎么理解呢? 其实,Redis本质上是一个内存数据库,他的性能瓶颈主要在于: 1、内存容量与内存性能 2、网络带宽与延迟 Redis的主要操作是内存访问,并不是"计算",更别说复杂计算了,这些都是不耗费CPU的。所以,CPU并不是Redis的瓶颈。 但是我认为这句话也不完全对,这也是建立在Redis主要是通过集群来对外提供服务的能力扩展的方式,如果他想把多核CPU的能力发挥起来的话,比如参考阿里巴巴的Tair和Dragonfly,他们也是缓存系统,对标的也是Redis,但是他们也是多线程的。 所以,通过多线程技术来提升Redis的CPU利用率这一点是完全没必要的。 那么,使用多线程技术来提升Redis的I/O利用率呢?是不是有必要呢? Redis确实是一个I/O操作密集的框架,他的数据操作过程中,会有大量的网络I/O和磁盘I/O的发生。要想提升Redis的性能,是一定要提升Redis的I/O利用率的,这一点毋庸置疑。 但是,提升I/O利用率,并不是只有采用多线程技术这一条路可以走! 多线程的弊端 我们在很多文章中介绍过一些Java中的多线程技术,如内存模型、锁、CAS等,这些都是Java中提供的一些在多线程情况下保证线程安全的技术。 线程安全:是编程中的术语,指某个函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 和Java类似,所有支持多线程的编程语言或者框架,都不得不面对的一个问题,那就是如何解决多线程编程模式带来的共享资源的并发控制问题。 虽然,采用多线程可以帮助我们提升CPU和I/O的利用率,但是多线程带来的并发问题也给这些语言和框架带来了更多的复杂性。而且,多线程模型中,多个线程的互相切换也会带来一定的性能开销。 所以,在提升I/O利用率这个方面上,Redis并没有采用多线程技术,而是选择了多路复用 I/O技术。 小结 Redis并没有在网络请求模块和数据操作模块中使用多线程模型,主要是基于以下四个原因: 1、Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU 2、使用单线程模型,可维护性更高,开发,调试和维护的成本更低 3、单线程模型,避免了线程间切换带来的性能开销 4、在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率 还是要记住:Redis并不是完全单线程的,只是有关键的键值对读写是由一个线程完成的。 Redis的多路复用 为什么Redis设计成单线程也能这么快? 为什么Redis 6.0 引入多线程 为什么Redis 6.0引入了多线程?

March 22, 2026 · 1 min · santu

留言给博主