redis-----03-----redis-list结构以及应用

1 list

首尾相接的双向链表,链表首尾操作时间复杂度为O(1) ;查找中间元素时间复杂度为 ;O(n)。
列表中数据可能会被压缩:

  1. 元素长度小于 48,不压缩。
  2. 元素压缩前后长度差不超过 8,不压缩。例如压缩前大小是60,压缩后大小是56,那么不会压缩。

所以一个占用内存很大答结构,可能会被redis压缩成多个ziplist,即quicklist。
具体压缩后如何提高性能,可以看回这篇文章:redis-----01-----redis介绍(redis安装下载、底层存储结构原理剖析)

2 基础命令

2.1 LPUSH、LPOP、RPUSH、RPOP
# 从队列的左侧入队一个或多个元素。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key 不是一个 list 的话,那么会返回一个错误。
# 返回值integer-reply: 在 push 操作后的 list 长度。
LPUSH key value [value ...] 
# 从队列的左侧弹出一个元素。
# 返回值bulk-string-reply: 返回这个被弹出元素的值,或者当 key 不存在时返回 nil。
LPOP key 

# 从队列的右侧入队一个或多个元素。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key 不是一个 list 的话,那么会返回一个错误。
# 返回值integer-reply: 在 push 操作后的 list 长度。
RPUSH key value [value ...] 
# 从队列的右侧弹出一个元素。
# 返回值bulk-string-reply: 返回这个被弹出元素的值,或者当 key 不存在时返回 nil。
RPOP key

例如:
在这里插入图片描述

2.2 LRANGE、LREM、BRPOP
# 返回从队列的 start 和 end 之间的元素 下标从0开始。
# 注意:超过范围的下标时:当下标超过list范围的时候不会产生error。
# 如果start比list的尾部下标大的时候,会返回一个空列表。 
# 如果stop比list的实际尾部大的时候,Redis会当它是最后一个元素的下标。 
# 返回值array-reply: 返回指定范围里的列表元素。
LRANGE key start end 

# 从存于 key 的列表里移除前 count 次出现的值为 value 的元素。
# count > 0: 从头往尾移除值为 value 的元素。
# count < 0: 从尾往头移除值为 value 的元素。
# count = 0: 移除所有值为 value 的元素。
# 返回值integer-reply: 被移除的元素个数。
# 注意:如果key不存在或者key里面没有value值,那么它们就会被当作空list处理,所以它们都会返回 0。
LREM key count value 

# 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出任何元素的时候阻塞连接。
# BRPOP指的是:block right pop。
# 返回值多批量回复(multi-bulk-reply): 具体来说:
# 1)当没有元素时被弹出时返回一个 nil 的多批量值,并且 timeout 过期。
# 2)当有元素弹出时会返回一个双元素的多批量值,其中第一个元素是弹出元素的 key,第二个元素是 value。
# 这个命令是比较重要的。
BRPOP key [key ...] timeout

2.2.1 演示LRANGE:
在这里插入图片描述

2.2.2 演示LREM
可以看到,当我们想删除的count多于list的实际个数时,那么redis会删除list中的所有的这个value,但不会报错。redis删除个数可以根据返回值查看。
在这里插入图片描述
验证LREM的count的情况:
在这里插入图片描述
验证LREM的注意点:
在这里插入图片描述

2.2.3 延时BRPOP
这演示之前,先重点讲一下BRPOP的阻塞,BRPOP的 阻塞是阻塞在连接上面,而不是阻塞在redis本身。例如下图,有3个后端服务器连接了同一个redis服务器。假设s1进行brpop并阻塞住了,此时s2、s3发送其它命令例如都是get xxx,那么redis可以处理s2、s3的请求。

这里就可以看到,如果阻塞是阻塞在redis,那么有连接进行brpop,其它连接就必须等待,这种设计是不合理的,所以阻塞肯定是阻塞在后端服务器与redis之间的连接上面,而非阻塞在redis服务器本身。
在这里插入图片描述

演示BRPOP的返回值:

  • 1)超时返回nil,即超过设置的timeout时间,会返回nil。
    在这里插入图片描述
  • 2)测试返回对应的key-value键值对。
# 测试方法,首先在一个客户端输入,超时时间按照自己需要设置即可,我这里30s。
BRPOP list 30

# 然后开启另一个客户端,往key=list这个链表push数据,好让上面的客户端在超时时间内可以返回。
LPUSH list hello

结果,其中左边的客户端的BRPOP的返回值的意思:list代表key。hello代表BRPOP获取到的内容value。9.05s代表获取到这个内容的时间。因为我是间隔了9.05s才输入LPUSH list hello这个命令的。
在这里插入图片描述

2 应用

2.1 栈(先进后出 FILO)
LPUSH + LPOP 
# 或者 
RPUSH + RPOP
2.2 队列(先进先出 FIFO)
LPUSH + RPOP 
# 或者 
RPUSH + LPOP

在redis中,队列按照常见的场景,可以再分为异步队列和阻塞队列。

  • 1)异步队列:异步就是我在干着一件事情的同时可以干另一件事。例如下面,web可能不断的往redis的队列产生消息,此时后端服务器会不断循环的从redis队列pop消息。这样web的产生和server的消费相当于redis队列同时干着不同的事情。 web和server相当于两个线程,一个线程在生产,一个在消费。
    但是这个"线程安全"问题,需要客户端自己解决,因为redis本身是单线程的,所以自己是安全的,但由于redis本身就是一个共享资源,所以要做到线程安全,需要web、server对redis这个共享资源进行加锁。这个锁是web、server自己内部处理的,和redis无关,让两者在同一时间内只能有一个客户端操作用户状态。
    不过加锁我们就需要考虑锁粒度、死锁等问题了,无疑添加了程序的复杂性,不利于维护。
    这里简谈了一下redis如何做到“线程安全”的问题。
    在这里插入图片描述

  • 2)阻塞队列(blocking queue):因为上面的异步队列的server需要不断轮询redis的队列有无消息,这必然导致CPU增高,所以我们可以使用阻塞的方式来pop,即BRPOP,这样当没有数据时,我们可以阻塞等待,从而不占用CPU,但是这样我们在程序中需要额外使用一条redis连接,以此来区别操作redis命令的连接,不然你阻塞住了,你其它redis命令就无法输入了。

LPUSH + BRPOP 
# 或者 
RPUSH + BLPOP

对于栈和队列的redis实现命令,可以简单使用口诀记住,但前提必须是理解了这层含义。

口诀:栈同队不同
2.3 获取固定窗口记录

在某些业务场景下,需要获取固定数量的记录。例如微信固定只展示最近的前5条朋友圈,或者游戏的战绩固定只显示最近的50条。

例如下面以固定只展示最近的前5条朋友圈为例子:

  • 1)首先插入以下六条数据到链表list中,最好从上往下依次插入。其中name表示发朋友圈的人,text代表文字,picture代表此次朋友圈的图片,timestamp代表发表时间。
lpush says '{["name"]:"tyy1", ["text"]:"Happy Spring Festival!", ["picture"]:["url://image-20220601172741434.jpg", "url://image- 20220601172741435.jpg"], timestamp = 1231231230}' 
lpush says '{["name"]:"tyy2", ["text"]:"Happy Spring Festival!", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231231}' 
lpush says '{["name"]:"tyy3", ["text"]:"Happy Spring Festival!", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231232}' 
lpush says '{["name"]:"tyy4", ["text"]:"Happy Spring Festival", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231233}' 
lpush says '{["name"]:"tyy5", ["text"]:"hello world", ["picture"]:["url://image-xxx.jpg", "url://image-xxx.jpg"], timestamp = 1231231234}' 
lpush says '{["name"]:"tyy6", ["text"]:"hello world", ["picture"]:["url://image-xxx.jpg", "url://image- xxx.jpg"], timestamp = 1231231235}'
  • 2)插入后,那么我们如何保证每次从链表中获取的数据都是最新的呢?答案就是使用redis的ltrim命令,它能够对链表进行裁剪,链表本身会发生改变,例如本身5个元素,裁剪后变成3个元素,其余两个元素就会被删除掉。具体用法看http://redis.cn/commands.html。
# 因为我们使用lpush进行插入链表,也就是左边的元素必定是最近插入也就是最新的,所以我们每次裁剪左边的5个元素即可保证固定窗口记录。
# 也就是最新的5条朋友圈。
# 裁剪最近5条记录
ltrim says 0 4

# 查看list是否是保存着最新的记录。
lrange says 0 -1

那么看到这里你就会处理 游戏的战绩固定只显示最近的50条 的功能,非常简单。

结果,因为tyy2、tyy3、tyy4、tyy5、tyy6是最近插入的,所以裁剪后肯定剩下它们,而tyy1是最先插入的,不在最近5条的范围内,所以它会被删除掉。
在这里插入图片描述

3 从获取固定窗口记录面临的一个问题

# 下面两条命令如何做到原子执行呢?
# lpush可能会执行多次再裁剪,这里举例为一次而已。
lpush says '**' 
ltrim says 0 4

上面2.3的例子知道,只要我每次lpush完数据,就使用ltrim进行裁剪,就能做到固定窗口记录。
但是问题来了,由于redis是单线程的,在lpush完数据后,ltrim裁剪之前,可能也有其它redis连接在操作这个链表,导致数据不同步,进而获取不到自己想要的结果,所以需要确保这两个命令原子执行。
那我们如何确保这两个命令能按照原子操作来执行呢?

答:实际应用过程中,需要保证命令的原子性,所以需要使用 lua 脚本或者使用 pipeline 命令 + 事务。这个会在redis后续的文章讲到。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值