【Redis四】Jedis操作Redis的List类型
⼀、Jedis操作List链表对象的命令
private void  setListValue(String key){
//从左边插⼊元素 lpush key value [value ...]
getJedis().lpush(key,"value1");
//从左边插⼊多个值
getJedis().lpush(key,"value2","value3","value4","value5");
//从右边插⼊元素 rpush key value [value ...]
String numberKey = "numbers";
getJedis().rpush(numberKey, "5","4","3","2");
//获取指定范围内的元素列表 lrange key start end
List<String> lrange = getJedis().lrange(key, 0, -1);
//向某个元素前或者后插⼊元素 linsert key before|after pivot value
Long linsert = getJedis().linsert(key, BinaryClient.LIST_POSITION.BEFORE, "2", "3");
//获取列表指定索引下标的元素 lindex key index
String lindex = getJedis().lindex(key, 2);
//获取列表长度 llen key
Long llen = getJedis().llen(key);
//从列表左侧弹出元素 lpop key
String lpop = getJedis().lpop(key);
//从列表右侧弹出rpop key
String rpop = getJedis().rpop(key);
/
/删除指定元素,count>0从左到右,count<0从右到左,count=0,删除所有  lrem key count value
//删除列表指定的值,第⼀个参数为删除的个数(有重复时),后添加进去的值先被删,类似于出栈,返回删除个数
Long value1 = getJedis().lrem(key, 1, "value1");
//按照索引范围修剪列表,删除区间以外的数据,成功返回 OK ltrim key start end
String ltrim = getJedis().ltrim(key, 0, 2);
//修改指定索引下标的元素: lset key index newValue
String newValue = getJedis().lset(key, 2, "newValue");
//阻塞式弹出 blpop key [key ...] timeout、brpop key [key ...] timeout
//列表为空,则按照设置的timeout值进⾏阻塞
//列表不为空,则会⽴即返回
List<String> blpop = getJedis().blpop(1000, key);
}
⼆、什么是List列表对象类型
Redis的List是String类型的双向链表,List 是存储多个有序字符串的列表,列表的每个字符串被称为Element(元素),⼀个列表最多可以存储2^32次幂-1个元素。可以通过push、pop操作从链表的头部或者尾部添加删除元素,这样list既可以作为栈,⼜可以作为队列(栈就是insertFirst+deleteFirst,队列就是insertLast+deleteFirst)。Redis可以⽀持反向查和遍历,⽅便操作,不过带来了部分额外的内存开销。Redis内部的很多实现,包括发送缓冲队列等也都是⽤的这个数据结构。
List 中 [lr]push 和 [lr]pop命令的算法时间复杂度都是O(n),另外list会记录链表的长度, 所以len操作也是O(n)。链表的最⼤长度是(2的32次⽅-1)。 当我们[lr]pop⼀个list对象,如果list是空,或者不存在,会⽴即返回nil。但是阻塞版本的b[lr]pop可以则可以阻塞, 当然可以加超时时间,超时后也会返回nil。为什么要阻塞版本的pop呢,主要是为了避免轮询。⽤list来实现⼀个⼯作队列,执⾏任务的thread 可以调⽤阻塞版本的pop去 ,获取任务这样就可以避免轮询去检查是否有任务存在。当任务来时候⼯作线程可以⽴即返回,也可以避免轮询带来的延迟。
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
我们来看看之前提到的那个Redis有5种数据类型,10种对象编码类型,List对象对应的type为
#define OBJ_LIST 1      //列表对象
然后List的对象 编码类型如下?:
//双端链表,不在使⽤
#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
//由压缩列表组成的双向列表-->快速列表
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
对应的REDIS_LIST对应的 类型和编码的对象:
#使⽤压缩列表实现的列表对象。
REDIS_LIST  REDIS_ENCODING_ZIPLIST
#使⽤双端链表实现的列表对象。
REDIS_LIST  REDIS_ENCODING_LINKEDLIST
OBJECT ENCODING 对不同编码的输出:
双端链表    REDIS_ENCODING_LINKEDLIST  "linkedlist"
压缩列表    REDIS_ENCODING_ZIPLIST  "ziplist"
#Redis3.0之前的列表对象的编码可以是ziplist或者linkedlist。
从3.2开始Redis只使⽤quicklist作为列表的编码,quicklist是ziplist和双向链表的结合体。
列表键的底层实现之⼀为链表,当⼀个列表键包含了数量较多的元素,⼜或者列表中包含的元素都是⽐较长的字符串时,Redis 就会使⽤链表作为列表键的底层实现。接着就来看看链表的数据结构:?
#每个链表节点使⽤⼀个listNode结构表⽰(adlist.h/listNode)
// file: adlist.h
typedef struct listNode {
struct listNode *prev;  // 前置节点
struct listNode *next;  //后置节点
void *value;            //节点的值
} ListNode;
链表提供了⾼效的节点重排能⼒,以及顺序性的节点访问⽅式,并且可以通过增删节点来灵活地调整链表的长度。多个链表节点通
过 prev和 next 组成双端链表。Redis通过List来管理链表节点?:
typedef struct list {
listNode *head;        //表头节点
listNode *tail;        //表尾节点
unsigned long len;      //链表的节点数量
void *(*dup) (void *ptr);  //节点值复制函数
void (*free)(void *ptr);    //节点值释放函数
int (*match)(void *ptr, void *key); //节点值对⽐函数
} list;
Redis 的链表实现特性可以总结如下:
双端: 因为带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的复杂度都是$O(1)$ 。
⽆环: 表头的 prev 指针和表尾的 next 指针均指向 NULL, 对链表的访问以 NULL 结束。
带表头指针和表尾指针: 通过 list 结构获取表头和表尾的复杂度为$ O(1)$。
带链表长度计数器: list 结构的 len 属性对链表节点的长度计数, 获取链表中节点的数量复杂度为$ O(1)$ 。
多态: 链表节点使⽤ void * 指针来保存节点值, 并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数。
链表可以⽤于保存各种不同类型的值。
我们可以来看看⽤list和listNode组成的链表,以及列表对象的编码ziplist和linkedlist:
其实上⼀篇⽂章⾥⾯也有提及 Hash对象也是使⽤压缩列表(zipList)实现的哈希对象,List对象也会使⽤压缩列表(zipList)来实现
的。zipList的好处在于更能节省空间,怎么做到的更能节省空间,因为ziplist 所存储的东西都是放在连续内存区域当中。当列表对象元素不⼤,每个元素也不⼤的时候,就采⽤ziplist存储但当数据量过⼤时就ziplist就不是那么好⽤了。因为为了保证他存储内容在内存中的连续性,插⼊的复杂度是O(N),即每次插⼊都会重新进⾏realloc。
realloc函数⽤于修改⼀个原先已经分配的内存块的⼤⼩,可以使⼀块内存的扩⼤或缩⼩。
malloc的作⽤是在内存的动态存储区中分配⼀个长度为size的连续空间。malloc所分配的内存是⼀块连续的空间。同时,malloc实际分配的内存空间可能会⽐你请求的多⼀点,但是这个⾏为只是由编译器定义的。malloc函数是对存储区域进⾏分配的。
连续内存分配:给进程分配⼀块不⼩于指定⼤⼩的连续的物理内存区域,在考虑将多个进程,考虑怎么样可以是输⼊队列中需要调⼊内存的进程分配空间,当在采⽤连续内存分配时,每个进程位于⼀个连续的内存区域,与包含下⼀个进程的内存相连。
如下图所⽰,对象结构中ptr所指向的就是⼀个ziplist,整个ziplist只需要malloc⼀次,它们在内存中是⼀块连续的区域。
接着来看看LinkedList,linkedlist是⼀种双向链表。它的结构⽐较简单,节点中存放pre和next两个指针,还有节点相关的信息。当每增加⼀个node的时候,就需要重新malloc⼀块内存。
Redis3.0之前的列表对象的编码可以是ziplist或者linkedlist。当列表对象保存的字符串元素的长度都⼩于64字节并且保存的元素数量⼩于512个,使⽤ziplist编码,可以通过修改Redis的配置list-max-ziplist-value和list-max-ziplist-entries来修改这两个条件的上限值、两个条件任意⼀个不满⾜时,ziplist会变为linkedlist。
Redis 从3.2开始只使⽤quicklist作为列表的编码,quicklist是ziplist和双向链表的结合体,quicklist的每个节点都是⼀个ziplist。可以通过修改list-max-ziplist-size来设置⼀个quicklist节点上的ziplist的长度,取正值表⽰通过元素数量来限定ziplist的长度;负数表⽰按照占⽤字节数来限定,并且Redis规定只能取 -1 到 -5这五个负值。
#设置⼀个quicklist节点上的ziplist的长度,按照占⽤字节数来限定
-5: 每个quicklist节点上的ziplist⼤⼩不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist⼤⼩不能超过32 Kb。
-3: 每个quicklist节点上的ziplist⼤⼩不能超过16 Kb。
-2: 每个quicklist节点上的ziplist⼤⼩不能超过8 Kb。(默认值)
-1: 每个quicklist节点上的ziplist⼤⼩不能超过4 Kb。
这⾥redis还有⼀种LZF的⽆损压缩算法,可以通过配置redis的参数list-compress-depth表⽰⼀个quicklist两端不被压缩的节点个数。
#设置quicklist两端不被压缩的节点个数
0: 表⽰都不压缩。默认值。
1: 表⽰quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表⽰quicklist两端各有2个节点不压缩,中间的节点压缩。
3: 表⽰quicklist两端各有3个节点不压缩,中间的节点压缩。
依此类推…
三、List的应⽤场景
套⽤⼀句话,⽤物之长,天下⽆不可⽤之物。 列表的优点就是在于 列表的元素是有序的,这就意味着可以通过索引下标获取某个或某个范围内的元素列表。加之,列表内的元素是可以重复的。就可以被⽤来?:
redis支持的五种数据类型
1. 消息队列: redis的lpush+brpop命令组合即可实现阻塞队列,⽣产者客户端是⽤lupsh从列表左侧插⼊元素, 多个消费者客户端使
⽤brpop命令阻塞时的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和⾼可⽤性。
2. Redis⽤作⽇志收集器,多个端点将⽇志信息写⼊Redis,然后⼀个worker统⼀将所有⽇志写到磁盘。
3. 各种列表,⽐如类似QQ、微博的关注列表、粉丝列表等,最新消息排⾏、每篇⽂章的评论等也可以⽤Redis的list结构来实现。
4. 利⽤LRANGE可以很⽅便的实现list内容分页的功能。
5. 取最新N个数据的操作:LPUSH⽤来插⼊⼀个内容ID,作为关键字存储在列表头部。LTRIM⽤来限制列表中的项⽬数最多为5000。
如果⽤户需要的检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库。