了解Redis数据结构——链表(linked-list)

本文深入解析Redis中链表结构的应用,介绍其双向特性及各种操作命令,如lpush、rpush、lindex等,同时演示了如何通过Java进行链表操作,包括节点插入、删除、查询等,并强调了链表命令的进程安全性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

链表结构是Redis中的一个常用的结构,他可以存储多个字符串,而且它是有序的,能够存储40多亿个节点。Redis链表是双向的,因此既可以从左到右,也可以从右到左遍历它存储的节点,链表结构如下:
在这里插入图片描述
由于它是双向链表,所以它的读性能就会相对丧失,而插入和删除就会显得很便利。它的增删操作与双向链表同。

因为是双向链表结构,所以Redis链表命令分为左操作和右操作两种命令,左操作意味着是从左到右,右操作意味着是从右到左。Redis关于链表的命令如下:

  • lpush key node1 [node2…]:把节点node1加入到链表的最左端,如果是node1,node2…noden这样的加入,那么链表的开头从左到右的顺序是noden…,node2,node1。
  • rpush key node1 [node2…]:把节点node1加入到链表的最右端,如果是node1,node2…noden这样的加入,那么链表的开头从左到右的顺序是noden1,node2…noden。
  • lindex key index:从左往右读取下标为index的节点,返回节点字符串,从0开始算。
  • llen key:求链表的长度。返回链表节点数。
  • lpop key:删除左边第一个节点,并将其返回。
  • rpop key:删除右边第一个节点,并将其返回。
  • linsert key before|after pivot node:插入一个节点node,并且可以指定在值为pivot node的结点的前面或者后面。如果list不存在,则报错;如果没有值为对应pivot node,也会插入失败。
  • lpushx list node:如果存在key为list的链表,则插入节点node,并且作为从左到右的第一个节点,如果list不存在,则失败。
  • rpushx list node:如果存在key为list的链表,则插入节点node,并且作为从右到左的第一个节点,如果list不存在,则失败。
  • lrange list start end:获取链表从start下标到end下标的节点数,包含start和end下标的值。
  • lrem list count value:如果count为0,则删除所有值为value的节点;如果count不为0,则先对count取绝对值,假设极为abs,然后从左到右删除不大于abs个等于value的节点。
  • lset key index node:设置下标为index的节点的值为node。
  • ltrim key start stop:修建链表,只保留从start到stop的区间的节点,其余的都删除掉,包含start和end的下标的节点会保留。

但是上述的所有方法都是进程不安全的,因为当我们操作这些命令的时候,其他的Redis的客户端也可能操作同一个链表,这样就会造成并发数据安全和一致性的问题。为了解决这些问题,Redis提供了链表的阻塞命令,它们在运行的时候,会给链表加锁,以保证操作链表的命令安全性。链表的阻塞命令如下:

  • blpop key timeout:移出并获取列表的第一个元素,如果链表没有元素会阻塞链表直到等待超时或发现可弹出元素为止。相对于lpop命令,它是安全的。
  • brpop key timeout:移出并获取列表的最后一个元素,如果链表没有元素会阻塞链表直到等待超时或发现可弹出元素为止。相对于rpop命令,它是安全的。
  • rpoplpush key src dest:按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表的最左边。不能设置超时时间。
  • brpoplpush key src dest timeout:按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表的最左边。并可以设置超时时间。

我们在用一个简单的demo来实现这些功能,代码如下:

redisSpring-cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="50" />
        <property name="maxTotal" value="100" />
        <property name="maxWaitMillis" value="20000" />
    </bean>

    <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />

    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="localhost" />
        <property name="port" value="6379" />
        <property name="password" value="123456" />
        <property name="poolConfig" ref="poolConfig" />
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="defaultSerializer" ref="stringRedisSerializer" />
        <property name="keySerializer" ref="stringRedisSerializer" />
        <property name="valueSerializer" ref="stringRedisSerializer" />
    </bean>
</beans>

testList.java

package com.ssm.redis1.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.connection.RedisListCommands;
import org.springframework.data.redis.core.RedisTemplate;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import static com.sun.xml.internal.messaging.saaj.packaging.mime.util.ASCIIUtility.getBytes;

public class testList {
    public static void main(String[] args){
        ApplicationContext applicationContext=new ClassPathXmlApplicationContext("redisSpring-cfg.xml");
        RedisTemplate redisTemplate=applicationContext.getBean(RedisTemplate.class);
        try {
            //删除链表,以便我们反复测试
            redisTemplate.delete("list");
            //把node3插入链表list
            redisTemplate.opsForList().leftPush("list","node3");
            List<String> nodeList=new ArrayList<String>();
            for(int i=2;i>=1;i--){
                nodeList.add("node"+1);
            }
            //相当于lpush把多个节点从左插入链表
            redisTemplate.opsForList().leftPushAll("list",nodeList);
            //从右边插入一个节点
            redisTemplate.opsForList().rightPush("list","node4");
            //获取下标为0的节点
            String node1=(String)redisTemplate.opsForList().index("list",0);
            //获取链表的长度
            long length=redisTemplate.opsForList().size("list");
            //从左边弹出一个节点
            String lpop=(String)redisTemplate.opsForList().leftPop("list");
            //从右边弹出一个节点
            String rpop=(String)redisTemplate.opsForList().rightPop("list");
            //注意,需要使用更为底层的命令才能操作linsert命令
            //使用linsert命令在node2前插入一个节点
            redisTemplate.getConnectionFactory().getConnection().lInsert(getBytes("utf-8"),
                                                                         RedisListCommands.Position.BEFORE,
                                                                         "node2".getBytes("utf-8"),
                                                                         "before_node".getBytes("utf-8"));

            //使用linsert命令在node2后插入一个节点
            redisTemplate.getConnectionFactory().getConnection().lInsert(getBytes("utf-8"),
                                                                         RedisListCommands.Position.BEFORE,
                                                                         "node2".getBytes("utf-8"),
                                                                         "after_node".getBytes("utf-8"));

            //判断list是否存在,如果存在则从左边插入head节点
            redisTemplate.opsForList().leftPushIfPresent("list","head");
            //判断list是否存在,如果存在则从右边边插入end节点
            redisTemplate.opsForList().rightPushIfPresent("list","end");
            //从左到右,或者下摆哦为0搭配10的节点元素
            List valueList=redisTemplate.opsForList().range("list",0,10);
            nodeList.clear();
            for(int i=1;i<3;i++){
                nodeList.add("node");
            }
            //在链表左边插入三个值为node的节点
            redisTemplate.opsForList().leftPushAll("list",nodeList);
            //从左到右删除至多三个node节点
            redisTemplate.opsForList().remove("list",3,"node");
            //给链表下标为0的节点设置新值
            redisTemplate.opsForList().set("list",0,"new_head_node");
        }catch (UnsupportedEncodingException ex){
            ex.printStackTrace();
        }
        //打印链表数据
        printList(redisTemplate,"list");
    }

    private static void printList(RedisTemplate redisTemplate, String list) {
        //链表长度
        Long size=redisTemplate.opsForList().size(list);
        //获取整个链表的值
        List valueList=redisTemplate.opsForList().range(list,0,size);
        //打印
        System.out.println(valueList);
    }
}

运行结果:
在这里插入图片描述

### RedisList 数据结构的特性和使用 #### 1. 底层实现 Redis 的 `List` 数据结构基于两种底层实现方式: - **双向链表 (Linked List)** 和 **压缩列表 (Ziplist)** 是早期版本中的主要实现形式[^1]。 -Redis 5.0 起引入了新的数据结构 `listpack`,它替代了原有的 `ziplist`,进一步优化了内存占用和性能表现[^2]。 这两种实现的选择取决于存储对象的数量以及单个对象的大小。当满足特定条件时(如元素较少且值较短),会优先采用更加节省空间的 `listpack` 或之前的 `ziplist`;否则则切换到标准的双向链表--- #### 2. 基本操作命令 以下是常用的 Redis `List` 操作命令及其功能: - **LPUSH/RPUSH**: 将一个或多个值插入到列表头部/尾部。 ```bash LPUSH mylist "value1" RPUSH mylist "value2" ``` - **LPOP/RPOP**: 移除并返回列表的第一个/最后一个元素。 ```bash LPOP mylist RPOP mylist ``` - **LRANGE**: 返回指定索引范围内的元素。 ```bash LRANGE mylist 0 9 ``` 这里表示获取从第 0 到第 9 位之间的所有元素。 - **LLEN**: 获取列表中元素的总数量。 如果目标键不存在,则返回 0[^4]。 ```bash LLEN mylist ``` - **LINDEX**: 查找某位置上的具体项。 若超出边界或者该键为空,则得到特殊标记 nil/null。 ```bash LINDEX mylist 3 ``` - **LTRIM**: 截断保留部分子序列之外的内容会被丢弃掉。 ```bash LTRIM mylist 1 5 ``` - **BLPOP/BRPOP**: 阻塞式的弹出左侧/右侧第一个可用条目直到超时为止。 它们非常适合用于消息队列场景下消费者端轮询等待新任务到来的情况。 --- #### 3. 时间复杂度分析 对于大多数上述提到的操作而言,在理想情况下它们的时间消耗都接近 O(1),因为无论是通过指针快速定位首末端节点还是借助连续地址访问中间片段均能高效完成相应动作。然而需要注意的是随着规模增大某些涉及全量扫描的任务比如排序可能会退化至线性甚至平方级别效率下降明显. --- #### 4. 场景应用举例 由于其支持先进先出(FIFO)及后进先出(LIFO)模式所以广泛应用于缓存管理、工作流调度等领域之中。例如可以构建简单的生产者-消费者模型其中一方不断往共享容器里面填充项目而另一方负责依次取出处理直至结束整个流程形成闭环控制机制从而达到异步解耦的效果提升整体吞吐能力减少阻塞性延时现象发生几率. 另外还可以利用持久化的特性保存历史记录便于后续审计追踪等功能扩展需求考虑周全设计合理架构方案解决实际业务痛点难点问题提高开发运维人员工作效率降低成本支出获得更大收益回报率等等诸多方面都有非常重要的意义价值所在值得深入研究探讨学习掌握核心技术要点精髓之处才能更好地服务于社会大众群体创造更多财富机会可能性无限广阔前景光明灿烂辉煌未来可期! ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值