六: redis的list数据类型

六: 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);
    }
}

  1. c 客户端对象 在refval之前或之后(where 0:左端 1:右端)插入val
  2. 查询键是否存在以及是否数据类型是集合,否则返回
  3. 如果指定元素refval不为Null则迭代查询元素进行插入
  4. 创建迭代器
  5. 如果指定元素refval存在,则根据where的方向进行插入元素,设置inserted插入
  6. 释放迭代器
  7. 如果插入元素成功,ziplist的元素大于512,则ziplist转为linkedlist
    8.没有插入元素直接返回-1
  8. 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)

  • 源码
  1. 查找键是否存在以及是否是list类型,否则直接返回
  2. 输入的第二个参数字符串转换为整型数
  3. 如果内部编码为ziplist,根据索引在ziplist查找值
  4. 内部编码为LinkList,根据索引查找节点
  5. 内部编码不是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


  • 源码:
  1. 查询键是否存在以及是否数据类型是集合,否则返回
  2. 传入的第二个参数index转为int,第三个参数value转为robj对象
  3. 如果内部编码是ziplist
    3.1 查找索引是否存在
    3.2 元素不存在,返回错误错误信息
    3.3 更新元素的值
  4. 如果内部编码是linklist
    4.1 元素不存在,返回错误信息
    4.2 元素存在,更新值
  5. 不是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)

  • 源码:
  1. 查找键是否存在以及是否是list类型,否则直接返回
  2. 查找到左端第一个元素,并且删除元素 where=左端
  3. 没有元素了,返回nil
  4. 返回元素的值,如果此时集合的长度为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模式,如果应用消费消息的时候,出现了异常,则这条消息会丢失。

定时排行榜

最新列表

朋友圈点赞通知

发布了121 篇原创文章 · 获赞 56 · 访问量 167万+

猜你喜欢

转载自blog.csdn.net/u013565163/article/details/105204670
今日推荐