Redis-列表结构实操

列表实操

    • 前言
    • 简单练习
        • 基本的LPUSH和RPUSH操作
        • 列表元素的访问与修改
        • 列表元素的插入和删除
        • 列表阻塞操作
    • 困难练习
      • 分页列表
        • 游标机制
        • 业务上考虑直接访问任意页
        • 如何高效分页
        • 局限性
        • 小结
      • 实现限时排行版轮换
      • 消息队列可靠性实现
      • 分布式锁实现
    • 总结

前言

之前总结过-列表的数据结构,但是只是理论知识肯定是不行的,思来想去,总结了一些实操例子和实操场景,个人觉得还是很有用的,尤其是后面的场景,平时有我遇见的然后总结下来,例子都是我自己跑过的,没什么问题,有问题评论区见.

简单练习

基本的LPUSH和RPUSH操作
  1. 创建一个名为"students"的列表,从左侧依次添加"张三"、“李四”、“王五”
  2. 从右侧添加"赵六"、“钱七”
  3. 查看整个列表内容
  4. 获取列表长度
> LPUSH students 王五 李四 张三
3
> RPUSH students 赵六 钱七
5
> LRANGE students 0 -1
张三
李四
王五
赵六
钱七
> LLEN students
5
列表元素的访问与修改
  1. 获取"students"列表的第3个元素
  2. 将第2个元素修改为"黎彤"
  3. 查看修改后的列表
> lindex students 2
王五
> lset students 1 黎彤
OK
> lrange students 0 -1
张三
黎彤
王五
赵六
钱七
列表元素的插入和删除
  1. 在"张三"之前插入"刘备"
  2. 在"钱七"之后插入"孙权"
  3. 删除一个值为"黎彤"的元素
  4. 从左侧弹出一个元素
  5. 从右侧弹出一个元素
  6. 查看当前列表
> linsert students before 张三 刘备
6
> linsert students after 钱七 孙权
7
> lrange students 0 -1
刘备
张三
黎彤
王五
赵六
钱七
孙权> lrem students 1 黎彤
1
> lpop students
刘备
> rpop students
孙权
> lrange students 0 -1
张三
王五
赵六
钱七
列表阻塞操作

如何创建终端连接,因为我用的是可视化工具,所以只需要打开多个查询窗口即可,如下图是打开了两个:
在这里插入图片描述
列表阻塞操作是Redis提供的一种特殊功能,允许客户端在列表为空时进入等待状态,直到有新元素被添加到列表中。这种操作主要通过BLPOP、BRPOP、BRPOPLPUSH等命令实现。
阻塞操作的核心特点是:

  • 当列表有元素时,立即返回元素
  • 当列表为空时,不立即返回,而是等待(阻塞)
  • 可以设置最大等待时间
  • 如果在等待时间内有元素被添加到列表,立即返回该元素
  • 如果等待超时,返回nil
  1. 创建一个新的终端连接到Redis
  2. 在第一个终端中执行阻塞弹出操作,等待"tasks"列表的元素
  3. 在第二个终端中向"tasks"列表添加一个元素
  4. 观察第一个终端的结果
第一个终端:
> blpop tasks 30
第二个终端:
> lpush tasks "紧急任务"
1
回头看第一个终端会输出数据:
> blpop tasks 30
tasks
紧急任务

困难练习

下面所有代码都经过本人验证,放心跑.
只是很简单的去结合场景.如果真是具体业务场景,是要麻烦很多的.
实操目的是熟悉常用列表命令!!!

分页列表

实现一个产品列表的分页功能,包含1000个产品,每页显示10个。
任务:

  1. 创建包含1000个产品的列表
  2. 实现获取第5页产品的功能
  3. 实现获取第50页产品的功能
  4. 比较两次操作的性能差异
# 创建产品列表
DEL products
EVAL "for i=1,1000 do redis.call('RPUSH', 'products', 'product_'..i) end" 0
# 获取第5页(索引40-49)
TIME
LRANGE products 40 49
TIME# 获取第50页(索引490-499)
TIME
LRANGE products 490 499
TIME

获取第5页用时:
在这里插入图片描述

获取第50页用时:
在这里插入图片描述
可以看到第50页的访问时间会明显长于第5页,这说明LRANGE操作的时间复杂度与索引位置成正比,对于大型列表,不适合用LRANGE实现远端分页.
那怎么办呢?下面有个优化方案

# 使用HSCAN实现分页(先将数据转为哈希表)
DEL products_hash
EVAL "for i=1,1000 do redis.call('HSET', 'products_hash', i, 'product_'..i) end" 0
HSCAN products_hash 0 COUNT 10
HSCAN products_hash 490 COUNT 10

为什么呢?
列表分页必须从头遍历到490个元素(获取第50页),页码越大,性能越差.
而用哈希表:

HSCAN products_hash 0 COUNT 10      # 第1页
HSCAN products_hash [cursor] COUNT 10  # 下一页

它使用的是游标(cursor)机制,每次返回一个新游标.
下次从该游标继续扫描,不需要从头开始,性能相对稳定,不受页码影响.
ok,那你可能要问了:
那如果我用哈希查10页和第1000页,查第10页返回的游标对于查1000页有帮助?

游标机制

首先HSCAN的游标机制可能确实容易引起误解,我们可以解释一下它:

  • 游标不是页码:游标不直接对应于"第几页",而是一个内部扫描位置的标识
  • 游标不是线性的:游标值与数据在哈希表中的位置没有线性关系
  • 游标是无状态的:Redis不会记住之前的扫描位置,每次都需要提供游标
    那就是说游标不是页码,且非线性无状态,那我们访问1000页怎么办?
    首先如果说我们直接跳转到1000页,那使用HSCAN是没办法的.
    因为如果我们要访问1000页,使用HSCAN必须要从头开始,执行HSCAN大概1000次!每次使用上一次返回的游标:
# 第1次扫描
HSCAN products_hash 0 COUNT 10  # 返回游标值cursor1# 第2次扫描
HSCAN products_hash cursor1 COUNT 10  # 返回游标值cursor2# 第3次扫描
HSCAN products_hash cursor2 COUNT 10  # 返回游标值cursor3# ... 重复大约997...# 第1000次扫描
HSCAN products_hash cursor999 COUNT 10  # 这才是第1000页的数据

但是即使是这样,HSCAN仍然比LRANGE好!
虽然HSCAN不支持直接跳页,但它仍有重要优势:

  • 增量式扫描:HSCAN每次只锁定少量数据,不会阻塞Redis
  • 内存效率:不需要一次性将大量数据加载到内存
  • 服务器友好:对Redis服务器的影响较小
业务上考虑直接访问任意页

那如果我们要直接访问任意页.怎么办呢? = =
首先从业务上考虑,直接访问任意页实际上是比较少见的,就像我们在一些社交媒体(qq,微博),或者电商平台(淘宝,京东),大家基本上都只关注前几页的热门商品,我个人基本只看前三页(超过第二页都很少).
其次,用户都访问到第1000页还没找到想要的,这个通常意味着用户的搜索策略不精确,你想想都看1000页了要花多长时间,还没找到想看到的,这对于用户是无法容忍的!
最后这种深度分页会导致数据库和缓存压力比较大,从技术上来看会增加技术成本.

那么话说回来,也不能说就一定不会有这样的场景是吧.
比如一些数据分析工具它需要访问特定的数据段,管理后台要定位特定记录,所以还是要考虑有这样业务时如何实现.

如何高效分页

这里只针对中小型数据集(百万级以下),注意是基于有序集合(Sorted Set)去分页.

  • 数据准备
# 清除旧数据
DEL products_sorted# 批量添加产品数据(使用Lua脚本)
EVAL "for i=1,10000 do redis.call('ZADD', 'products_sorted', i, 'product_'..i) end; return 'OK'" 0

在这里插入图片描述

  • 分页公式
起始索引 = (页码 - 1) * 每页大小
结束索引 = 起始索引 + 每页大小 - 1
  • 直接访问任意页
# 获取第1(每页20)
ZRANGE products_sorted 0 19# 获取第50页
ZRANGE products_sorted 980 999# 获取第500页
ZRANGE products_sorted 9980 9999
  • 获取分页元数据
# 获取总记录数
ZCARD products_sorted# 使用Lua脚本获取完整的分页信息
EVAL "
local key = ARGV[1]
local page = tonumber(ARGV[2])
local page_size = tonumber(ARGV[3])local start = (page - 1) * page_size
local stop = start + page_size - 1
local total = redis.call('ZCARD', key)
local total_pages = math.ceil(total / page_size)
local items = redis.call('ZRANGE', key, start, stop)return {total, total_pages, items}
" 0 products_sorted 500 20

当前也不能我说快就快,下面是我测试的在不同数据量下性能表现:
以下是在不同数据量下ZRANGE操作的典型性能表现:

数据量直接访问第1页直接访问第1000页内存占用
1,000条<1ms<1ms~100KB
10,000条<1ms<1ms~1MB
100,000条<1ms1-2ms~10MB
1,000,000条1-2ms2-5ms~100MB

这些数据说明,即使在百万级数据集上,有序集合的分页性能依然非常出色,可以实现毫秒级的响应时间。

局限性

在数据量上:
当数据量超过千万级时,单个ZSET的内存占用会变得相当大,可能影响Redis的整体性能。
条件查询上:
ZSET不支持复杂的多条件查询和过滤,如果需要’价格在100-200之间且评分大于4.5的产品’这样的查询,单纯使用ZSET是无法实现的,需要结合其他数据结构或使用RediSearch模块。
写入性能上:
ZSET的写入操作(ZADD)复杂度为O(log(N)),在数据量极大且写入频繁的场景下,可能成为性能瓶颈

小结

有序集合(ZSET)是Redis中实现中小型数据集分页的最佳选择,主要基于以下几个关键优势:

  • ZSET提供了O(log(N) + M)的时间复杂度来访问任意范围的元素,其中N是集合大小,M是返回的元素数量。这意味着即使在百万级数据上,直接跳转到任意页面的性能依然很高。
  • ZSET内置了排序能力,每个元素都关联一个分数,可以自动按分数排序,无需额外的排序操作。这对于实现’按时间排序’、'按价格排序’等常见分页场景非常便利。
  • ZSET的内部实现结合了跳跃表和哈希表的优势,既能高效地进行范围查询,又能O(1)时间复杂度查找单个元素
  • 对于百万级以下的数据集,ZSET的内存占用是合理的,而且Redis会根据数据量自动选择最优的内部编码方式。存储100万个简单元素的ZSET大约需要100MB左右的内存.

当前对于其他不同场景还有很多方法去实现,但是我都不太熟悉,等后面能力比较强了再回来填坑.

实现限时排行版轮换

实现一个每小时更新一次的热门文章排行榜系统。

  1. 创建当前小时的排行榜
  2. 添加文章到排行榜
  3. 模拟小时更替,保留旧排行榜同时创建新排行榜
  4. 获取当前和前一小时的排行榜
  5. 清理24小时前的排行榜
# 设置当前时间(假设为10点)
SET current_hour 10# 创建当前小时排行榜
DEL trending_articles:10
RPUSH trending_articles:10 "文章A" "文章B" "文章C"# 添加新热门文章
LPUSH trending_articles:10 "文章D" "文章E"# 获取当前排行榜
LRANGE trending_articles:10 0 -1# 模拟小时更替(11点)
SET current_hour 11
DEL trending_articles:11
RPUSH trending_articles:11 "文章E" "文章D" "文章F"# 获取当前和前一小时排行榜
LRANGE trending_articles:11 0 4
LRANGE trending_articles:10 0 4# 清理24小时前的排行榜(假设为前一天11点的数据)
DEL trending_articles:$(redis-cli GET current_hour | awk '{print $1-24}')

消息队列可靠性实现

实现一个可靠的消息队列,确保消息处理失败时不会丢失。

  1. 创建待处理队列和处理中队列
  2. 模拟消费者获取并处理消息
  3. 模拟处理成功和失败的情况
  4. 实现超时未确认的消息回收机制
# 创建队列
DEL pending_tasks processing_tasks completed_tasks
# 添加任务
RPUSH pending_tasks "任务1" "任务2" "任务3" "任务4" "任务5"
# 消费者获取任务
RPOPLPUSH pending_tasks processing_tasks
# 查看两个队列
LRANGE pending_tasks 0 -1
LRANGE processing_tasks 0 -1
# 模拟处理成功
RPOPLPUSH processing_tasks completed_tasks
# 模拟处理失败(将任务放回待处理队列)
RPOPLPUSH processing_tasks pending_tasks
# 实现超时回收(Lua脚本)
EVAL "
local processing = redis.call('LRANGE', 'processing_tasks', 0, -1)
for i, task in ipairs(processing) dolocal task_info = cjson.decode(task)if (tonumber(task_info.timestamp) + 300) < tonumber(ARGV[1]) thenredis.call('RPOPLPUSH', 'processing_tasks', 'pending_tasks')end
end
return 'OK'
" 0 $(date +%s)

分布式锁实现

使用Redis列表实现一个简单的分布式锁机制。

  1. 创建锁列表
  2. 实现获取锁的功能
  3. 实现释放锁的功能
  4. 实现锁超时自动释放
# 创建锁列表
DEL resource_locks# 获取锁(原子操作)
-- 参数说明:
-- KEYS[1]: lock_name = "resource_locks"    -- 锁的名称
-- ARGV[1]: client_id = "client-123"        -- 客户端ID
-- ARGV[2]: timeout = "30"                  -- 锁的超时时间
EVAL "
local lock_name = KEYS[1]
local client_id = ARGV[1]
local timeout = ARGV[2]
-- 将客户端ID和超时时间组合成锁信息
local lock_info = client_id..':'..timeout
-- 检查锁是否存在(通过检查列表长度)
if redis.call('LLEN', lock_name) == 0 then-- 如果列表为空,说明没有锁,则添加锁redis.call('LPUSH', lock_name, lock_info)return 1  -- 获取锁成功
elsereturn 0  -- 锁已被占用,获取失败
end
" 1 resource_locks "client-123" "30"# 释放锁(原子操作)EVAL "
local lock_name = KEYS[1]
local client_id = ARGV[1]local lock_value = redis.call('LINDEX', lock_name, 0)
-- 检查锁是否存在且属于该客户端
if lock_value and string.find(lock_value, client_id) thenredis.call('LPOP', lock_name)return 1
elsereturn 0
end# 锁超时自动释放(定期执行)
-- 参数说明:
-- KEYS[1]: lock_name = "resource_locks"           -- 锁的名称
-- ARGV[1]: current_time = $(date +%s)            -- 当前时间戳
EVAL "
local lock_name = KEYS[1]
local current_time = tonumber(ARGV[1])
-- 获取锁列表中的第一个元素(最新的锁)
local lock_value = redis.call('LINDEX', lock_name, 0)
if lock_value thenlocal parts = {}for part in string.gmatch(lock_value, '[^:]+') dotable.insert(parts, part)endlocal client_id = parts[1]   -- 获取客户端IDlocal timeout = tonumber(parts[2])  -- 获取超时时间if current_time > timeout thenredis.call('LPOP', lock_name)  -- 移除超时的锁return 1      -- 返回1表示锁被释放end
end
return 0     -- 返回0表示锁未被释放 

总结

本来还有一些,但是我写着写着感觉如果不介绍一些rua脚本的内容,现在意义也不大,如果只是练习指令,上面的应该已经足够了.
关于业务场景的,后面我们单独出内容去聊.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/24865.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot 2 后端通用开发模板搭建(异常处理,请求响应)

目录 一、环境准备 二、新建项目 三、整合依赖 1、MyBatis Plus 数据库操作 2、Hutool 工具库 3、Knife4j 接口文档 4、其他依赖 四、通用基础代码 1、自定义异常 2、响应包装类 3、全局异常处理器 4、请求包装类 5、全局跨域配置 补充&#xff1a;设置新建类/接…

实现Python+Django+Transformers库中的BertTokenizer和BertModel来进行BERT预训练,并将其应用于商品推荐功能

一、环境安装准备 #git拉取 bert-base-chinese 文件#创建 虚拟运行环境python -m venv myicrplatenv#刷新source myicrplatenv/bin/activate#python Django 集成nacospip install nacos-sdk-python#安装 Djangopip3 install Django5.1#安装 pymysql settings.py 里面需要 # 强制…

Rk3568驱动开发_点亮led灯代码完善(手动挡)_6

1.实现思路&#xff1a; 应用层打开设备后通过write函数向内核中写值&#xff0c;1代表要打开灯&#xff0c;0代表要关闭灯 Linux配置gpio和控制gpio多了一个虚拟内存映射操作 2.注意事项&#xff1a; 配置和读写操作的时候要谨慎&#xff0c;比如先关掉gpio再注销掉虚拟内存…

线性回归(一)基于Scikit-Learn的简单线性回归

主要参考学习资料&#xff1a; 《机器学习算法的数学解析与Python实现》莫凡 著 前置知识&#xff1a;线性代数-Python 目录 问题背景数学模型假设函数损失函数优化方法训练步骤 代码实现特点 问题背景 回归问题是一类预测连续值的问题&#xff0c;满足这样要求的数学模型称作…

P10108 [GESP202312 六级] 闯关游戏

题目大意 如题 分析 设最佳通关方案为 { s 1 , s 2 , . . . , s k } \{s_1,s_2,...,s_k\} {s1​,s2​,...,sk​}&#xff0c;其中 s i s_i si​ 代表第 i i i 次到达的关卡&#xff08; ≥ N \ge N ≥N 的不算&#xff09;。 当 a k N − 1 a_kN-1 ak​N−1 时&#…

vllm的使用方式,入门教程

vLLM是一个由伯克利大学LMSYS组织开源的大语言模型推理框架&#xff0c;旨在提升实时场景下的大语言模型服务的吞吐与内存使用效率。以下是详细的vLLM使用方式和入门教程&#xff1a; 1. 前期准备 在开始使用vLLM之前&#xff0c;建议先掌握一些基础知识&#xff0c;包括操作…

web的分离不分离:前后端分离与不分离全面分析

让我们一起走向未来 &#x1f393;作者简介&#xff1a;全栈领域优质创作者 &#x1f310;个人主页&#xff1a;百锦再新空间代码工作室 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[1504566…

HDFS扩缩容及数据迁移

1.黑白名单机制 在HDFS中可以通过黑名单、白名单机制进行节点管理&#xff0c;决定数据可以复制/不可以复制到哪些节点。 黑名单通常是指在HDFS中被标记为不可用或不可访问的节点列表&#xff0c;这些节点可能由于硬件故障、网络问题或其他原因而暂时或永久性地无法使用。当一…

数据如何安全“过桥”?分类分级与风险评估,守护数据流通安全

信息化高速发展&#xff0c;数据已成为企业的核心资产&#xff0c;驱动着业务决策、创新与市场竞争力。随着数据开发利用不断深入&#xff0c;常态化的数据流通不仅促进了信息的快速传递与共享&#xff0c;还能帮助企业快速响应市场变化&#xff0c;把握商业机遇&#xff0c;实…

[Web 安全] PHP 反序列化漏洞 —— PHP 序列化 反序列化

关注这个专栏的其他相关笔记&#xff1a;[Web 安全] 反序列化漏洞 - 学习笔记-CSDN博客 0x01&#xff1a;PHP 序列化 — Serialize 序列化就是将对象的状态信息转化为可以存储或传输的形式的过程&#xff0c;在 PHP 中&#xff0c;通常使用 serialize() 函数来完成序列化的操作…

DeepSeek-R1:模型部署与应用实践

深入探索DeepSeek-R1&#xff1a;模型部署与应用实践 在当今人工智能飞速发展的时代&#xff0c;大语言模型&#xff08;LLMs&#xff09;已经成为众多领域的核心驱动力。DeepSeek-R1作为一款备受瞩目的模型&#xff0c;在自然语言处理任务中展现出了强大的能力。本文将深入探…

Java 大视界 -- 基于 Java 的大数据机器学习模型压缩与部署优化(99)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

socket编程详解

TCP报文格式 0. 举例 首先来看一个TCP连接的例子&#xff0c;如图1所示&#xff0c;分别给出了服务器和客户端所调用的API&#xff0c;对这些函数有一个总体认识之后&#xff0c;再逐个对每个函数详细介绍。 图1 创建TCP连接时服务器、客户端调用的API 1. socket() 注&#xf…

企业知识库搭建:14款开源与免费系统选择

本文介绍了以下14 款知识库管理系统&#xff1a;1.Worktile&#xff1b;2.PingCode&#xff1b;3.石墨文档&#xff1b; 4. 语雀&#xff1b; 5. 有道云笔记&#xff1b; 6. Bitrix24&#xff1b; 7. Logseq等。 在如今的数字化时代&#xff0c;企业和团队面临着越来越多的信息…

Spring Cloud Alibaba学习 3- Sentinel入门使用

Spring Cloud Alibaba学习 3- Sentinel入门使用 中文文档参考&#xff1a;Sentinel中文文档 一. SpringCloud整合Sentinel 1.1 下载Sentinel-Dashboard Sentinel下载地址&#xff1a;Sentinel-Dashboard 到下载目录&#xff0c;cmd输入 java -jar sentinel-dashboard-1.8…

STM32——HAL库开发笔记23(定时器4—输入捕获)(参考来源:b站铁头山羊)

定时器有四个通道&#xff0c;这些通道既可以用来作为输入&#xff0c;又可以作为输出。做输入的时候&#xff0c;可以使用定时器对外部输入的信号的时间参数进行测量&#xff1b;做输出的时候&#xff0c;可以使用定时器向外输出精确定时的方波信号。 一、输入捕获 的基本原理…

Spring MVC框架六:Ajax技术

精心整理了最新的面试资料&#xff0c;有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 简介 jQuery.ajax Ajax原理 结语 创作不易&#xff0c;希望能对大家给予帮助 想要获取更多资源? 点击链接获取

通过返回的key值匹配字典中的value值

需求 页面中上面搜索项有获取字典枚举接口&#xff0c;table表格中也有根据key匹配字典中的value 方案一 需要做到的要求 这里上面下拉列表是一个组件获取的字典&#xff0c;下面也是通过字典匹配&#xff0c;所以尽量统一封装一个函数&#xff0c;每个组件保证最少变动tabl…

Python游戏编程之赛车游戏6-2

3.2 move()方法的定义 Player类的move()方法用于玩家控制汽车左右移动&#xff0c;当玩家点击键盘上的左右按键时&#xff0c;汽车会相应地进行左右移动。 move()方法的代码如图7所示。 图7 move()方法的代码 其中&#xff0c;第20行代码通过pygame.key.get_pressed()函数获…

通俗易懂:RustDesk Server的搭建及使用

最近有很多远程桌面连接的需求&#xff0c;使用花生壳、topdesk等现有的远程控制又有数量上的限制&#xff0c;因此利用公司现有的具有固定IP地址的服务器&#xff0c;搭建了一台 RustDesk Server来解决工作中的痛点。 结论是丝毫不输哪些收费的软件&#xff0c;不论是剪切板、…