learn.lianglianglee.com/专栏/Redis 核心原理与实战/10 列表使用与内部实现原理.md.html
2022-05-11 18:57:05 +08:00

1413 lines
30 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>10 列表使用与内部实现原理.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Redis 核心原理与实战/01 Redis 是如何执行的.md.html">01 Redis 是如何执行的.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/02 Redis 快速搭建与使用.md.html">02 Redis 快速搭建与使用.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/03 Redis 持久化——RDB.md.html">03 Redis 持久化——RDB.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/04 Redis 持久化——AOF.md.html">04 Redis 持久化——AOF.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/05 Redis 持久化——混合持久化.md.html">05 Redis 持久化——混合持久化.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/06 字符串使用与内部实现原理.md.html">06 字符串使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/07 附录:更多字符串操作命令.md.html">07 附录:更多字符串操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/08 字典使用与内部实现原理.md.html">08 字典使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/09 附录:更多字典操作命令.md.html">09 附录:更多字典操作命令.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Redis 核心原理与实战/10 列表使用与内部实现原理.md.html">10 列表使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/11 附录:更多列表操作命令.md.html">11 附录:更多列表操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/12 集合使用与内部实现原理.md.html">12 集合使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/13 附录:更多集合操作命令.md.html">13 附录:更多集合操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/14 有序集合使用与内部实现原理.md.html">14 有序集合使用与内部实现原理.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/15 附录:更多有序集合操作命令.md.html">15 附录:更多有序集合操作命令.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/16 Redis 事务深入解析.md.html">16 Redis 事务深入解析.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/17 Redis 键值过期操作.md.html">17 Redis 键值过期操作.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/18 Redis 过期策略与源码分析.md.html">18 Redis 过期策略与源码分析.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/19 Redis 管道技术——Pipeline.md.html">19 Redis 管道技术——Pipeline.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/20 查询附近的人——GEO.md.html">20 查询附近的人——GEO.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/21 游标迭代器过滤器——Scan.md.html">21 游标迭代器过滤器——Scan.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/22 优秀的基数统计算法——HyperLogLog.md.html">22 优秀的基数统计算法——HyperLogLog.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/23 内存淘汰机制与算法.md.html">23 内存淘汰机制与算法.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/24 消息队列——发布订阅模式.md.html">24 消息队列——发布订阅模式.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/25 消息队列的其他实现方式.md.html">25 消息队列的其他实现方式.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/26 消息队列终极解决方案——Stream.md.html">26 消息队列终极解决方案——Stream.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/27 消息队列终极解决方案——Stream.md.html">27 消息队列终极解决方案——Stream.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/28 实战:分布式锁详解与代码.md.html">28 实战:分布式锁详解与代码.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/29 实战:布隆过滤器安装与使用及原理分析.md.html">29 实战:布隆过滤器安装与使用及原理分析.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/30 完整案例:实现延迟队列的两种方法.md.html">30 完整案例:实现延迟队列的两种方法.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/31 实战:定时任务案例.md.html">31 实战:定时任务案例.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/32 实战RediSearch 高性能的全文搜索引擎.md.html">32 实战RediSearch 高性能的全文搜索引擎.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/33 实战Redis 性能测试.md.html">33 实战Redis 性能测试.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/34 实战Redis 慢查询.md.html">34 实战Redis 慢查询.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/35 实战Redis 性能优化方案.md.html">35 实战Redis 性能优化方案.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/36 实战Redis 主从同步.md.html">36 实战Redis 主从同步.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/37 实战Redis哨兵模式.md.html">37 实战Redis哨兵模式.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/38 实战Redis 哨兵模式(下).md.html">38 实战Redis 哨兵模式(下).md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/39 实战Redis 集群模式(上).md.html">39 实战Redis 集群模式(上).md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/40 实战Redis 集群模式(下).md.html">40 实战Redis 集群模式(下).md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/41 案例Redis 问题汇总和相关解决方案.md.html">41 案例Redis 问题汇总和相关解决方案.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/42 技能学习指南.md.html">42 技能学习指南.md.html</a>
</li>
<li>
<a href="/专栏/Redis 核心原理与实战/43 加餐Redis 的可视化管理工具.md.html">43 加餐Redis 的可视化管理工具.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>10 列表使用与内部实现原理</h1>
<p>列表类型 (List) 是一个使用链表结构存储的有序结构,它的元素插入会按照先后顺序存储到链表结构中,因此它的元素操作 (插入\删除) 时间复杂度为 O(1),所以相对来说速度还是比较快的,但它的查询时间复杂度为 O(n),因此查询可能会比较慢。</p>
<h3>1 基础使用</h3>
<p>列表类型的使用相对来说比较简单,对它的操作就相当操作一个没有任何 key 值的 value 集合,如下图所示: <img src="assets/2020-02-28-031229.png" alt="列表类型使用-列表结构图.png" /></p>
<h4>1给列表添加一个或多个元素</h4>
<p>语法lpush key value [value …] 示例:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; lpush list 1 2 3
(integer) 3
</code></pre>
<h4>2给列表尾部添加一个或多个元素</h4>
<p>语法rpush key value [value …] 示例:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; rpush list2 1 2 3
(integer) 3
</code></pre>
<h4>3返回列表指定区间内的元素</h4>
<p>语法lrange key start stop 示例:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; lrange list 0 -1
&quot;3&quot;
&quot;2&quot;
&quot;1&quot;
127.0.0.1:6379&gt; lrange list2 0 -1
&quot;1&quot;
&quot;2&quot;
&quot;3&quot;
</code></pre>
<p>其中 -1 代表列表中的最后一个元素。</p>
<h4>4获取并删除列表的第一个元素</h4>
<p>语法lpop key 示例:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; lrange list 0 -1
1) &quot;d&quot;
2) &quot;c&quot;
3) &quot;b&quot;
4) &quot;a&quot;
127.0.0.1:6379&gt; lpop list
&quot;d&quot;
127.0.0.1:6379&gt; lrange list 0 -1
1) &quot;c&quot;
2) &quot;b&quot;
3) &quot;a&quot;
</code></pre>
<h4>5获取并删除列表的最后一个元素</h4>
<p>语法rpop key 示例:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; lrange list 0 -1
1) &quot;c&quot;
2) &quot;b&quot;
3) &quot;a&quot;
127.0.0.1:6379&gt; rpop list
&quot;a&quot;
127.0.0.1:6379&gt; lrange list 0 -1
1) &quot;c&quot;
2) &quot;b&quot;
</code></pre>
<h4>6根据下标获取对应的元素</h4>
<p>语法lindex key index 示例:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; rpush list3 a b c
(integer) 3
127.0.0.1:6379&gt; lindex list3 0
&quot;a&quot;
</code></pre>
<p>更多操作命令,详见附录部分。</p>
<h3>2 代码实战</h3>
<p>下面来看列表类型在 Java 中的使用,同样先添加 Jedis 框架,使用代码如下:</p>
<pre><code class="language-java">public class ListExample {
public static void main(String[] args) {
Jedis jedis = new Jedis(&quot;127.0.0.1&quot;, 6379);
// 声明 Redis key
final String REDISKEY = &quot;list&quot;;
// 在头部插入一个或多个元素
Long lpushResult = jedis.lpush(REDISKEY, &quot;Java&quot;, &quot;Sql&quot;);
System.out.println(lpushResult); // 输出2
// 获取第 0 个元素的值
String idValue = jedis.lindex(REDISKEY, 0);
System.out.println(idValue); // 输出Sql
// 查询指定区间的元素
List&lt;String&gt; list = jedis.lrange(REDISKEY, 0, -1);
System.out.println(list); // 输出:[Sql, Java]
// 在元素 Java 前面添加 MySQL 元素
jedis.linsert(REDISKEY, ListPosition.BEFORE, &quot;Java&quot;, &quot;MySQL&quot;);
System.out.println(jedis.lrange(REDISKEY, 0, -1)); // 输出:[Sql, MySQL, Java]
jedis.close();
}
}
</code></pre>
<p>程序运行结果如下:</p>
<blockquote>
<p>2 Sql [Sql, Java] [Sql, MySQL, Java]</p>
</blockquote>
<h3>3 内部实现</h3>
<p>我们先用 <code>debug encoding key</code> 来查看列表类型的内部存储类型,如下所示:</p>
<pre><code class="language-shell">127.0.0.1:6379&gt; object encoding list
&quot;quicklist&quot;
</code></pre>
<p>从结果可以看出,列表类型的底层数据类型是 quicklist。</p>
<p>quicklist (快速列表) 是 Redis 3.2 引入的数据类型早期的列表类型使用的是ziplist (压缩列表) 和双向链表组成的Redis 3.2 改为用 quicklist 来存储列表元素。</p>
<p>我们来看下 quicklist 的实现源码:</p>
<pre><code class="language-c">typedef struct quicklist { // src/quicklist.h
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* ziplist 的个数 */
unsigned long len; /* quicklist 的节点数 */
unsigned int compress : 16; /* LZF 压缩算法深度 */
//...
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl; /* 对应的 ziplist */
unsigned int sz; /* ziplist 字节数 */
unsigned int count : 16; /* ziplist 个数 */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* 该节点先前是否被压缩 */
unsigned int attempted_compress : 1; /* 节点太小无法压缩 */
//...
} quicklistNode;
typedef struct quicklistLZF {
unsigned int sz;
char compressed[];
} quicklistLZF;
</code></pre>
<p>从以上源码可以看出 quicklist 是一个双向链表,链表中的每个节点实际上是一个 ziplist它们的结构如下图所示 <img src="assets/2020-02-28-31230.png" alt="列表类型使用-quicklist结构图.png" /></p>
<p>ziplist 作为 quicklist 的实际存储结构它本质是一个字节数组ziplist 数据结构如下图所示:</p>
<p><img src="assets/2020-02-28-031231.png" alt="列表类型使用-压缩列表结构图.png" /></p>
<p>其中的字段含义如下:</p>
<ul>
<li>zlbytes压缩列表字节长度占 4 字节;</li>
<li>zltail压缩列表尾元素相对于起始元素地址的偏移量占 4 字节;</li>
<li>zllen压缩列表的元素个数</li>
<li>entryX压缩列表存储的所有元素可以是字节数组或者是整数</li>
<li>zlend压缩列表的结尾占 1 字节。</li>
</ul>
<h3>4 源码解析</h3>
<p>下面我们来看一下更多关于列表类型的源码实现。</p>
<h4>1添加功能源码分析</h4>
<p>quicklist 添加操作对应函数是 quicklistPush源码如下</p>
<pre><code class="language-c">void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
int where) {
if (where == QUICKLIST_HEAD) {
// 在列表头部添加元素
quicklistPushHead(quicklist, value, sz);
} else if (where == QUICKLIST_TAIL) {
// 在列表尾部添加元素
quicklistPushTail(quicklist, value, sz);
}
}
</code></pre>
<p>以 quicklistPushHead 为例,源码如下:</p>
<pre><code class="language-c">int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_head = quicklist-&gt;head;
if (likely(
_quicklistNodeAllowInsert(quicklist-&gt;head, quicklist-&gt;fill, sz))) {
// 在头部节点插入元素
quicklist-&gt;head-&gt;zl =
ziplistPush(quicklist-&gt;head-&gt;zl, value, sz, ZIPLIST_HEAD);
quicklistNodeUpdateSz(quicklist-&gt;head);
} else {
// 头部节点不能继续插入,需要新建 quicklistNode、ziplist 进行插入
quicklistNode *node = quicklistCreateNode();
node-&gt;zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
quicklistNodeUpdateSz(node);
// 将新建的 quicklistNode 插入到 quicklist 结构中
_quicklistInsertNodeBefore(quicklist, quicklist-&gt;head, node);
}
quicklist-&gt;count++;
quicklist-&gt;head-&gt;count++;
return (orig_head != quicklist-&gt;head);
}
</code></pre>
<p>quicklistPushHead 函数的执行流程,先判断 quicklist 的 head 节点是否可以插入数据,如果可以插入则使用 ziplist 的接口进行插入,否则就新建 quicklistNode 节点进行插入。</p>
<p>函数的入参是待插入的 quicklist还有需要插入的值 value 以及他的大小 sz。</p>
<p>函数的返回值为 int0 表示没有新建 head1 表示新建了 head。 quicklistPushHead 执行流程,如下图所示:</p>
<p><img src="assets/2020-02-28-031232.png" alt="列表类型使用-插入流程图.png" /></p>
<h4>2删除功能源码分析</h4>
<p>quicklist 元素删除分为两种情况:单一元素删除和区间元素删除,它们都位于 src/quicklist.c 文件中。</p>
<h5>① 单一元素删除</h5>
<p>单一元素的删除函数是 quicklistDelEntry源码如下</p>
<pre><code class="language-c">void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
quicklistNode *prev = entry-&gt;node-&gt;prev;
quicklistNode *next = entry-&gt;node-&gt;next;
// 删除指定位置的元素
int deleted_node = quicklistDelIndex((quicklist *)entry-&gt;quicklist,
entry-&gt;node, &amp;entry-&gt;zi);
//...
}
</code></pre>
<p>可以看出 quicklistDelEntry 函数的底层,依赖 quicklistDelIndex 函数进行元素删除。</p>
<h5>② 区间元素删除</h5>
<p>区间元素删除的函数是 quicklistDelRange源码如下</p>
<pre><code class="language-c">// start 表示开始删除的下标count 表示要删除的个数
int quicklistDelRange(quicklist *quicklist, const long start,
const long count) {
if (count &lt;= 0)
return 0;
unsigned long extent = count;
if (start &gt;= 0 &amp;&amp; extent &gt; (quicklist-&gt;count - start)) {
// 删除的元素个数大于已有元素
extent = quicklist-&gt;count - start;
} else if (start &lt; 0 &amp;&amp; extent &gt; (unsigned long)(-start)) {
// 删除指定的元素个数
extent = -start; /* c.f. LREM -29 29; just delete until end. */
}
//...
// extent 为剩余需要删除的元素个数,
while (extent) {
// 保存下个 quicklistNode因为本节点可能会被删除
quicklistNode *next = node-&gt;next;
unsigned long del;
int delete_entire_node = 0;
if (entry.offset == 0 &amp;&amp; extent &gt;= node-&gt;count) {
// 删除整个 quicklistNode
delete_entire_node = 1;
del = node-&gt;count;
} else if (entry.offset &gt;= 0 &amp;&amp; extent &gt;= node-&gt;count) {
// 删除本节点的所有元素
del = node-&gt;count - entry.offset;
} else if (entry.offset &lt; 0) {
// entry.offset&lt;0 表示从后向前,相反则表示从前向后剩余的元素个数
del = -entry.offset;
if (del &gt; extent)
del = extent;
} else {
// 删除本节点部分元素
del = extent;
}
D(&quot;[%ld]: asking to del: %ld because offset: %d; (ENTIRE NODE: %d), &quot;
&quot;node count: %u&quot;,
extent, del, entry.offset, delete_entire_node, node-&gt;count);
if (delete_entire_node) {
__quicklistDelNode(quicklist, node);
} else {
quicklistDecompressNodeForUse(node);
node-&gt;zl = ziplistDeleteRange(node-&gt;zl, entry.offset, del);
quicklistNodeUpdateSz(node);
node-&gt;count -= del;
quicklist-&gt;count -= del;
quicklistDeleteIfEmpty(quicklist, node);
if (node)
quicklistRecompressOnly(quicklist, node);
}
// 剩余待删除元素的个数
extent -= del;
// 下个 quicklistNode
node = next;
// 从下个 quicklistNode 起始位置开始删除
entry.offset = 0;
}
return 1;
}
</code></pre>
<p>从上面代码可以看出quicklist 在区间删除时,会先找到 start 所在的 quicklistNode计算删除的元素是否小于要删除的 count如果不满足删除的个数则会移动至下一个 quicklistNode 继续删除,依次循环直到删除完成为止。</p>
<p>quicklistDelRange 函数的返回值为 int 类型,当返回 1 时表示成功的删除了指定区间的元素,返回 0 时表示没有删除任何元素。</p>
<h4>3更多源码</h4>
<p>除了上面介绍的几个常用函数之外,还有一些更多的函数,例如:</p>
<ul>
<li>quicklistCreate创建 quicklist</li>
<li>quicklistInsertAfter在某个元素的后面添加数据</li>
<li>quicklistInsertBefore在某个元素的前面添加数据</li>
<li>quicklistPop取出并删除列表的第一个或最后一个元素</li>
<li>quicklistReplaceAtIndex替换某个元素。</li>
</ul>
<h3>5 使用场景</h3>
<p>列表的典型使用场景有以下两个:</p>
<ul>
<li>消息队列:列表类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询并删除)第一个元素,所以列表类型可以用来实现消息队列;</li>
<li>文章列表:对于博客站点来说,当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入到 List 中,因为 List 是有序的结构,所以这样又可以完美的实现分页功能,从而加速了程序的响应速度。</li>
</ul>
<h3>6 小结</h3>
<p>通过本文我们可以知道列表类型并不是简单的双向链表,而是采用了 quicklist 的数据结构对数据进行存取quicklist 是 Redis 3.2 新增的数据类型它的底层采取的是压缩列表加双向链表的存储结构quicklist 为了存储更多的数据,会对每个 quicklistNode 节点进行压缩,这样就可以有效的存储更多的消息队列或者文章的数据了。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Redis 核心原理与实战/09 附录:更多字典操作命令.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Redis 核心原理与实战/11 附录:更多列表操作命令.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709973c3bddf3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>