想要看更加舒服的排版、更加准时的推送
关注公众号“不太灵光的程序员”
每日八点有干货推送
公众号“不太灵光的程序员” 同时发布《【Redis数据结构 List 类型】List 类型生产中的应用 消息队列、排行榜、朋友圈、监控程序的实现》
本文依旧会对学习内容进行拆分,建议阅读时间基本保持10分钟内,想学习之前章节内容点击《你不了解的Redis》阅读所有章节内容。
Redis数据结构系列是对Redis常用的String、List、Set、Sorted Set、Hashe和Stream 6种数据结构进行介绍,并使用Python模块redis-py进行实践操作。
我们先简单了解下 List 的操作命令再来使用这些功能做些小实验。
List 基于链表(Linked List)实现,按插入顺序排序的字符串元素的集合。
优点是对不同量级长度的 Redis List 进行头部或尾部进行增删元素操作时时间复杂度都是常数级别的。
缺点是通过索引访问元素的速度较低,需要快速访问集合元素建议使用有序集合(Sorted Set)。
链表就像是我们去吃饭时叫号排队一样,叫的号存在顺序但人本身是可以任意走动的,不需要按照号的顺序站着。
为了后面的理解,我们定链表的左边为头部、链表的右边为尾部。
从头部或尾部添加一个或多个新的元素,返回链表长度。
LPUSHX/RPUSHX 是只能对已经存在元素的链表才会做增加操作。
LPOP/RPOP 是以非阻塞的方式读取,从头部或尾部删除一个元素并返回元素的值,当 List 中没有元素时继续进行弹出元素操作,返回 None。
BLPOP/BRPOP 是以阻塞的方式读取,从头部或尾部删除一个元素并返回链表的key值和元素的值 组成的元组,链表中没有元素的时候返回None,timeout是阻塞超时时间。
非阻塞模式下是轮询读取队列中的数据,是可以容易的去观察程序的运行情况,但是阻塞模式下队列中没有数据的话程序会一直处于阻塞状态难以观察运行状态,设置一个合理的超时时间是比较有效的方法。
从 List 中取出一定范围的元素,参数为 start、stop 两个索引,start 和 end 偏移量都是基于0的下标(也就是头部的第一个元素),可以使用超出链表长度的索引和负索引。
对链表切片,截取从左边起的值的长度,参数为两个索引。
返回存储在 key 里的list的长度。
原子性地返回并移除存储在 source 的链表的最后一个元素(链表尾部元素), 并把该元素放入存储在 destination 的链表的第一个元素位置(链表头部)。
假设 source 存储着链表 a,b,c, destination 存储着链表 x,y,z。 执行 RPOPLPUSH 得到的结果是 source 保存着链表 a,b ,而 destination 保存着链表 c,x,y,z。
如果 source 不存在,那么会返回 nil 值,并且不会执行任何操作。
如果 source 和 destination 是同一个链表的,那么这个操作等同于移除链表最后一个元素并且把该元素放在链表头部, 所以这个命令也可以当作是一个循环链表的命令。
针对一些开发中常见的场景使用上面介绍到的命令来进行实践。
在之前的内容中也提到了List类型的 LPUSH+RPOP/BRPOP、RLPUSH+LPOP/BLPOP 都能实现队列的功能,直接撸代码吧。
需要注意的就是阻塞模式的返回值与非阻塞的不同,是key值和元素值的元组。
设定队列 task:mq
生产者A生产两条消息从头部加入队列,消费者AB采用阻塞和非阻塞的方式读取队列消息。
keyname = "task:mq" r.lpush(keyname, u"你好啊 不太灵光的程序员!") r.lpush(keyname, u"更新个红锁")
keyname = "task:mq" while 1: message = r.rpop(keyname) print(message, datetime.now()) time.sleep(1)
keyname = "task:mq" while 1: message = r.brpop(keyname, timeout=5) print(message, datetime.now())
List是顺序链表,当做增加和删除元素时间复杂度是非常低的,对于索引元素位置效率就比较低了,这种特点对于需要频繁更新名次的实时排行榜就不合适了。
CSDN的周榜、总榜每周二更新一次,个人的单篇文章访问量排行榜每天更新一次,畅销书月榜等等访问量高但是更新周期不频繁的排行榜可以结合LRANGE命令实现。
设定文章排行榜日榜 article:tops:{date}
对文章访问量进行每日排名并支持分页查询。
文章列表 = [“Redis实现消息队列的6种方案”, “让运维更简单的7种定时任务实现方式”, “细品28岁程序员退休创业背后的可怕故事”, “工作中都有哪些让你心累的时刻”]
import pandas as pd import random for d in pd.date_range(start='2020-02-20', end='2020-02-25', freq='D'): date = d.date() tops = f"article:tops:{date}" for title in [f"《Redis实现消息队列的6种方案》 访问量 " + str(random.randint(1000, 2000)), f"《让运维更简单的7种定时任务实现方式》 访问量 " + str(random.randint(800, 1000)), f"《细品28岁程序员退休创业背后的可怕故事》 访问量 " + str(random.randint(600, 800)), f"《工作中都有哪些让你心累的时刻》 访问量 " + str(random.randint(100, 500)), ]: details = r.rpush(tops, title)
import pandas as pd for d in pd.date_range(start='2020-02-20', end='2020-02-25', freq='D'): date = d.date() tops = f"article:tops:{date}" print(f"{date} 日榜 ") for index in range(0, 10, 2): details = r.lrange(tops, index, index+1) if details: print(details) else: print("没有下一页了") break
你有新的消息、朋友圈、新的点赞、新的评论。
其实也是消息队列,每个用户都创建一个消息队列来保存待阅的消息,你不点击查看就显示队列的长度,点击查看再去一次消费所有消息。
moments1 = """ 老干妈: 我冤枉啊 2020-07-01 """ moments2 = """ XXX: 老干妈上架大客户专属1000瓶辣椒酱 售价9999 2020-07-02 """ moments3 = """ XXX: 腾讯公司未展开调查直接冻结老干妈1600万 2020-07-02 """ keyname = "user:1000:moments" r.lpush(keyname, moments1, moments2, moments3) print("今天新的朋友圈消息待刷新", r.llen(keyname)) while 1: print(r.rpop(keyname)) time.sleep(2)
监控程序说的太宽泛了,其实只是循环队列的一种在监控场景中的应用,在公司里会有许多接口是需要监控是否存活的,一般情况会留一个返回 successful 的接口供监控程序调用。
可以把这些接口存在数据库中,程序读取出来让再去轮询,当有新的接口加入时需要通知程序从新去查数据库,再轮询,就存在了判断是否有新的接口的判断。
上面提到了 RPOPLPUSH/BRPOPLPUSH 命令,当操作的key是同一个是就是循环队列,程序运行中可以动态加入新的成员,缺点时能进不能出,尬尬尬 移除成员需要单独实现。
def get_api(url): print(url, "successful") time.sleep(2) r.lpush("task:mq", "api1", "api2", "api3", "api4") while 1: url = r.brpoplpush("task:mq", "task:mq", timeout=30) get_api(url)