redis 六. list应用场景及底层分析
Posted 苹果香蕉西红柿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis 六. list应用场景及底层分析相关的知识,希望对你有一定的参考价值。
List 类型
一. 简单命令示例
1.首先简单说明: List是一个双端链表的结构,内容是2的32次方减1个元素,大概40多亿,主要功能有push/pop等,一般用在栈,队列,消息队列等场景
2.简单命令
//1.向列表左边添加元素
LPUSH key value[value...]
//2.向列表右边添加元素
RPUSH key value[value...]
//3.查看列表
LRANGE key start stop
//4.获取列表中的元素个数
LLEN key
二. java 操作示例
- 操作 List 列表类型,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),通过list进行指定下标的查询,可以做到简单分页
@Test
public void test03()
//1.创建操作List类型数据的对象
ListOperations<String ,String> listOperations= stringRedisTemplate.opsForList();
//2.左上添加,第一个添加的会被后一个添加的挤压到下面
listOperations.leftPush("student","liuliuliu");
//3.右下追加,第一个添加的会被后一个添加的挤到上面
listOperations.rightPush("students", "Zhao Liu");
//4.查询,根据key与下标获取指定位置的数据,左闭右闭,两边包含,返回一个List
List<String> students = listOperations.range("student", 0,2);
//5.根据key与指定下标获取单个数据
String stu = listOperations.index("student", 1);
//6.获取当前List类型数据的长度
Long total = listOperations.size("student");
//7.删除List类型数据,key为student的,value值为"Li",在List中第二次出现的,
listOperations.remove("student", 2, "Li");
// 删除多条,有左删除一条,右删除一条等
stringRedisTemplate.delete("student");
//8.从左边开始获取并删除
String srt1 = listOperations.leftPop("student");
//9.右边开始获取并删除
String str2= listOperations.rightPop("student");
三. 使用场景
- 公众号订阅: 假设用户a订阅了某个公众号sss,当sss公众号发布新文章,就会push到a用户的list中
//发布文章命令
LPUSH (like文章模块+a用户id) sss公众号文章id, bbb公众号文章id
//分页
LRANGE (like文章模块+a用户id) 0 9
- 商品评论列表: 一个商品会被不同用户评论,保存评论是按照先后顺序排序,查询商品时按照时间逆序排序,list存储时,key是商品id,value是评论信息:商品编号
LPUSH (品id)key 评论者id:评论信息
四. 底层分析
- list底层有linkedList、zipList和quickList三种存储方式
- 当列表对象保存的所有字符串元素的长度都小于 list-max-ziplist-value 默认 64字节, 并且保存的元素数量小于list-max-ziplist-entries默认512个时采用zipList, 否则采用linkedlist
- linkedlist: redis中自建了listNode对象, 通过内部的prev 和 next 指针组成的一个双端链表,
- 在redis 在 3.2 版本时,考虑到redis的空间存储效率和时间效率,引入了quicklist快速列表作为 list 的底层实现可以看成ziplist+linkedList实现的一个双端链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当于一个quicklist节点保存的是一片数据,而不再是一个数据
- redis中封装了quicklist 结构体变量, 内部通过quicklistNode 保存每一个节点数据,
typedef struct quicklist
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
quicklist;
typedef struct quicklistNode
//上一个node节点
struct quicklistNode *prev;
//下一个node
struct quicklistNode *next;
//保存的数据 压缩前ziplist 压缩后压缩的数据
unsigned char *zl;
//表示zl执行的ziplist的总大小,注意如果ziplist被压缩了,这个sz的值仍然时压缩前的大小
unsigned int sz;
//表示ziplist包含的数据个数,16bit
unsigned int count : 16;
//表示ziplist是否压缩,1表示没有,2表示压缩了
unsigned int encoding : 2;
//预留字段,当前是一个固定值2,表示使用zplist作为数据容器
unsigned int container : 2;
//解压标记1, 通过该标记可以再次压缩
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int extra : 10;
quicklistNode;
- 使用quicklist 插入数据时可以头部插入,或者尾部插入,
- 如果头节点(或尾节点) 上ziplist大小没有超过限制(即_quicklistModeAlLowEInsert 返回1),那么新数据被直接插入到ziplist中(调明ziplisteush ) 。
- 如果头节点(或尾节点)上ziplist太大了,那么新创建一个quicklistlode节点(对应地也会新创建一个ziplist),然后把这个新创建的节点插入到quicklist双向链表中
- quicklist 在任意指定位置插入数据时
- 当插入位置所在的ziplist大小没有超过限制时,直接插入到ziplist中
- 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的quicklist链表节点的ziplist大小没有超过限制,那么就转而插入到相邻的那个quicklist链表节点的ziplist中;
- 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的Squicklist链表节点的ziplist大小也超过限制,这时需要新创建一个quicklist链表节点插入。
- 对于插入位置所在的ziplist大小超过了限制的其它情况(主要对应于在ziplist中间插入数据的情况),则需要把当前ziplist分裂为两个节点,然后再其中一个节点上插入数据
- quicklist 查找: quicklist的节点是由一个一个的ziplist构成的每个ziplist都有大小,所以先根据每个node个数,找到对应的ziplist,调用ziplist的index就能成功找到。
- quicklist 删除: 在区间删除时,会先找到start 所在的 quicklistlode,计算删除的元素是否小于要删除的count,如果不满足删除的个数,则会移动至下一个quicklistNode 继续删除,依次循环直到删除完成为止
深入分析Redis特点及应用场景
本文和大家分享的主要是redis 特点及应用场景相关内容,一起来看看吧,希望对大家 学习redis有所帮助。
REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统。
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、 Key-Value 数据库,并提供多种语言的 API 。
它通常被称为数据结构服务器,因为值(value )可以是 字符串 (String), 哈希 (Map), 列表 (list), 集合(sets) 和 有序集合 (sorted sets) 等类型。
Redis的特点:
Redis 与其他 key - value 缓存产品有以下三个特点:
· Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
· Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list , set , zset , hash 等数据结构的存储。
· Redis 支持数据的备份,即 master-slave 模式的数据备份。
Redis的优势:
· 性能极高 – Redis 能读的速度是 110000 次 /s, 写的速度是 81000 次 /s 。
· 丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
· 原子 – Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作全并后的原子性执行。
· 丰富的特性 – Redis 还支持 publish/subscribe, 通知 , key 过期等等特性。
Redis与其他key-value存储有什么不同?
· Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。 Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
· Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis 可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
Redis应用场景
1. 显示最新的项目列表
下面这个语句常用来显示最新项目,随着数据多了,查询毫无疑问会越来越慢。
SELECT * FROM foo WHERE ... ORDER BY time DESC LIMIT 10
在Web 应用中, “ 列出最新的回复 ” 之类的查询非常普遍,这通常会带来可扩展性问题。这令人沮丧,因为项目本来就是按这个顺序被创建的,但要输出这个顺序却不得不进行排序操作。
类似的问题就可以用Redis 来解决。比如说,我们的一个 Web 应用想要列出用户贴出的最新 20 条评论。在最新的评论边上我们有一个 “ 显示全部 ” 的链接,点击后就可以获得更多的评论。
我们假设数据库中的每条评论都有一个唯一的递增的ID 字段。我们可以使用分页来制作主页和评论页,使用 Redis 的模板:
· 每次新评论发表时,我们会将它的 ID 添加到一个 Redis 列表:
LPUSH latest.comments
· 我们将列表裁剪为指定长度,因此 Redis 只需要保存最新的 5000 条评论:
LTRIM latest.comments 0 5000
· 每次我们需要获取最新评论的项目范围时,我们调用一个函数来完成(使用伪代码):
FUNCTION get_latest_comments(start,num_items): id_list = redis.lrange("latest.comments",start,start+num_items-1) IF id_list.length < num_items id_list = SQL_DB("SELECT ... ORDER BY time LIMIT ...") END RETURN id_list END
这里我们做的很简单。在Redis 中我们的最新 ID 使用了常驻缓存,这是一直更新的。但是我们做了限制不能超过 5000 个 ID ,因此我们的获取 ID 函数会一直询问 Redis 。只有在 start/count 参数超出了这个范围的时候,才需要去访问数据库。
我们的系统不会像传统方式那样“ 刷新 ” 缓存, Redis 实例中的信息永远是一致的。 SQL 数据库(或是硬盘上的其他类型数据库)只是在用户需要获取 “ 很远 ” 的数据时才会被触发,而主页或第一个评论页是不会麻烦到硬盘上的数据库了。
2. 删除与过滤
我们可以使用LREM 来删除评论。如果删除操作非常少,另一个选择是直接跳过评论条目的入口,报告说该评论已经不存在。
有些时候你想要给不同的列表附加上不同的过滤器。如果过滤器的数量受到限制,你可以简单的为每个不同的过滤器使用不同的Redis 列表。毕竟每个列表只有 5000 条项目,但 Redis 却能够使用非常少的内存来处理几百万条项目。
3. 排行榜相关
另一个很普遍的需求是各种数据库的数据并非存储在内存中,因此在按得分排序以及实时更新这些几乎每秒钟都需要更新的功能上数据库的性能不够理想。
典型的比如那些在线游戏的排行榜,比如一个Facebook 的游戏,根据得分你通常想要:
·
列出前100 名高分选手
·
·
列出某用户当前的全球排名
·
这些操作对于Redis 来说小菜一碟,即使你有几百万个用户,每分钟都会有几百万个新的得分。
模式是这样的,每次获得新得分时,我们用这样的代码:
ZADD leaderboard
你可能用userID 来取代 username ,这取决于你是怎么设计的。
得到前100 名高分用户很简单:
ZREVRANGE leaderboard 0 99
用户的全球排名也相似,只需要:
ZRANK leaderboard
4. 按照用户投票和时间排序
排行榜的一种常见变体模式就像Reddit 或 Hacker News 用的那样,新闻按照类似下面的公式根据得分来排序: score = points / time^alpha
因此用户的投票会相应的把新闻挖出来,但时间会按照一定的指数将新闻埋下去。下面是我们的模式,当然算法由你决定。
模式是这样的,开始时先观察那些可能是最新的项目,例如首页上的1000 条新闻都是候选者,因此我们先忽视掉其他的,这实现起来很简单。
· 每次新的新闻贴上来后,我们将 ID 添加到列表中,使用 LPUSH + LTRIM ,确保只取出最新的 1000 条项目。
· 有一项后台任务获取这个列表,并且持续的计算这 1000 条新闻中每条新闻的最终得分。计算结果由ZADD 命令按照新的顺序填充生成列表,老新闻则被清除。这里的关键思路是排序工作是由后台任务来完成的。
5. 过期项目处理
另一种常用的项目排序是按照时间排序。我们使用unix 时间作为得分即可。
模式如下:
· 每次有新项目添加到我们的非 Redis 数据库时,我们把它加入到排序集合中。这时我们用的是时间属性, current_time 和 time_to_live 。
· 另一项后台任务使用 ZRANGE…SCORES 查询排序集合,取出最新的 10 个项目。如果发现 unix 时间已经过期,则在数据库中删除条目。
6. 计数
Redis 是一个很好的计数器,这要感谢 INCRBY 和其他相似命令。
我相信你曾许多次想要给数据库加上新的计数器,用来获取统计或显示新信息,但是最后却由于写入敏感而不得不放弃它们。
好了,现在使用Redis 就不需要再担心了。有了原子递增( atomic increment ),你可以放心的加上各种计数,用 GETSET 重置,或者是让它们过期。
例如这样操作:
INCR user: EXPIRE user: 60
你可以计算出最近用户在页面间停顿不超过60 秒的页面浏览量,当计数达到比如 20 时,就可以显示出某些条幅提示,或是其它你想显示的东西。
7. 特定时间内的特定项目
另一项对于其他数据库很难,但Redis 做起来却轻而易举的事就是统计在某段特点时间里有多少特定用户访问了某个特定资源。比如我想要知道某些特定的注册用户或 IP 地址,他们到底有多少访问了某篇文章。
每次我获得一次新的页面浏览时我只需要这样做:
SADD page:day1:< page_id> < user_id>
当然你可能想用unix 时间替换 day1 ,比如 time()-(time()%3600*24) 等等。
想知道特定用户的数量吗?只需要使用
SCARD page:day1:
需要测试某个特定用户是否访问了这个页面?
8. 实时分析正在发生的情况,用于数据统计与防止垃圾邮件等
我们只做了几个例子,但如果你研究Redis 的命令集,并且组合一下,就能获得大量的实时分析方法,有效而且非常省力。使用 Redis 原语命令,更容易实施垃圾邮件过滤系统或其他实时跟踪系统。
9. Pub/Sub
Redis 的 Pub/Sub 非常非常简单,运行稳定并且快速。支持模式匹配,能够实时订阅与取消频道。
10. 队列
你应该已经注意到像list push 和 list pop 这样的 Redis 命令能够很方便的执行队列操作了,但能做的可不止这些:比如 Redis 还有 list pop 的变体命令,能够在列表为空时阻塞队列。
11. 缓存
Redis 的缓存部分值得写一篇新文章,我这里只是简单的说一下。 Redis 能够替代 memcached ,让你的缓存从只能存储数据变得能够更新数据,因此你不再需要每次都重新生成数据了。
来源:简书
以上是关于redis 六. list应用场景及底层分析的主要内容,如果未能解决你的问题,请参考以下文章