Redis的ZipList、SkipList和ListPack之间有什么区别?

典型回答 之所以把他们三个放在一起比较,主要是因为他们都是实现ZSet的主要数据结构。 ✅Redis中的Zset是怎么实现的? 在Redis 7.0之前,ZSet主要靠ZipList和SkipList实现。而在Redis 7.0开始,ZipList已经被ListPack给替代了,也就是说Redis 7.0之后,ZSet主要靠ListPack和SkipList实现。 这里SkipList是一直在的,先介绍他。 SkipList SkipList就是跳表。跳表是一种在链表的基础上增加多层索引的结构。跳表的结构通过多层索引链表来提高查找效率,相比于传统的链表,它能够在对数时间内完成元素的查找、插入和删除。 跳表的优势就是插入、删除、查找操作都可以在 O(log N) 的时间复杂度内完成。并且支持高效的范围查询,如 **ZRANGEBYSCORE**、**ZRANGE**** 等。** 但是跳表有个缺点,那就是他的数据结构决定了他的内存占用更高。每个元素有多个指针来维护多层链表,导致内存开销更大。 ZipList 而ZipList是一个压缩的数据结构,它的每个元素都是连续存储的,因此内存的使用非常紧凑。与其他数据结构相比,ZipList在小规模数据存储时显著减少了内存占用。 但是ZipList也有缺点,正因为他是紧凑的线性结构,所以如果在 ZipList 中查找一个元素时,可能需要遍历整个列表,同理插入和删除操作也是线性的。所以ZipList的插入、删除和查找操作的时间复杂度通常是 O(N)。相对来说是比较慢的。 这也是为什么Redis会在元素数量比较少的时候用ZipList,而在数据量大了之后转成SkipList的原因。 ✅ZSet为什么在数据量少的时候用ZipList,而在数据量大的时候转成SkipList? 而且,因为ZipList的结构导致了,它在极端情况下可能会出现一种级联更新的问题: ✅介绍下Redis中的ZipList和他的级联更新问题 所以就有了ListPack ListPack ListPack是 Redis 在 5.0 版本引入的一种新的内存高效数据结构(在Redis 7.0正式替代ZipList用在ZSet中),它是为了解决 ziplist 和 skiplist 在一些场景下的不足而提出的。 listpack 是一种 适用于小型有序集合、列表和哈希的压缩数据结构,它比 ziplist 更加灵活、适应性更强,并且提供了对大数据量的更好支持。 更重要的事,ListPack通过创新的数据结构方案,避免了级联更新的问题。 ✅Redis中的ListPack是如何解决级联更新问题的? 对比总结 特性 SkipList (跳表) ZipList (压缩列表) ListPack (紧凑列表) 设计目标 高效范围查询和有序访问 内存紧凑,减少碎片 解决级联更新问题 内存布局 多层链表结构 连续内存块 连续内存块 查询复杂度 O(log N) O(n) O(n) 插入复杂度 O(log N) O(1)~O(n²) (级联更新) O(1) (无级联更新) 删除复杂度 O(log N) O(1)~O(n²) (级联更新) O(1) (无级联更新) 内存占用 高 (有指针开销) 低 极低 版本支持 所有版本 Redis ≤6.2 Redis ≥5.0 (7.0+默认) 核心优势 高效有序访问 小数据内存优化 无级联更新+内存紧凑 主要缺点 内存占用高 级联更新 范围查询效率低 应用场景 有序集合(ZSet) 小规模Hash/Set/ZSet 全类型小规模存储

March 22, 2026 · 1 min · santu

了解Redis的内存碎片吗?

典型回答 所谓内存碎片,就是被分配给Redis的内存空间,但是实际上并没有被用到的部分。(这和MySQL的内存碎片还是有些差异的,MySQL内存碎片指的是由于数据的插入、更新和删除操作导致的存储空间不连续和浪费。) Redis的内存碎片形成的主要原因是内存在分配的时候,没办法做到精准的按需分配。 Redis支持libc、jemalloc、tcmalloc多种内存分配器来分配内存,默认使用jemalloc。他是按固定大小(如 8B、16B、32B…4KB)划分内存页。当存储的数据大小与分配器提供的块不完全匹配时,会产生剩余空间。例如申请 5B 数据可能分配 8B,剩余 3B就成为碎片。 而且jemalloc 内部会缓存未使用的内存块(目的是为了快速满足后续的内存分配请求,避免频繁向操作系统申请或释放内存,提升性能)除非显式调用 malloc_trim 或 Redis 的 MEMORY PURGE,这些空间不会释放给操作系统;所以 RSS(物理内存) 不会下降,即使数据删除了。 另外,一些数据结构自动扩容也会导致碎片,Redis 内部有些结构(如哈希表、列表、集合)会自动扩容。在扩容时,可能会出现申请了新内存,但是旧内存不释放的情况,比如hash表的渐进式rehash过程: ✅什么是Redis的渐进式rehash 查看碎片情况 1 2 3 4 5 6 7 8 INFO memory # Memory used_memory:1000000 used_memory_human:976.56K used_memory_rss:1200000 used_memory_rss_human:1.14M ... mem_fragmentation_ratio:1.20 used_memory:表示Redis为了保存数据实际申请使用的内存空间。 used_memory_rss:表示操作系统实际分配给Redis的物理内存空间,其中包含了内存空间碎片。 mem_fragmentation_ratio:表示Redis当前的内存碎片率。等于used_memory_rss/used_memory 大于等于1但小于等于1.5,这种情况是合理的。 大于1.5,表明内存碎片率已经超过了50%。说明碎片比较多了。 ...

March 22, 2026 · 1 min · santu

Redisson里面的锁是如何实现可重入的?

典型回答 所谓可重入,就是同一个线程在运行过程中,如果多次拿同一把锁,需要让他可以获取成功。可重入的锁可以有效的避免死锁的问题。 这种题,就是讲源码最有说服力了。(这句话好像在哪见过,确实,Redisson讲不误删的也有这句话,而且下面的代码也都一样的。因为这俩问题是一段代码解决的。https://www.yuque.com/hollis666/ec96i7/ucarmoyv1belggn0) 先来看Redisson加锁的核心lua相关的代码,在RedissonLock#tryLockInnerAsync中。 1 2 3 4 5 6 7 8 9 10 11 <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { return evalWriteSyncedNoRetryAsync(getRawName(), LongCodec.INSTANCE, command, "if ((redis.call('exists', KEYS[1]) == 0) " + "or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "return redis.call('pttl', KEYS[1]);", Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId)); } 这段lua脚本执行之后,最终会在Redis中存一个Hash结构,key就是我们指定的加锁的key的名字,比如Hollis666,然后存储的hash的key是一个真正的lockName(不展开了,在介绍误删的文章中有),存储的值是这个锁被重入的次数。 ...

March 22, 2026 · 2 min · santu

留言给博主