相关命令:
操作类型 | 命令 | 时间复杂度 |
添加 | rpush key value [value ...] | O(k),k 是元素个数,从右侧添加,如果key不存在则会创建这个key,返回key中元素个数 |
添加 | lpush key value [value ...] | O(k),k 是元素个数,从左侧添加,如果key不存在则会创建这个key |
添加 | rpushx key value [value ...] | O(k),k 是元素个数 ,从右侧添加 ,如果key不存在则不会创建这个key,返回值为零,成功返回key中元素个数。 |
添加 | lpushx key value [value ...] | O(k),k 是元素个数 ,从左侧添加 ,如果key不存在则不会创建这个key,返回值为零,成功返回key中元素个数。 |
添加 | linsert key before | after pivot value | O(n),n 是 pivot 距离头尾的距离 |
查找 | lrange key start end | O(s+n),s 是 start 偏移量,n 是 start 到 end 的范围 |
查找 | lindex key index | O(n),n 是索引的偏移量 |
查找 | llen key | O(1) |
删除 | lpop key | O(1) |
删除 | rpop key | O(1) |
删除 | lrem key count value | O(k),k 是元素个数 |
删除 | ltrim key start end | O(k),k 是元素个数 |
修改 | lset key index value | O(n),n 是索引的偏移量 |
阻塞操作 | blpop | O(1) |
阻塞操作 | brpop | O(1) |
- 列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,例如要获取图 2-20 的第 5 个元素,可以执行 lindex user:1:messages 4 或者倒数第 1 个元素,lindex user:1:messages -1 就可以得到元素 e,此处的有序指的不是升序或者降序,而是强调顺序很关键,也就是说把一个list的元素不同元素位置颠倒一下得到的新list和原来的不等价,列表中允许出现重复元素。
- 区分获取和删除的区别,例如图 2-20 中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删除,这个操作会导致列表的⻓度从 5 变成 4;但是执行 lindex 4 只会获取元素,但列表长度是不会变化 的。
- 列表中的元素是允许重复的,例如图 2-21 中的列表中是包含了两个 a 元素的。
Redis中list底层更像双端队列,和java中的list并不一样,所不要期待这里的list有和Java中list有相同的方法/狗头!
lpushx & rpushx:
lpushx & rpushx 在 key 存在时,将⼀个或者多个元素从左侧放⼊(头插)到 list 中。不存在,直接返回0:
LPUSHX key element [element ...] // element 存入list的字符串,只能存字符串吗??
RPUSHX key element [element ...] // x 的含义是 exists 存在
因为key1不存在,所以 lpush key1 777 888 并没有添加成功,也没有创建key1。
lrange:
lrange key start end // 获取闭区间内的元素
一谈到下标,我们往往就要关注是否越界的问题:
- C++中,下标越界访问一般认为是“未定义行为”,它会越界的位置访问数据,可能会导致程序崩溃或者得到一个非法数据、或者合法但错误的数据、又或者敲好合法的正确数据都有可能。
- Java中,如果数组越界会直接抛出异常
- lrange 命令中如果下标越界采用的是一种软着陆的方式,对于越界的位置会尽可能的获取里面的合法数据,如下图:
虽然下标100越界了,但是redis并没有报错,对于合法区间正常获取元素,对于非法区间不做处理
lrang == list rang != light rang
因此并不存在 rrang
lpop & rpop:
删除元素,redis5中不支持删除指定个数元素,redis6.2及以后版本支持删除指定个数的元素:
lpop key //redis5
lpop key element//redis6.2
lindex:
获取给定下标的元素,同样也是软着陆,需要注意的是这个命令的时间复杂度是O(N)的,N指的该list中的规模。
linstert:
linsert key before | after pivot value // O(n),n 是 pivot 距离头尾的距离
如果有多个基准点,则会从左向右查找第一个基准点添加元素。
lrem:
lrem key count value // lrem => list remove != light rremove 所以没有rrem
下面是官方解释:如果count是正数代表从head开始删除count个value,如果count是负数代表从tial开始删除count个value,如果count为0代表删除列表中所有的value。
Redis中的list的底层更像是双端列表,而不是Arraylist,所以有些操作的时间复杂度是O(N),关于下标越界的如何处理的问题,lindex key index 如果越界了则会返回nil,并不会报错或者“拆忙盒”,而lset key index element 如果下标越界则会直接报错。
blpop & brpop:
阻塞队列:当队列为空时,尝试出队列会产生阻塞,直到队列不会空;当队列已满时,尝试入队列则会产生阻塞。谁阻塞?想要出队列和入队列的线程阻塞。
blpop & brpop 时出队的命令,所以消费者客户端执行这两个命令的时候队列为空,则会进入阻塞状态(注意可不是redis服务器端阻塞),但是也不能一直阻塞,所以要设置一个过期时间(单位为秒s)解除阻塞状态:
进入阻塞状态:
当另外一个终端向队列中插入元素后,队列不为空,解除阻塞状态:
blpop & brpop 还支持监控多个列表:所有列表都为空则进入阻塞状态,那个列表先不空,就从那个列表出元素。如果有多个列表都不为空,则从 blpop key [key ...] timeout 中第一个不为空的列表中出元素。
编码方式:
列表类型的内部编码有两种(现在已经结合为了一种quicklist编码方式) :
- ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个),同时 列表中每个元素的⻓度都⼩于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选用 ziplist 来作为列表的内部编码实现来减少内存消耗。
- linkedlist(链表):当列表类型无法满足 ziplist 的条件时,Redis会使用linkedlist作为列表的内部实现。
上述中的512、64是可以在redis配置文件中修改的,这两个配置都已经不用了,原因是list采用了quicklist进行编码:
quicklist相当于ziplist和linkedlist的结合:整体看是一个链表,每个节点又是一个压缩列表,每个压缩列表都不会太大,同时再把多个压缩列表通过链式结构连起来:
应用场景:
充当消息队列:
上图的消息队列模型可以起到“轮询”的作用,谁先发起brpop谁就先得到元素,如果再发起一brpop,以为redis是单线程模型,所以会重新排到后面。
分频道的消息队列:
分页查询:
微博 Timeline 每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不仅是有序的,同时支持按照索引范围获取元素。
1)每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content:
比特就业课 hmset mblog:1 title xx timestamp 1476536196 content xxxxx
hmset mblog:n title xx timestamp 1476536196 content xxxxx
2)向用户 Timeline 添加微博,user::mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3 ...
lpush user:k:mblogs mblog:9
3)分页获取用户的 Timeline,例如获取用户 1 的前 10 篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
此方案在实际中可能存在两个问题:
1. 1 + n 问题。即如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线)模式批量提交命令(把多个命令合并成一个网络请求),或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。
2. 分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。
写的有点乱了,多多包涵!