六: redis的list数据类型
list是一个列表元素存在重复,可以作为队列,栈:列表的双端都是可以进行操作的。
命令
lpush
- 解释: 往列表左端插入元素,如果键不存在则创建再插入元素,返回列表的长度
- 用法: lpush key value [value …]
2.4版本及之后的版本支持多个value,之前的只支持单个value - 示例:
127.0.0.1:6379> lpush key1 a
(integer)1
127.0.0.0.1:6379> lpush key1 1 2 3 4 5
(integer)6
127.0.0.1:6379> lrange key1 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
6) "a"
- 源码
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"lpush",lpushCommand,3,REDIS_CMD_DENYOOM,NULL,1,1,1}
}
/**
** redis.h
** 列表的头(左端)和尾(右端)
**/
#define REDIS_HEAD 0
#define REDIS_TAIL 1
/**
**
** t_list.c
**/
void lpushCommand(redisClient *c) {
pushGenericCommand(c,REDIS_HEAD);
}
1. 键是否已经存在
2. 键不存在
2.1 判断是否有阻塞等待的客户端,把插入的值直接返回给阻塞的客户端们
2.2 键不存在,创建内部编码为ziplist的列表
2.3 键插入到redis的字典中
3. 键存在
3.1 键存在但是数据类型不是列表,返回错误
3.2 判断是否有阻塞等待的客户端,把插入的值直接返回给阻塞的客户端们
4. 根据where插入值到键的左端/右端
/**
** 通用的插入元素方法
** where = 0 : 左端插入
** where = 1 : 右端插入
**/
void pushGenericCommand(redisClient *c, int where) {
//1. 键是否已经存在
robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
c->argv[2] = tryObjectEncoding(c->argv[2]);
//2. 键不存在
if (lobj == NULL) {
//2.1 判断是否有阻塞等待的客户端
if (handleClientsWaitingListPush(c,c->argv[1],c->argv[2])) {
addReply(c,shared.cone);
return;
}
//2.2 键不存在,创建内部编码为ziplist的列表
lobj = createZiplistObject();
//2.3 键插入到redis的字典中
dbAdd(c->db,c->argv[1],lobj);
} else {
//3. 键存在
if (lobj->type != REDIS_LIST) {
//3.1 键存在但是数据类型不是列表,返回错误
addReply(c,shared.wrongtypeerr);
return;
}
//3.2 判断是否有阻塞等待的客户端
if (handleClientsWaitingListPush(c,c->argv[1],c->argv[2])) {
touchWatchedKey(c->db,c->argv[1]);
addReply(c,shared.cone);
return;
}
}
//4. 根据where插入值到键的左端/右端
listTypePush(lobj,c->argv[2],where);
addReplyLongLong(c,listTypeLength(lobj));
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
}
/**
** object.c
** 创建一个新的zipList
**/
robj *createZiplistObject(void) {
//1.创建新的ziplist
unsigned char *zl = ziplistNew();
//2.创建键的对象
robj *o = createObject(REDIS_LIST,zl);
//3.设置内部编码为ziplist
o->encoding = REDIS_ENCODING_ZIPLIST;
return o;
}
/**
** ziplist.c
** 初始化ziplist
**/
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = bytes;
ZIPLIST_TAIL_OFFSET(zl) = ZIPLIST_HEADER_SIZE;
ZIPLIST_LENGTH(zl) = 0;
zl[bytes-1] = ZIP_END;
return zl;
}
rpush
- 解释: 往列表右端(尾部)插入元素,如果键不存在则创建再插入元素,返回列表的长度
- 用法: rpush key value [value …]
2.4版本及之后的版本支持多个value,之前的只支持单个value - 示例:
127.0.0.1:6379> rpush key1 a
(integer)1
127.0.0.0.1:6379> rpush key1 1 2 3 4 5
(integer)6
127.0.0.1:6379> lrange key1 0 -1
1) "a"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5
- 源码
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"lpush",lpushCommand,3,REDIS_CMD_DENYOOM,NULL,1,1,1}
}
/**
** redis.h
** 列表的头(左端)和尾(右端)
**/
#define REDIS_HEAD 0
#define REDIS_TAIL 1
/**
**
** t_list.c
**/
void lpushCommand(redisClient *c) {
pushGenericCommand(c,REDIS_TAIL);
}
pushGenericCommand方法参考lpush的源码分析
linsert
- 解释:在数据类型为列表的键的指定元素之前/之后插入元素,成功返回列表当前长度
如果键不存在,元素不存在则不插入,如果指定元素有多个,
则before在最左端的元素之前插入,after在最右端的元素之前插入
注意这个操作的时间复杂的度是O(N) - 用法: linsert key before|after pivot value
- 示例:
127.0.0.1:6379> linsert key1 before a b
(integer) 1
127.0.0.1:6379> lpush key1 a
(integer)1
127.0.0.1:6379> linsert key1 before a 1
(integer)2
127.0.0.1:6379> lrange key1 0 -1
1)"1"
2)"a"
127.0.0.1:6379> linsert key1 after a 2
(integer)3
127.0.0.1:6379> lrange key1 0 -1
1)"1"
2)"a"
3)"2"
- 源码
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"linsert",linsertCommand,5,REDIS_CMD_DENYOOM,NULL,1,1,1}
}
1. 要插入的元素值编码
2. 判断是左插入还是右插入,插入元素
3. 如果都不是返回语法错误
/**
** t_list.c
**
**/
void linsertCommand(redisClient *c) {
//1. 要插入的元素值编码
c->argv[4] = tryObjectEncoding(c->argv[4]);
//2.判断是左插入还是右插入,插入元素
if (strcasecmp(c->argv[2]->ptr,"after") == 0) {
pushxGenericCommand(c,c->argv[3],c->argv[4],REDIS_TAIL);
} else if (strcasecmp(c->argv[2]->ptr,"before") == 0) {
pushxGenericCommand(c,c->argv[3],c->argv[4],REDIS_HEAD);
} else {
//3. 如果都不是返回语法错误
addReply(c,shared.syntaxerr);
}
}
- c 客户端对象 在refval之前或之后(where 0:左端 1:右端)插入val
- 查询键是否存在以及是否数据类型是列表,否则返回
- 如果指定元素refval不为Null则迭代查询元素进行插入
- 创建迭代器
- 如果指定元素refval存在,则根据where的方向进行插入元素,设置inserted插入
- 释放迭代器
- 如果插入元素成功,ziplist的元素大于512,则ziplist转为linkedlist
8.没有插入元素直接返回-1 - refval为null,则根据where的方向插入元素
/**
** t_list.c
**
**/
void pushxGenericCommand(redisClient *c, robj *refval, robj *val, int where) {
//1. c 客户端对象 在refval之前或之后(where 0:左端 1:右端)插入val
robj *subject;
listTypeIterator *iter;
listTypeEntry entry;
int inserted = 0;
//2. 查询键是否存在以及是否数据类型是列表,否则返回
if ((subject = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,subject,REDIS_LIST)) return;
//3. 如果指定元素refval不为Null则迭代查询元素进行插入
if (refval != NULL) {
//4. 创建迭代器
iter = listTypeInitIterator(subject,0,REDIS_TAIL);
while (listTypeNext(iter,&entry)) {
if (listTypeEqual(&entry,refval)) {
//5. 如果指定元素refval存在,则根据where的方向进行插入元素,设置inserted插入
listTypeInsert(&entry,val,where);
inserted = 1;
break;
}
}
//6. 释放迭代器
listTypeReleaseIterator(iter);
if (inserted) {
//7. 如果插入元素成功,ziplist的元素大于512,则ziplist转为linkedlist
/* Check if the length exceeds the ziplist length threshold. */
if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
ziplistLen(subject->ptr) > server.list_max_ziplist_entries)
listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
} else {
// 8.没有插入元素直接返回-1
/* Notify client of a failed insert */
addReply(c,shared.cnegone);
return;
}
} else {
//9. refval为null,则根据where的方向插入元素
listTypePush(subject,val,where);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
}
addReplyLongLong(c,listTypeLength(subject));
}
lrange
- 解释: 从左端到右端返回数据类型是列表的键的指定范围内(包含end的元素)的元素
如果键不存在返回错误信息
最左端元素索引为0,最右端索引为-1,如果start的大于实际的end则返回空
如果end大于实际的end,则返回start到列表的右末尾 - 用法: lrange key start stop
- 示例:
127.0.0.1:6379> lrange key1 0 -1
(error) WRONGTYPE Operation against a key holding the wrong kind of vlaue
127.0.0.1:6379> lpush key1 a b c d e
(integer)5
127.0.0.1:6379> lrange key1 0 -1
1)"e"
2)"d"
3)"c"
4)"b"
5)"a"
127.0.0.1:6379> lrange key1 0 1
1)"e"
2)"d"
``
- 源码
1. 得到start,end的值
2. 查找键是否存在以及是否是list类型,否则返回
3. 键键中元素的个数
4. 如果start,end为负数转换为正数从左端到右端返回元素
5. 如果转为正数后的start大于end或start大于等于list的长度(索引从0开始)返回空
6. 如果end查过了List的长度,end等于最后一个元素的索引
7. 计算返回元素的个数
8. 内部编码为ziplist
8.1 指针指向ziplist的第start个元素
8.2 迭代ziplist,返回元素的值
9. 内部编码为Linklist,迭代返回元素
10. 内部编码不是zipList也不是linkList返回错误
```c
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"lrange",lrangeCommand,4,0,NULL,1,1,1}
}
/**
** t_list.c
**
**/
void lrangeCommand(redisClient *c) {
robj *o;
//1. 得到start,end的值
int start = atoi(c->argv[2]->ptr);
int end = atoi(c->argv[3]->ptr);
int llen;
int rangelen;
//2. 查找键是否存在以及是否是list类型,否则返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL
|| checkType(c,o,REDIS_LIST)) return;
//3. 键键中元素的个数
llen = listTypeLength(o);
//4. 如果start,end为负数转换为正数从左端到右端返回元素
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
//5. 如果转为正数后的start大于end或start大于等于list的长度(索引从0开始)返回空
if (start > end || start >= llen) {
addReply(c,shared.emptymultibulk);
return;
}
//6. 如果end查过了List的长度,end等于最后一个元素的索引
if (end >= llen) end = llen-1;
//7. 计算返回元素的个数
rangelen = (end-start)+1;
addReplyMultiBulkLen(c,rangelen);
//8. 内部编码为ziplist
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
//8.1 指针指向ziplist的第start个元素
unsigned char *p = ziplistIndex(o->ptr,start);
unsigned char *vstr;
unsigned int vlen;
long long vlong;
while(rangelen--) {
//8.2 迭代ziplist,返回元素的值
ziplistGet(p,&vstr,&vlen,&vlong);
if (vstr) {
addReplyBulkCBuffer(c,vstr,vlen);
} else {
addReplyBulkLongLong(c,vlong);
}
p = ziplistNext(o->ptr,p);
}
} else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
//9. 内部编码为Linklist,迭代返回元素
listNode *ln;
if (start > llen/2) start -= llen;
ln = listIndex(o->ptr,start);
while(rangelen--) {
addReplyBulk(c,ln->value);
ln = ln->next;
}
} else {
//10. 内部编码不是zipList也不是linkList返回错误
redisPanic("List encoding is not LINKEDLIST nor ZIPLIST!");
}
}
lindex
- 解释: 在数据类型为list的键中根据索返回元素,
索引从0开始,0表示最左端的元素,如果索引超
范围,返回nil - 用法: lindex key index
- 示例:
127.0.0.1:6379 > lindex key1 0
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> lpush key1 a
(integer)1
127.0.0.1:6379> lpush key1 b
(integer)2
127.0.0.1:6379> lindex key1 0
"b"
127.0.0.1:6379> lindex key1 1
"a"
127.0.0.1:6379. lindex key1 2
(nil)
- 源码
- 查找键是否存在以及是否是list类型,否则直接返回
- 输入的第二个参数字符串转换为整型数
- 如果内部编码为ziplist,根据索引在ziplist查找值
- 内部编码为LinkList,根据索引查找节点
- 内部编码不是zipList也不是Linklist返回错误
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"lindex",lindexCommand,3,0,NULL,1,1,1}
}
/**
** t_list.c
**
**/
void lindexCommand(redisClient *c) {
//1. 查找键是否存在以及是否是list类型,否则直接返回
robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk);
if (o == NULL || checkType(c,o,REDIS_LIST)) return;
//2. 输入的第二个参数字符串转换为整型数
int index = atoi(c->argv[2]->ptr);
robj *value = NULL;
//3. 如果内部编码为ziplist,根据索引在ziplist查找值
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
unsigned char *p;
unsigned char *vstr;
unsigned int vlen;
long long vlong;
p = ziplistIndex(o->ptr,index);
if (ziplistGet(p,&vstr,&vlen,&vlong)) {
if (vstr) {
value = createStringObject((char*)vstr,vlen);
} else {
value = createStringObjectFromLongLong(vlong);
}
addReplyBulk(c,value);
decrRefCount(value);
} else {
addReply(c,shared.nullbulk);
}
} else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
//4. 内部编码为LinkList,根据索引查找节点
listNode *ln = listIndex(o->ptr,index);
if (ln != NULL) {
value = listNodeValue(ln);
addReplyBulk(c,value);
} else {
addReply(c,shared.nullbulk);
}
} else {
//5. 内部编码不是zipList也不是Linklist返回错误
redisPanic("Unknown list encoding");
}
}
llen
- 解释: 返回数据类型为list的键的元素个数
如果键不存在返回0,如果键存在但不是list数据类型返回错误 - 用法: llen key
- 示例:
127.0.0.1:6379> llen key1
(integer) 0
127.0.0.1:6379> lpush key1 a
(integer) 1
127.0.0.1:6379> lpush key1 b
(integer) 2
127.0.0.1:6379> llen key1
(integer) 2
- 源码
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"llen",llenCommand,2,0,NULL,1,1,1}
}
/**
** t_list.c
**
**/
void llenCommand(redisClient *c) {
robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.czero);
if (o == NULL || checkType(c,o,REDIS_LIST)) return;
addReplyLongLong(c,listTypeLength(o));
}
unsigned long listTypeLength(robj *subject) {
//1. 内部编码是ziplist,在ziplist中查找
if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
return ziplistLen(subject->ptr);
//2. 内部编码是linklist, 在linklist中查找
} else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
return listLength((list*)subject->ptr);
} else {
redisPanic("Unknown list encoding");
}
}
lset
- 解释: 更新数据类型为list的某个索引的值
如果索引超出了范围,会报错。 - 用法: lset key index value
- 示例:
127.0.0.1:6379> lpush key1 1
(integer) 1
127.0.0.1:6379> lpush key1 2
(integer)2
127.0.0.1:6379> lpush key1 3
(integer)3
127.0.0.1:6379> lrange 0 -1
1) 3
2) 2
3) 1
127.0.0.1:6379> lset key1 0 4
OK
127.0.0.1:6379> lrange key1 0 -1
1) 4
2) 2
3) 1
127.0.0.1:6379> lset key1 5 7
(error) ERR index out of range
- 源码:
- 查询键是否存在以及是否数据类型是列表,否则返回
- 传入的第二个参数index转为int,第三个参数value转为robj对象
- 如果内部编码是ziplist
3.1 查找索引是否存在
3.2 元素不存在,返回错误错误信息
3.3 更新元素的值 - 如果内部编码是linklist
4.1 元素不存在,返回错误信息
4.2 元素存在,更新值 - 不是ziplist,quicklist返回错误
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"lset",lsetCommand,4,REDIS_CMD_DENYOOM,NULL,1,1,1}
}
/**
** t_list.c
**
**/
void lsetCommand(redisClient *c) {
//1. 查询键是否存在以及是否数据类型是列表,否则返回
robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr);
if (o == NULL || checkType(c,o,REDIS_LIST)) return;
//2. 传入的第二个参数index转为int,第三个参数value转为robj对象
int index = atoi(c->argv[2]->ptr);
robj *value = (c->argv[3] = tryObjectEncoding(c->argv[3]));
listTypeTryConversion(o,value);
//3. 如果内部编码是ziplist
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
unsigned char *p, *zl = o->ptr;
//3.1 查找索引是否存在,
p = ziplistIndex(zl,index);
if (p == NULL) {
//3.2 元素不存在,返回错误错误信息
addReply(c,shared.outofrangeerr);
} else {
//3.3 更新元素的值
o->ptr = ziplistDelete(o->ptr,&p);
value = getDecodedObject(value);
o->ptr = ziplistInsert(o->ptr,p,value->ptr,sdslen(value->ptr));
decrRefCount(value);
addReply(c,shared.ok);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
}
} else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
//4. 如果内部编码是linklist
listNode *ln = listIndex(o->ptr,index);
if (ln == NULL) {
//4.1 元素不存在,返回错误信息
addReply(c,shared.outofrangeerr);
} else {
//4.2 元素存在,更新值
decrRefCount((robj*)listNodeValue(ln));
listNodeValue(ln) = value;
incrRefCount(value);
addReply(c,shared.ok);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
}
} else {
//5. 不是ziplist,quicklist返回错误
redisPanic("Unknown list encoding");
}
}
lpop
- 解释: 返回数据类型为List键的左端第一个元素
并且在list中移除这个元素,如果列表中
没有元素则直接返回nil - 用法: lpop key
- 示例:
127.0.0.1:6379> lpush key1 1
(integer)1
127.0.0.1:6379> lpop key1
"1"
127.0.0.1:6379> lpop key1
(nil)
- 源码:
- 查找键是否存在以及是否是list类型,否则直接返回
- 查找到左端第一个元素,并且删除元素 where=左端
- 没有元素了,返回nil
- 返回元素的值,如果此时列表的长度为0,删除键
/**
** redis.c
**
**/
struct redisCommand readonlyCommandTable[] = {
{"lpop",lpopCommand,2,0,NULL,1,1,1}
}
/**
** t_list.c
**
**/
void lpopCommand(redisClient *c) {
popGenericCommand(c,REDIS_HEAD);
}
void popGenericCommand(redisClient *c, int where) {
//1. 查找键是否存在以及是否是list类型,否则直接返回
robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk);
if (o == NULL || checkType(c,o,REDIS_LIST)) return;
//2. 查找到左端第一个元素,并且删除元素 where=左端
robj *value = listTypePop(o,where);
if (value == NULL) {
//3. 没有元素了,返回nil
addReply(c,shared.nullbulk);
} else {
//4. 返回元素的值,如果此时列表的长度为0,删除键
addReplyBulk(c,value);
decrRefCount(value);
if (listTypeLength(o) == 0) dbDelete(c->db,c->argv[1]);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
}
}
rpop
- 解释:返回数据类型为list键的右端第一个元素
并且在List中移除这个元素,如果列表中
没有元素则直接返回null - 用法:rpop key
- 示例:
127.0.0.1:6379> lpush key1 1
(integer)1
127.0.0.1:6379> rpop key1
"1"
127.0.0.1:6379> rpop key1
(nil)
- 源码:
参考lpop源码
ltrim
- 解释: 修剪数据类型为List键,只保留指定范围的元素(包含start,end)
如果start大于实际的end,则键的值被置为空,
如果end大于实际的end,则保留到实际的end - 用法 ltrim key start end
- 示例
127.0.0.1:6379> lpush key1 1
(integer)1
127.0.0.1:6379> lpush key1 2
(integer)2
127.0.0.1:6379> lpush key1 3
(integer)3
127.0.0.1:6379> ltrim key1 0 1
ok
127.0.0.1:6379> lrange key1 0 -1
1)3
2)2
lrem
- 解释: 在数据类型为list的列表中移除count数量的元素
count > 0 从左端到右端移除与value相等的元素,数量为count
count < 0 从右端到左端移除与value相等的元素,数量为count
count = 0 移除列表中所有与value相等的值 - 用法: lrem key count value
- 示例:
127.0.0.1:6379> lpush key1 1
(integer) 1
127.0.0.1:6379> lpush key1 2
(integer) 2
127.0.0.1:6379> lpush key1 1
(integer) 3
127.0.0.1:6379> lrem key1 1 1
(integer) 1
127.0.0.1:6379> lrange key1 0 -1
1) "2"
2) "1"
blpop
- 解释:返回数据类型为List键的左端第一个元素
并且在list中移除这个元素,如果列表中
没有元素则阻塞等待列表中有元素,如果超时时间没到继续等待 - 用法:blpop key
- 示例:
使用和lpop一样,只是在列表没有元素时会等待
- 源码:
brpop
- 解释:返回数据类型为list键的右端第一个元素
并且在List中移除这个元素,如果列表中
没有元素则阻塞等待列表中有元素,如果超时时间没到继续等待 - 用法:brpop key
- 示例:
使用和rpop一样,只是在列表没有元素时会等待
- 源码:
内部编码
ziplist
linklist
场景
简单的消息队列: 相比专门的消息队列,reids实现消息队列不支持ack模式,如果应用消费消息的时候,出现了异常,则这条消息会丢失。
定时排行榜
最新列表
朋友圈点赞通知