Redis链表
链表是Redis中一个常用的结构,可以存储多个字符串,而且它是有序的。Redis链表是双向的,可以从左到右,也可以从右到左遍历它存储的节点
由于是双向链表,所以只能从左到右,或者从右到左访问和操作链表里面的数据节点。但是使用链表结构就意味着读性能的丧失,所以要在大量数据中找到一个节点的操作性能是不佳的,因为链表只能从一个方向中去遍历所要节点,比如从查找节点10000开始查询,它需要按照节点1、节点2、节点3……直至节点10000,这样的顺序查找,然后把一个个节点和你给出的值对比,才能确定节点所在。如果这个链表很大,如有上百万个节点,可能需要遍历几十万次才能找到所需的节点,显然查找性能是不佳的
链表的优势在于插入和删除的便利,因为链表的数据节点是分配在不同的内存区域的,并不连续,只是根据上一个节点保存下一个节点的顺序来索引而已,无需移动元素
因为是双向链表结构,所以Redis链表命令分为左操作和右操作两种命令,左操作就意味着从左到右,右操作就意味着从右到左
命令 | 说明 | 备注 |
---|---|---|
LPUSH key value1 [value2] | 把节点node1加入到链表最左边 | 如果是node1、node2….noden这样加入,那么链表开头从左到右的顺序是noden…node2、node1 |
RPUSH key value1 [value2] | 把节点node1加入到链表最右边 | 如果是node1、node2….noden这样加入,那么链表开头从左到右的顺序是node1、node2…noden |
LINDEX key index | 读取下标为index的节点 | 返回节点字符串,从0开始算 |
LLEN key | 链表的长度 | 返回链表节点数 |
LPOP key | 删除左边第一个节点,并将其返回 | |
RPOP key | 删除右边第一个节点,并将其返回 | |
LINSERT key BEFORE | AFTER pivot value | 插入一个节点node,并且可以指定在值为pivot的节点的前面或者后面 |
LPUSHX list node | 如果存在key未list的链表,则插入节点node,并且作为从左到右的第一个节点 | 如果list不存在,则失败 |
RPUSHX list node | 如果存在key未list的链表,则插入节点node,并且作为从左到右的最后一个节点 | 如果list不存在,则失败 |
LRANGE key start stop | 获取list从start下标到end下标的节点值 | 包含start和end下标的值 |
LREM key count value | 如果count为0,则删除所有值等于value的节点;如果count不是0,则先对count取绝对值,假设记为abs,然后从左到右删除不大于abs个等于value的节点 | 注意,count为整数,如果是负数,则redis会先取其绝对值,然后传递到后台操作 |
LSET key index value | 设置列表下标为index的节点的值为node | |
LTRIM key start stop | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除 | 包含start和end的下标的节点会保留 |
如下的例子:
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
try {
// 删除链表
redisTemplate.delete("list");
// 把node3插入链表list
redisTemplate.opsForList().leftPush("list", "node3");
printList(redisTemplate, "list");//[node3]
// 相当于lpush把多个值从左插入链表
List<String> nodeList = new ArrayList<String>();
for (int i = 2; i >= 1; i--) {
nodeList.add("node" + i);
}
redisTemplate.opsForList().leftPushAll("list", nodeList);
printList(redisTemplate, "list");//[node1, node2, node3]
// 从右边插入一个节点
redisTemplate.opsForList().rightPush("list", "node4");
printList(redisTemplate, "list");//[node1, node2, node3, node4]
// 获取下标为0的节点
String node1 = (String) redisTemplate.opsForList().index("list", 0);
System.out.println(node1); //node1
// 获取链表的长度
long size = redisTemplate.opsForList().size("list");
System.out.println(size); //4
// 从左边弹出一个节点
String lpop = (String) redisTemplate.opsForList().leftPop("list");
System.out.println(lpop);//node1
printList(redisTemplate, "list");//[node2, node3, node4]
// 从右边弹出一个节点
String rpop = (String) redisTemplate.opsForList().rightPop("list");
System.out.println(rpop);//node4
printList(redisTemplate, "list");//[node2, node3]
// 使用linsert命令在node2前插入一个节点
redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),
RedisListCommands.Position.BEFORE, "node2".getBytes("utf-8"), "before_node".getBytes("utf-8"));
printList(redisTemplate, "list");//[before_node, node2, node3]
// 使用linsert命令在node2后插入一个节点
redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),
RedisListCommands.Position.AFTER, "node2".getBytes("utf-8"), "after_node".getBytes("utf-8"));
printList(redisTemplate, "list");//[before_node, node2, after_node, node3]
// 判断list是否存在,如果存在则在左边插入head节点
redisTemplate.opsForList().leftPushIfPresent("list", "head");
printList(redisTemplate, "list");//[head, before_node, node2, after_node, node3]
// 判断list是否存在,如果存在则在右边插入end节点
redisTemplate.opsForList().rightPushIfPresent("list", "end");
printList(redisTemplate, "list");//[head, before_node, node2, after_node, node3, end]
// 从左到右,获取下标从0到10的节点元素
List valueList = redisTemplate.opsForList().range("list", 0, 10);
System.out.println(valueList);//[head, before_node, node2, after_node, node3, end]
nodeList.clear();
// 在链表的左边插入三个值为node的节点
for (int i = 1; i <= 3; i++) {
nodeList.add("node");
}
redisTemplate.opsForList().leftPushAll("list", nodeList);
printList(redisTemplate, "list");//[node, node, node, head, before_node, node2, after_node, node3, end]
// 从左到右删除至多三个node节点
redisTemplate.opsForList().remove("list", 3, "node");
printList(redisTemplate, "list");//[head, before_node, node2, after_node, node3, end]
// 给下标为0的节点设置新值
redisTemplate.opsForList().set("list", 0, "new_head_value");
printList(redisTemplate, "list");//[new_head_value, before_node, node2, after_node, node3, end]
} catch (UnsupportedEncodingException ex) {
ex.printStackTrace();
}
}
public static void printList(RedisTemplate redisTemplate, String key) {
// 链表长度
Long size = redisTemplate.opsForList().size(key);
// 获取整个链表的值
List valueList = redisTemplate.opsForList().range(key, 0, size);
// 打印
System.out.println(valueList);
}
需要指出的是,之前的这些操作都是进程不安全的,因为当我们操作这些命令的时候,其他Redis的客户端也可能操作同一个链表,这样就造成并发数据和一致性的问题,尤其是当你操作一个数据量不小的链表结构时,常常会遇到这样的问题。为了克服这些问题,Redis提供了链表的阻塞命令,它们在运行的时候,会给链表加锁,以保证操作链表的命令安全性
命令 | 说明 | 备注 |
---|---|---|
BLPOP key1 [key2 ] timeout | 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | 相对于lpop命令,它的操作是进程安全的 |
BRPOP key1 [key2 ] timeout | 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | 相对于rpop命令,它的操作是进程安全的 |
BRPOPLPUSH source destination timeout | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | 可设置超时时间 |
RPOPLPUSH source destination | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | 不可设置超时时间 |
public static void testBList() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
// 清空数据,可以重复测试
redisTemplate.delete("list1");
redisTemplate.delete("list2");
// 初始化链表list1
List<String> nodeList = new ArrayList<String>();
for (int i = 1; i <= 5; i++) {
nodeList.add("node" + i);
}
redisTemplate.opsForList().leftPushAll("list1", nodeList);
// Spring使用参数超时时间作为阻塞命令区分,等价于blpop命令,并且可以设置时间参数
redisTemplate.opsForList().leftPop("list1", 1, TimeUnit.SECONDS);
// Spring使用参数超时时间作为阻塞命令区分,等价于brpop命令,并且可以设置时间参数
redisTemplate.opsForList().rightPop("list1", 1, TimeUnit.SECONDS);
nodeList.clear();
// 初始化链表list2
for (int i = 1; i <= 3; i++) {
nodeList.add("data" + i);
}
redisTemplate.opsForList().leftPushAll("list2", nodeList);
// 相当于rpoplpush命令,弹出list1最右边的节点,插入到list2最左边
redisTemplate.opsForList().rightPopAndLeftPush("list1", "list2");
// 相当于brpoplpush命令,弹出list1最右边的节点,插入到list2最左边
redisTemplate.opsForList().rightPopAndLeftPush("list1", "list2", 1, TimeUnit.SECONDS);
// 打印链表数据
printList(redisTemplate, "list1");
printList(redisTemplate, "list2");
}