Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
hanoi2005
redis
提交
96ffb2fe
R
redis
项目概览
hanoi2005
/
redis
与 Fork 源项目一致
从无法访问的项目Fork
通知
1
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
R
redis
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
96ffb2fe
编写于
7月 02, 2010
作者:
P
Pieter Noordhuis
浏览文件
操作
浏览文件
下载
差异文件
merged intset code into the split files
上级
3688d7f3
400aea2b
变更
10
展开全部
隐藏空白更改
内联
并排
Showing
10 changed file
with
1064 addition
and
277 deletion
+1064
-277
src/Makefile
src/Makefile
+2
-1
src/config.c
src/config.c
+2
-0
src/intset.c
src/intset.c
+394
-0
src/intset.h
src/intset.h
+19
-0
src/object.c
src/object.c
+22
-3
src/rdb.c
src/rdb.c
+52
-16
src/redis.c
src/redis.c
+1
-0
src/redis.h
src/redis.h
+25
-0
src/t_set.c
src/t_set.c
+279
-125
tests/unit/type/set.tcl
tests/unit/type/set.tcl
+268
-132
未找到文件。
src/Makefile
浏览文件 @
96ffb2fe
...
...
@@ -15,7 +15,7 @@ endif
CCOPT
=
$(CFLAGS)
$(CCLINK)
$(ARCH)
$(PROF)
DEBUG
?=
-g
-rdynamic
-ggdb
OBJ
=
adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o vm.o pubsub.o multi.o debug.o sort.o
OBJ
=
adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o vm.o pubsub.o multi.o debug.o sort.o
intset.o
BENCHOBJ
=
ae.o anet.o redis-benchmark.o sds.o adlist.o zmalloc.o
CLIOBJ
=
anet.o sds.o adlist.o redis-cli.o zmalloc.o linenoise.o
CHECKDUMPOBJ
=
redis-check-dump.o lzf_c.o lzf_d.o
...
...
@@ -54,6 +54,7 @@ sds.o: sds.c sds.h zmalloc.h
sha1.o
:
sha1.c sha1.h
ziplist.o
:
ziplist.c zmalloc.h ziplist.h
zipmap.o
:
zipmap.c zmalloc.h
intset.o
:
intset.c zmalloc.h
zmalloc.o
:
zmalloc.c config.h
redis-server
:
$(OBJ)
...
...
src/config.c
浏览文件 @
96ffb2fe
...
...
@@ -199,6 +199,8 @@ void loadServerConfig(char *filename) {
server
.
list_max_ziplist_entries
=
memtoll
(
argv
[
1
],
NULL
);
}
else
if
(
!
strcasecmp
(
argv
[
0
],
"list-max-ziplist-value"
)
&&
argc
==
2
){
server
.
list_max_ziplist_value
=
memtoll
(
argv
[
1
],
NULL
);
}
else
if
(
!
strcasecmp
(
argv
[
0
],
"set-max-intset-entries"
)
&&
argc
==
2
){
server
.
set_max_intset_entries
=
memtoll
(
argv
[
1
],
NULL
);
}
else
{
err
=
"Bad directive or wrong number of arguments"
;
goto
loaderr
;
}
...
...
src/intset.c
0 → 100644
浏览文件 @
96ffb2fe
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "intset.h"
#include "zmalloc.h"
/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
/* Accessors for each type of encoding */
#define INTSET_VALUE_ENCODING(__val) (((__val) < INT32_MIN || (__val) > INT32_MAX) ? \
INTSET_ENC_INT64 : (((__val) < INT16_MIN || (__val) > INT16_MAX) ? \
INTSET_ENC_INT32 : INTSET_ENC_INT16))
#define INTSET_GET_ENCODED(__is,__pos,__enc) ((__enc == INTSET_ENC_INT64) ? \
((int64_t*)(__is)->contents)[__pos] : ((__enc == INTSET_ENC_INT32) ? \
((int32_t*)(__is)->contents)[__pos] : ((int16_t*)(__is)->contents)[__pos]))
#define INTSET_GET(__is,__pos) (INTSET_GET_ENCODED(__is,__pos,(__is)->encoding))
#define INTSET_SET(__is,__pos,__val) { \
if ((__is)->encoding == INTSET_ENC_INT64) \
((int64_t*)(__is)->contents)[__pos] = (__val); \
else if ((__is)->encoding == INTSET_ENC_INT32) \
((int32_t*)(__is)->contents)[__pos] = (__val); \
else \
((int16_t*)(__is)->contents)[__pos] = (__val); }
/* Create an empty intset. */
intset
*
intsetNew
(
void
)
{
intset
*
is
=
zmalloc
(
sizeof
(
intset
));
is
->
encoding
=
INTSET_ENC_INT16
;
is
->
length
=
0
;
return
is
;
}
/* Resize the intset */
static
intset
*
intsetResize
(
intset
*
is
,
uint32_t
len
)
{
uint32_t
size
=
len
*
is
->
encoding
;
is
=
zrealloc
(
is
,
sizeof
(
intset
)
+
size
);
return
is
;
}
static
intset
*
intsetUpgrade
(
intset
*
is
,
uint8_t
newenc
,
uint8_t
extra
,
uint8_t
offset
)
{
uint8_t
curenc
=
is
->
encoding
;
int
length
=
is
->
length
;
/* First set new encoding and resize */
is
->
encoding
=
newenc
;
is
=
intsetResize
(
is
,
is
->
length
+
extra
);
/* Upgrade back-to-front so we don't overwrite values */
while
(
length
--
)
INTSET_SET
(
is
,
length
+
offset
,
INTSET_GET_ENCODED
(
is
,
length
,
curenc
));
return
is
;
}
/* Search for the position of "value". Return 1 when the value was found and
* sets "pos" to the position of the value within the intset. Return 0 when
* the value is not present in the intset and sets "pos" to the position
* where "value" can be inserted. */
static
uint8_t
intsetSearch
(
intset
*
is
,
int64_t
value
,
uint32_t
*
pos
)
{
int
min
=
0
,
max
=
is
->
length
-
1
,
mid
=
-
1
;
int64_t
cur
=
-
1
;
/* The value can never be found when the set is empty */
if
(
is
->
length
==
0
)
{
if
(
pos
)
*
pos
=
0
;
return
0
;
}
else
{
/* Check for the case where we know we cannot find the value,
* but do know the insert position. */
if
(
value
>
INTSET_GET
(
is
,
is
->
length
-
1
))
{
if
(
pos
)
*
pos
=
is
->
length
;
return
0
;
}
else
if
(
value
<
INTSET_GET
(
is
,
0
))
{
if
(
pos
)
*
pos
=
0
;
return
0
;
}
}
while
(
max
>=
min
)
{
mid
=
(
min
+
max
)
/
2
;
cur
=
INTSET_GET
(
is
,
mid
);
if
(
value
>
cur
)
{
min
=
mid
+
1
;
}
else
if
(
value
<
cur
)
{
max
=
mid
-
1
;
}
else
{
break
;
}
}
if
(
value
==
cur
)
{
if
(
pos
)
*
pos
=
mid
;
return
1
;
}
else
{
if
(
pos
)
*
pos
=
min
;
return
0
;
}
}
static
void
intsetMoveTail
(
intset
*
is
,
uint32_t
from
,
uint32_t
to
)
{
void
*
src
,
*
dst
;
uint32_t
bytes
=
is
->
length
-
from
;
if
(
is
->
encoding
==
INTSET_ENC_INT64
)
{
src
=
(
int64_t
*
)
is
->
contents
+
from
;
dst
=
(
int64_t
*
)
is
->
contents
+
to
;
bytes
*=
sizeof
(
int64_t
);
}
else
if
(
is
->
encoding
==
INTSET_ENC_INT32
)
{
src
=
(
int32_t
*
)
is
->
contents
+
from
;
dst
=
(
int32_t
*
)
is
->
contents
+
to
;
bytes
*=
sizeof
(
int32_t
);
}
else
{
src
=
(
int16_t
*
)
is
->
contents
+
from
;
dst
=
(
int16_t
*
)
is
->
contents
+
to
;
bytes
*=
sizeof
(
int16_t
);
}
memmove
(
dst
,
src
,
bytes
);
}
/* Insert an integer in the intset */
intset
*
intsetAdd
(
intset
*
is
,
int64_t
value
,
uint8_t
*
success
)
{
uint8_t
valenc
=
INTSET_VALUE_ENCODING
(
value
);
uint32_t
pos
,
offset
;
if
(
success
)
*
success
=
1
;
/* Upgrade encoding if necessary. If we need to upgrade, we know that
* this value should be either appended (if > 0) or prepended (if < 0),
* because it lies outside the range of existing values. */
if
(
valenc
>
is
->
encoding
)
{
offset
=
value
<
0
?
1
:
0
;
is
=
intsetUpgrade
(
is
,
valenc
,
1
,
offset
);
pos
=
(
value
<
0
)
?
0
:
is
->
length
;
}
else
{
/* Abort if the value is already present in the set.
* This call will populate "pos" with the right position to insert
* the value when it cannot be found. */
if
(
intsetSearch
(
is
,
value
,
&
pos
))
{
if
(
success
)
*
success
=
0
;
return
is
;
}
is
=
intsetResize
(
is
,
is
->
length
+
1
);
if
(
pos
<
is
->
length
)
intsetMoveTail
(
is
,
pos
,
pos
+
1
);
}
INTSET_SET
(
is
,
pos
,
value
);
is
->
length
++
;
return
is
;
}
/* Delete integer from intset */
intset
*
intsetRemove
(
intset
*
is
,
int64_t
value
,
uint8_t
*
success
)
{
uint8_t
valenc
=
INTSET_VALUE_ENCODING
(
value
);
uint32_t
pos
;
if
(
success
)
*
success
=
0
;
if
(
valenc
<=
is
->
encoding
&&
intsetSearch
(
is
,
value
,
&
pos
))
{
/* We know we can delete */
if
(
success
)
*
success
=
1
;
/* Overwrite value with tail and update length */
if
(
pos
<
(
is
->
length
-
1
))
intsetMoveTail
(
is
,
pos
+
1
,
pos
);
is
=
intsetResize
(
is
,
is
->
length
-
1
);
is
->
length
--
;
}
return
is
;
}
/* Determine whether a value belongs to this set */
uint8_t
intsetFind
(
intset
*
is
,
int64_t
value
)
{
uint8_t
valenc
=
INTSET_VALUE_ENCODING
(
value
);
return
valenc
<=
is
->
encoding
&&
intsetSearch
(
is
,
value
,
NULL
);
}
/* Return random member */
int64_t
intsetRandom
(
intset
*
is
)
{
return
INTSET_GET
(
is
,
rand
()
%
is
->
length
);
}
/* Sets the value to the value at the given position. When this position is
* out of range the function returns 0, when in range it returns 1. */
uint8_t
intsetGet
(
intset
*
is
,
uint32_t
pos
,
int64_t
*
value
)
{
if
(
pos
<
is
->
length
)
{
*
value
=
INTSET_GET
(
is
,
pos
);
return
1
;
}
return
0
;
}
/* Return intset length */
uint32_t
intsetLen
(
intset
*
is
)
{
return
is
->
length
;
}
#ifdef INTSET_TEST_MAIN
#include <sys/time.h>
void
intsetRepr
(
intset
*
is
)
{
int
i
;
for
(
i
=
0
;
i
<
is
->
length
;
i
++
)
{
printf
(
"%lld
\n
"
,
(
uint64_t
)
INTSET_GET
(
is
,
i
));
}
printf
(
"
\n
"
);
}
void
error
(
char
*
err
)
{
printf
(
"%s
\n
"
,
err
);
exit
(
1
);
}
void
ok
(
void
)
{
printf
(
"OK
\n
"
);
}
long
long
usec
(
void
)
{
struct
timeval
tv
;
gettimeofday
(
&
tv
,
NULL
);
return
(((
long
long
)
tv
.
tv_sec
)
*
1000000
)
+
tv
.
tv_usec
;
}
#define assert(_e) ((_e)?(void)0:(_assert(#_e,__FILE__,__LINE__),exit(1)))
void
_assert
(
char
*
estr
,
char
*
file
,
int
line
)
{
printf
(
"
\n\n
=== ASSERTION FAILED ===
\n
"
);
printf
(
"==> %s:%d '%s' is not true
\n
"
,
file
,
line
,
estr
);
}
intset
*
createSet
(
int
bits
,
int
size
)
{
uint64_t
mask
=
(
1
<<
bits
)
-
1
;
uint64_t
i
,
value
;
intset
*
is
=
intsetNew
();
for
(
i
=
0
;
i
<
size
;
i
++
)
{
if
(
bits
>
32
)
{
value
=
(
rand
()
*
rand
())
&
mask
;
}
else
{
value
=
rand
()
&
mask
;
}
is
=
intsetAdd
(
is
,
value
,
NULL
);
}
return
is
;
}
void
checkConsistency
(
intset
*
is
)
{
int
i
;
for
(
i
=
0
;
i
<
(
is
->
length
-
1
);
i
++
)
{
if
(
is
->
encoding
==
INTSET_ENC_INT16
)
{
int16_t
*
i16
=
(
int16_t
*
)
is
->
contents
;
assert
(
i16
[
i
]
<
i16
[
i
+
1
]);
}
else
if
(
is
->
encoding
==
INTSET_ENC_INT32
)
{
int32_t
*
i32
=
(
int32_t
*
)
is
->
contents
;
assert
(
i32
[
i
]
<
i32
[
i
+
1
]);
}
else
{
int64_t
*
i64
=
(
int64_t
*
)
is
->
contents
;
assert
(
i64
[
i
]
<
i64
[
i
+
1
]);
}
}
}
int
main
(
int
argc
,
char
**
argv
)
{
uint8_t
success
;
int
i
;
intset
*
is
;
sranddev
();
printf
(
"Value encodings: "
);
{
assert
(
INTSET_VALUE_ENCODING
(
-
32768
)
==
INTSET_ENC_INT16
);
assert
(
INTSET_VALUE_ENCODING
(
+
32767
)
==
INTSET_ENC_INT16
);
assert
(
INTSET_VALUE_ENCODING
(
-
32769
)
==
INTSET_ENC_INT32
);
assert
(
INTSET_VALUE_ENCODING
(
+
32768
)
==
INTSET_ENC_INT32
);
assert
(
INTSET_VALUE_ENCODING
(
-
2147483648
)
==
INTSET_ENC_INT32
);
assert
(
INTSET_VALUE_ENCODING
(
+
2147483647
)
==
INTSET_ENC_INT32
);
assert
(
INTSET_VALUE_ENCODING
(
-
2147483649
)
==
INTSET_ENC_INT64
);
assert
(
INTSET_VALUE_ENCODING
(
+
2147483648
)
==
INTSET_ENC_INT64
);
assert
(
INTSET_VALUE_ENCODING
(
-
9223372036854775808ull
)
==
INTSET_ENC_INT64
);
assert
(
INTSET_VALUE_ENCODING
(
+
9223372036854775807ull
)
==
INTSET_ENC_INT64
);
ok
();
}
printf
(
"Basic adding: "
);
{
is
=
intsetNew
();
is
=
intsetAdd
(
is
,
5
,
&
success
);
assert
(
success
);
is
=
intsetAdd
(
is
,
6
,
&
success
);
assert
(
success
);
is
=
intsetAdd
(
is
,
4
,
&
success
);
assert
(
success
);
is
=
intsetAdd
(
is
,
4
,
&
success
);
assert
(
!
success
);
ok
();
}
printf
(
"Large number of random adds: "
);
{
int
inserts
=
0
;
is
=
intsetNew
();
for
(
i
=
0
;
i
<
1024
;
i
++
)
{
is
=
intsetAdd
(
is
,
rand
()
%
0x800
,
&
success
);
if
(
success
)
inserts
++
;
}
assert
(
is
->
length
==
inserts
);
checkConsistency
(
is
);
ok
();
}
printf
(
"Upgrade from int16 to int32: "
);
{
is
=
intsetNew
();
is
=
intsetAdd
(
is
,
32
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT16
);
is
=
intsetAdd
(
is
,
65535
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT32
);
assert
(
intsetFind
(
is
,
32
));
assert
(
intsetFind
(
is
,
65535
));
checkConsistency
(
is
);
is
=
intsetNew
();
is
=
intsetAdd
(
is
,
32
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT16
);
is
=
intsetAdd
(
is
,
-
65535
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT32
);
assert
(
intsetFind
(
is
,
32
));
assert
(
intsetFind
(
is
,
-
65535
));
checkConsistency
(
is
);
ok
();
}
printf
(
"Upgrade from int16 to int64: "
);
{
is
=
intsetNew
();
is
=
intsetAdd
(
is
,
32
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT16
);
is
=
intsetAdd
(
is
,
4294967295
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT64
);
assert
(
intsetFind
(
is
,
32
));
assert
(
intsetFind
(
is
,
4294967295
));
checkConsistency
(
is
);
is
=
intsetNew
();
is
=
intsetAdd
(
is
,
32
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT16
);
is
=
intsetAdd
(
is
,
-
4294967295
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT64
);
assert
(
intsetFind
(
is
,
32
));
assert
(
intsetFind
(
is
,
-
4294967295
));
checkConsistency
(
is
);
ok
();
}
printf
(
"Upgrade from int32 to int64: "
);
{
is
=
intsetNew
();
is
=
intsetAdd
(
is
,
65535
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT32
);
is
=
intsetAdd
(
is
,
4294967295
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT64
);
assert
(
intsetFind
(
is
,
65535
));
assert
(
intsetFind
(
is
,
4294967295
));
checkConsistency
(
is
);
is
=
intsetNew
();
is
=
intsetAdd
(
is
,
65535
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT32
);
is
=
intsetAdd
(
is
,
-
4294967295
,
NULL
);
assert
(
is
->
encoding
==
INTSET_ENC_INT64
);
assert
(
intsetFind
(
is
,
65535
));
assert
(
intsetFind
(
is
,
-
4294967295
));
checkConsistency
(
is
);
ok
();
}
printf
(
"Stress lookups: "
);
{
long
num
=
100000
,
size
=
10000
;
int
i
,
bits
=
20
;
long
long
start
;
is
=
createSet
(
bits
,
size
);
checkConsistency
(
is
);
start
=
usec
();
for
(
i
=
0
;
i
<
num
;
i
++
)
intsetSearch
(
is
,
rand
()
%
((
1
<<
bits
)
-
1
),
NULL
);
printf
(
"%ld lookups, %ld element set, %lldusec
\n
"
,
num
,
size
,
usec
()
-
start
);
}
printf
(
"Stress add+delete: "
);
{
int
i
,
v1
,
v2
;
is
=
intsetNew
();
for
(
i
=
0
;
i
<
0xffff
;
i
++
)
{
v1
=
rand
()
%
0xfff
;
is
=
intsetAdd
(
is
,
v1
,
NULL
);
assert
(
intsetFind
(
is
,
v1
));
v2
=
rand
()
%
0xfff
;
is
=
intsetRemove
(
is
,
v2
,
NULL
);
assert
(
!
intsetFind
(
is
,
v2
));
}
checkConsistency
(
is
);
ok
();
}
}
#endif
src/intset.h
0 → 100644
浏览文件 @
96ffb2fe
#ifndef __INTSET_H
#define __INTSET_H
#include <stdint.h>
typedef
struct
intset
{
uint32_t
encoding
;
uint32_t
length
;
int8_t
contents
[];
}
intset
;
intset
*
intsetNew
(
void
);
intset
*
intsetAdd
(
intset
*
is
,
int64_t
value
,
uint8_t
*
success
);
intset
*
intsetRemove
(
intset
*
is
,
int64_t
value
,
uint8_t
*
success
);
uint8_t
intsetFind
(
intset
*
is
,
int64_t
value
);
int64_t
intsetRandom
(
intset
*
is
);
uint8_t
intsetGet
(
intset
*
is
,
uint32_t
pos
,
int64_t
*
value
);
uint32_t
intsetLen
(
intset
*
is
);
#endif // __INTSET_H
src/object.c
浏览文件 @
96ffb2fe
...
...
@@ -73,7 +73,16 @@ robj *createZiplistObject(void) {
robj
*
createSetObject
(
void
)
{
dict
*
d
=
dictCreate
(
&
setDictType
,
NULL
);
return
createObject
(
REDIS_SET
,
d
);
robj
*
o
=
createObject
(
REDIS_SET
,
d
);
o
->
encoding
=
REDIS_ENCODING_HT
;
return
o
;
}
robj
*
createIntsetObject
(
void
)
{
intset
*
is
=
intsetNew
();
robj
*
o
=
createObject
(
REDIS_SET
,
is
);
o
->
encoding
=
REDIS_ENCODING_INTSET
;
return
o
;
}
robj
*
createHashObject
(
void
)
{
...
...
@@ -114,7 +123,16 @@ void freeListObject(robj *o) {
}
void
freeSetObject
(
robj
*
o
)
{
dictRelease
((
dict
*
)
o
->
ptr
);
switch
(
o
->
encoding
)
{
case
REDIS_ENCODING_HT
:
dictRelease
((
dict
*
)
o
->
ptr
);
break
;
case
REDIS_ENCODING_INTSET
:
zfree
(
o
->
ptr
);
break
;
default:
redisPanic
(
"Unknown set encoding type"
);
}
}
void
freeZsetObject
(
robj
*
o
)
{
...
...
@@ -356,7 +374,7 @@ int getLongLongFromObject(robj *o, long long *target) {
}
}
*
target
=
value
;
if
(
target
)
*
target
=
value
;
return
REDIS_OK
;
}
...
...
@@ -400,6 +418,7 @@ char *strEncoding(int encoding) {
case
REDIS_ENCODING_ZIPMAP
:
return
"zipmap"
;
case
REDIS_ENCODING_LINKEDLIST
:
return
"linkedlist"
;
case
REDIS_ENCODING_ZIPLIST
:
return
"ziplist"
;
case
REDIS_ENCODING_INTSET
:
return
"intset"
;
default:
return
"unknown"
;
}
}
src/rdb.c
浏览文件 @
96ffb2fe
...
...
@@ -260,17 +260,29 @@ int rdbSaveObject(FILE *fp, robj *o) {
}
}
else
if
(
o
->
type
==
REDIS_SET
)
{
/* Save a set value */
dict
*
set
=
o
->
ptr
;
dictIterator
*
di
=
dictGetIterator
(
set
);
dictEntry
*
de
;
if
(
rdbSaveLen
(
fp
,
dictSize
(
set
))
==
-
1
)
return
-
1
;
while
((
de
=
dictNext
(
di
))
!=
NULL
)
{
robj
*
eleobj
=
dictGetEntryKey
(
de
);
if
(
o
->
encoding
==
REDIS_ENCODING_HT
)
{
dict
*
set
=
o
->
ptr
;
dictIterator
*
di
=
dictGetIterator
(
set
);
dictEntry
*
de
;
if
(
rdbSaveStringObject
(
fp
,
eleobj
)
==
-
1
)
return
-
1
;
if
(
rdbSaveLen
(
fp
,
dictSize
(
set
))
==
-
1
)
return
-
1
;
while
((
de
=
dictNext
(
di
))
!=
NULL
)
{
robj
*
eleobj
=
dictGetEntryKey
(
de
);
if
(
rdbSaveStringObject
(
fp
,
eleobj
)
==
-
1
)
return
-
1
;
}
dictReleaseIterator
(
di
);
}
else
if
(
o
->
encoding
==
REDIS_ENCODING_INTSET
)
{
intset
*
is
=
o
->
ptr
;
long
long
llval
;
int
i
=
0
;
if
(
rdbSaveLen
(
fp
,
intsetLen
(
is
))
==
-
1
)
return
-
1
;
while
(
intsetGet
(
is
,
i
++
,
&
llval
))
{
if
(
rdbSaveLongLongAsStringObject
(
fp
,
llval
)
==
-
1
)
return
-
1
;
}
}
else
{
redisPanic
(
"Unknown set encoding"
);
}
dictReleaseIterator
(
di
);
}
else
if
(
o
->
type
==
REDIS_ZSET
)
{
/* Save a set value */
zset
*
zs
=
o
->
ptr
;
...
...
@@ -628,6 +640,7 @@ int rdbLoadDoubleValue(FILE *fp, double *val) {
robj
*
rdbLoadObject
(
int
type
,
FILE
*
fp
)
{
robj
*
o
,
*
ele
,
*
dec
;
size_t
len
;
unsigned
int
i
;
redisLog
(
REDIS_DEBUG
,
"LOADING OBJECT %d (at %d)
\n
"
,
type
,
ftell
(
fp
));
if
(
type
==
REDIS_STRING
)
{
...
...
@@ -669,16 +682,39 @@ robj *rdbLoadObject(int type, FILE *fp) {
}
else
if
(
type
==
REDIS_SET
)
{
/* Read list/set value */
if
((
len
=
rdbLoadLen
(
fp
,
NULL
))
==
REDIS_RDB_LENERR
)
return
NULL
;
o
=
createSetObject
();
/* It's faster to expand the dict to the right size asap in order
* to avoid rehashing */
if
(
len
>
DICT_HT_INITIAL_SIZE
)
dictExpand
(
o
->
ptr
,
len
);
/* Use a regular set when there are too many entries. */
if
(
len
>
server
.
set_max_intset_entries
)
{
o
=
createSetObject
();
/* It's faster to expand the dict to the right size asap in order
* to avoid rehashing */
if
(
len
>
DICT_HT_INITIAL_SIZE
)
dictExpand
(
o
->
ptr
,
len
);
}
else
{
o
=
createIntsetObject
();
}
/* Load every single element of the list/set */
while
(
len
--
)
{
for
(
i
=
0
;
i
<
len
;
i
++
)
{
long
long
llval
;
if
((
ele
=
rdbLoadEncodedStringObject
(
fp
))
==
NULL
)
return
NULL
;
ele
=
tryObjectEncoding
(
ele
);
dictAdd
((
dict
*
)
o
->
ptr
,
ele
,
NULL
);
if
(
o
->
encoding
==
REDIS_ENCODING_INTSET
)
{
/* Fetch integer value from element */
if
(
getLongLongFromObject
(
ele
,
&
llval
)
==
REDIS_OK
)
{
o
->
ptr
=
intsetAdd
(
o
->
ptr
,
llval
,
NULL
);
}
else
{
setTypeConvert
(
o
,
REDIS_ENCODING_HT
);
dictExpand
(
o
->
ptr
,
len
);
}
}
/* This will also be called when the set was just converted
* to regular hashtable encoded set */
if
(
o
->
encoding
==
REDIS_ENCODING_HT
)
{
dictAdd
((
dict
*
)
o
->
ptr
,
ele
,
NULL
);
}
}
}
else
if
(
type
==
REDIS_ZSET
)
{
/* Read list/set value */
...
...
src/redis.c
浏览文件 @
96ffb2fe
...
...
@@ -731,6 +731,7 @@ void initServerConfig() {
server
.
hash_max_zipmap_value
=
REDIS_HASH_MAX_ZIPMAP_VALUE
;
server
.
list_max_ziplist_entries
=
REDIS_LIST_MAX_ZIPLIST_ENTRIES
;
server
.
list_max_ziplist_value
=
REDIS_LIST_MAX_ZIPLIST_VALUE
;
server
.
set_max_intset_entries
=
REDIS_SET_MAX_INTSET_ENTRIES
;
server
.
shutdown_asap
=
0
;
resetServerSaveParams
();
...
...
src/redis.h
浏览文件 @
96ffb2fe
...
...
@@ -25,6 +25,7 @@
#include "anet.h"
/* Networking the easy way */
#include "zipmap.h"
/* Compact string -> string data structure */
#include "ziplist.h"
/* Compact list data structure */
#include "intset.h"
/* Compact integer set structure */
#include "version.h"
/* Error codes */
...
...
@@ -81,6 +82,7 @@
#define REDIS_ENCODING_ZIPMAP 3
/* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4
/* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5
/* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6
/* Encoded as intset */
/* Object types only used for dumping to disk */
#define REDIS_EXPIRETIME 253
...
...
@@ -187,6 +189,7 @@
#define REDIS_HASH_MAX_ZIPMAP_VALUE 512
#define REDIS_LIST_MAX_ZIPLIST_ENTRIES 1024
#define REDIS_LIST_MAX_ZIPLIST_VALUE 32
#define REDIS_SET_MAX_INTSET_ENTRIES 4096
/* Sets operations codes */
#define REDIS_OP_UNION 0
...
...
@@ -396,6 +399,7 @@ struct redisServer {
size_t
hash_max_zipmap_value
;
size_t
list_max_ziplist_entries
;
size_t
list_max_ziplist_value
;
size_t
set_max_intset_entries
;
/* Virtual memory state */
FILE
*
vm_fp
;
int
vm_fd
;
...
...
@@ -533,6 +537,14 @@ typedef struct {
listNode
*
ln
;
/* Entry in linked list */
}
listTypeEntry
;
/* Structure to hold set iteration abstraction. */
typedef
struct
{
robj
*
subject
;
int
encoding
;
int
ii
;
/* intset iterator */
dictIterator
*
di
;
}
setIterator
;
/* Structure to hold hash iteration abstration. Note that iteration over
* hashes involves both fields and values. Because it is possible that
* not both are required, store pointers in the iterator to avoid
...
...
@@ -631,6 +643,7 @@ robj *createStringObjectFromLongLong(long long value);
robj
*
createListObject
(
void
);
robj
*
createZiplistObject
(
void
);
robj
*
createSetObject
(
void
);
robj
*
createIntsetObject
(
void
);
robj
*
createHashObject
(
void
);
robj
*
createZsetObject
(
void
);
int
getLongFromObjectOrReply
(
redisClient
*
c
,
robj
*
o
,
long
*
target
,
const
char
*
msg
);
...
...
@@ -714,6 +727,18 @@ int dontWaitForSwappedKey(redisClient *c, robj *key);
void
handleClientsBlockedOnSwappedKey
(
redisDb
*
db
,
robj
*
key
);
vmpointer
*
vmSwapObjectBlocking
(
robj
*
val
);
/* Set data type */
robj
*
setTypeCreate
(
robj
*
value
);
int
setTypeAdd
(
robj
*
subject
,
robj
*
value
);
int
setTypeRemove
(
robj
*
subject
,
robj
*
value
);
int
setTypeIsMember
(
robj
*
subject
,
robj
*
value
);
setIterator
*
setTypeInitIterator
(
robj
*
subject
);
void
setTypeReleaseIterator
(
setIterator
*
si
);
robj
*
setTypeNext
(
setIterator
*
si
);
robj
*
setTypeRandomElement
(
robj
*
subject
);
unsigned
long
setTypeSize
(
robj
*
subject
);
void
setTypeConvert
(
robj
*
subject
,
int
enc
);
/* Hash data type */
void
convertToRealHash
(
robj
*
o
);
void
hashTypeTryConversion
(
robj
*
subject
,
robj
**
argv
,
int
start
,
int
end
);
...
...
src/t_set.c
浏览文件 @
96ffb2fe
此差异已折叠。
点击以展开。
tests/unit/type/set.tcl
浏览文件 @
96ffb2fe
start_server
{
tags
{
"set"
}}
{
test
{
SADD, SCARD, SISMEMBER, SMEMBERS basics
}
{
r sadd myset foo
r sadd myset bar
list
[
r scard myset
]
[
r sismember myset foo
]
\
[
r sismember myset bar
]
[
r sismember myset bla
]
\
[
lsort
[
r smembers myset
]]
}
{
2 1 1 0
{
bar foo
}}
test
{
SADD adding the same element multiple times
}
{
r sadd myset foo
r sadd myset foo
r sadd myset foo
r scard myset
}
{
2
}
start_server
{
tags
{
"set"
}
overrides
{
"set-max-intset-entries"
512
}
}
{
proc create_set
{
key entries
}
{
r del $key
foreach entry $entries
{
r sadd $key $entry
}
}
test
{
SADD, SCARD, SISMEMBER, SMEMBERS basics - regular set
}
{
create_set myset
{
foo
}
assert_encoding hashtable myset
assert_equal 1
[
r sadd myset bar
]
assert_equal 0
[
r sadd myset bar
]
assert_equal 2
[
r scard myset
]
assert_equal 1
[
r sismember myset foo
]
assert_equal 1
[
r sismember myset bar
]
assert_equal 0
[
r sismember myset bla
]
assert_equal
{
bar foo
}
[
lsort
[
r smembers myset
]]
}
test
{
SADD, SCARD, SISMEMBER, SMEMBERS basics - intset
}
{
create_set myset
{
17
}
assert_encoding intset myset
assert_equal 1
[
r sadd myset 16
]
assert_equal 0
[
r sadd myset 16
]
assert_equal 2
[
r scard myset
]
assert_equal 1
[
r sismember myset 16
]
assert_equal 1
[
r sismember myset 17
]
assert_equal 0
[
r sismember myset 18
]
assert_equal
{
16 17
}
[
lsort
[
r smembers myset
]]
}
test
{
SADD against non set
}
{
r lpush mylist foo
catch
{
r sadd mylist bar
}
err
format $err
}
{
ERR*kind*
}
test
{
SREM basics
}
{
r sadd myset ciao
r srem myset foo
lsort
[
r smembers myset
]
}
{
bar ciao
}
test
{
Mass SADD and SINTER with two sets
}
{
for
{
set i 0
}
{
$i
< 1000
}
{
incr i
}
{
assert_error ERR*kind*
{
r sadd mylist bar
}
}
test
"SADD a non-integer against an intset"
{
create_set myset
{
1 2 3
}
assert_encoding intset myset
assert_equal 1
[
r sadd myset a
]
assert_encoding hashtable myset
}
test
"SADD overflows the maximum allowed integers in an intset"
{
r del myset
for
{
set i 0
}
{
$i
< 512
}
{
incr i
}
{
r sadd myset $i
}
assert_encoding intset myset
assert_equal 1
[
r sadd myset 512
]
assert_encoding hashtable myset
}
test
"Set encoding after DEBUG RELOAD"
{
r del myintset myhashset mylargeintset
for
{
set i 0
}
{
$i
< 100
}
{
incr i
}
{
r sadd myintset $i
}
for
{
set i 0
}
{
$i
< 1280
}
{
incr i
}
{
r sadd mylargeintset $i
}
for
{
set i 0
}
{
$i
< 256
}
{
incr i
}
{
r sadd myhashset
[
format
"i%03d"
$i
]
}
assert_encoding intset myintset
assert_encoding hashtable mylargeintset
assert_encoding hashtable myhashset
r debug reload
assert_encoding intset myintset
assert_encoding hashtable mylargeintset
assert_encoding hashtable myhashset
}
test
{
SREM basics - regular set
}
{
create_set myset
{
foo bar ciao
}
assert_encoding hashtable myset
assert_equal 0
[
r srem myset qux
]
assert_equal 1
[
r srem myset foo
]
assert_equal
{
bar ciao
}
[
lsort
[
r smembers myset
]]
}
test
{
SREM basics - intset
}
{
create_set myset
{
3 4 5
}
assert_encoding intset myset
assert_equal 0
[
r srem myset 6
]
assert_equal 1
[
r srem myset 4
]
assert_equal
{
3 5
}
[
lsort
[
r smembers myset
]]
}
foreach
{
type
}
{
hashtable intset
}
{
for
{
set i 1
}
{
$i
<= 5
}
{
incr i
}
{
r del
[
format
"set%d"
$i
]
}
for
{
set i 0
}
{
$i
< 200
}
{
incr i
}
{
r sadd set1 $i
r sadd set2
[
expr $i+995
]
r sadd set2
[
expr $i+195
]
}
foreach i
{
199 195 1000 2000
}
{
r sadd set3 $i
}
for
{
set i 5
}
{
$i
< 200
}
{
incr i
}
{
r sadd set4 $i
}
lsort
[
r sinter set1 set2
]
}
{
995 996 997 998 999
}
r sadd set5 0
test
{
SUNION with two sets
}
{
lsort
[
r sunion set1 set2
]
}
[
lsort -uniq
"
[
r smembers set1
]
[
r smembers set2
]
"
]
# it is possible that a hashtable encoded only contains integers,
# because it is converted from an intset to a hashtable when a
# non-integer element is added and then removed.
if
{
$type
eq
"hashtable"
}
{
for
{
set i 1
}
{
$i
<= 5
}
{
incr i
}
{
r sadd
[
format
"set%d"
$i
]
foo
r srem
[
format
"set%d"
$i
]
foo
}
}
test
{
SINTERSTORE with two sets
}
{
r sinterstore setres set1 set2
lsort
[
r smembers setres
]
}
{
995 996 997 998 999
}
test
"Generated sets must be encoded as
$type
"
{
for
{
set i 1
}
{
$i
<= 5
}
{
incr i
}
{
assert_encoding $type
[
format
"set%d"
$i
]
}
}
test
{
SINTERSTORE with two sets, after a DEBUG RELOAD
}
{
r debug reload
r sinterstore setres set1 set2
lsort
[
r smembers setres
]
}
{
995 996 997 998 999
}
test
"SINTER with two sets -
$type
"
{
assert_equal
{
195 196 197 198 199
}
[
lsort
[
r sinter set1 set2
]]
}
test
"SINTERSTORE with two sets -
$type
"
{
r sinterstore setres set1 set2
assert_encoding intset setres
assert_equal
{
195 196 197 198 199
}
[
lsort
[
r smembers setres
]]
}
test
"SINTERSTORE with two sets, after a DEBUG RELOAD -
$type
"
{
r debug reload
r sinterstore setres set1 set2
assert_encoding intset setres
assert_equal
{
195 196 197 198 199
}
[
lsort
[
r smembers setres
]]
}
test
"SUNION with two sets -
$type
"
{
set expected
[
lsort -uniq
"
[
r smembers set1
]
[
r smembers set2
]
"
]
assert_equal $expected
[
lsort
[
r sunion set1 set2
]]
}
test
"SUNIONSTORE with two sets -
$type
"
{
r sunionstore setres set1 set2
assert_encoding intset setres
set expected
[
lsort -uniq
"
[
r smembers set1
]
[
r smembers set2
]
"
]
assert_equal $expected
[
lsort
[
r smembers setres
]]
}
test
{
SUNIONSTORE with two sets
}
{
r sunionstore setres set1 set2
lsort
[
r smembers setres
]
}
[
lsort -uniq
"
[
r smembers set1
]
[
r smembers set2
]
"
]
test
"SINTER against three sets -
$type
"
{
assert_equal
{
195 199
}
[
lsort
[
r sinter set1 set2 set3
]]
}
test
"SINTERSTORE with three sets -
$type
"
{
r sinterstore setres set1 set2 set3
assert_equal
{
195 199
}
[
r smembers setres
]
}
test
"SUNION with non existing keys -
$type
"
{
set expected
[
lsort -uniq
"
[
r smembers set1
]
[
r smembers set2
]
"
]
assert_equal $expected
[
lsort
[
r sunion nokey1 set1 set2 nokey2
]]
}
test
"SDIFF with two sets -
$type
"
{
assert_equal
{
0 1 2 3 4
}
[
lsort
[
r sdiff set1 set4
]]
}
test
{
SUNIONSTORE against non existing keys
}
{
test
"SDIFF with three sets -
$type
"
{
assert_equal
{
1 2 3 4
}
[
lsort
[
r sdiff set1 set4 set5
]]
}
test
"SDIFFSTORE with three sets -
$type
"
{
r sdiffstore setres set1 set4 set5
assert_encoding intset setres
assert_equal
{
1 2 3 4
}
[
lsort
[
r smembers setres
]]
}
}
test
"SINTER against non-set should throw error"
{
r set key1 x
assert_error
"ERR*wrong kind*"
{
r sinter key1 noset
}
}
test
"SUNION against non-set should throw error"
{
r set key1 x
assert_error
"ERR*wrong kind*"
{
r sunion key1 noset
}
}
test
"SINTERSTORE against non existing keys should delete dstkey"
{
r set setres xxx
list
[
r sunionstore setres foo111 bar222
]
[
r exists xxx
]
}
{
0 0
}
test
{
SINTER against three sets
}
{
r sadd set3 999
r sadd set3 995
r sadd set3 1000
r sadd set3 2000
lsort
[
r sinter set1 set2 set3
]
}
{
995 999
}
test
{
SINTERSTORE with three sets
}
{
r sinterstore setres set1 set2 set3
lsort
[
r smembers setres
]
}
{
995 999
}
test
{
SUNION with non existing keys
}
{
lsort
[
r sunion nokey1 set1 set2 nokey2
]
}
[
lsort -uniq
"
[
r smembers set1
]
[
r smembers set2
]
"
]
test
{
SDIFF with two sets
}
{
for
{
set i 5
}
{
$i
< 1000
}
{
incr i
}
{
r sadd set4 $i
assert_equal 0
[
r sinterstore setres foo111 bar222
]
assert_equal 0
[
r exists setres
]
}
test
"SUNIONSTORE against non existing keys should delete dstkey"
{
r set setres xxx
assert_equal 0
[
r sunionstore setres foo111 bar222
]
assert_equal 0
[
r exists setres
]
}
foreach
{
type contents
}
{
hashtable
{
a b c
}
intset
{
1 2 3
}}
{
test
"SPOP basics -
$type
"
{
create_set myset $contents
assert_encoding $type myset
assert_equal $contents
[
lsort
[
list
[
r spop myset
]
[
r spop myset
]
[
r spop myset
]]]
assert_equal 0
[
r scard myset
]
}
lsort
[
r sdiff set1 set4
]
}
{
0 1 2 3 4
}
test
{
SDIFF with three sets
}
{
r sadd set5 0
lsort
[
r sdiff set1 set4 set5
]
}
{
1 2 3 4
}
test
"SRANDMEMBER -
$type
"
{
create_set myset $contents
unset -nocomplain myset
array set myset
{}
for
{
set i 0
}
{
$i
< 100
}
{
incr i
}
{
set myset
([
r srandmember myset
])
1
}
assert_equal $contents
[
lsort
[
array names myset
]]
}
}
test
{
SDIFFSTORE with three sets
}
{
r sdiffstore sres set1 set4 set5
lsort
[
r smembers sres
]
}
{
1 2 3 4
}
proc setup_move
{}
{
r del myset3 myset4
create_set myset1
{
1 a b
}
create_set myset2
{
2 3 4
}
assert_encoding hashtable myset1
assert_encoding intset myset2
}
test
{
SPOP basics
}
{
r del myset
r sadd myset 1
r sadd myset 2
r sadd myset 3
list
[
lsort
[
list
[
r spop myset
]
[
r spop myset
]
[
r spop myset
]]]
[
r scard myset
]
}
{{
1 2 3
}
0
}
test
{
SRANDMEMBER
}
{
r del myset
r sadd myset a
r sadd myset b
r sadd myset c
unset -nocomplain myset
array set myset
{}
for
{
set i 0
}
{
$i
< 100
}
{
incr i
}
{
set myset
([
r srandmember myset
])
1
}
lsort
[
array names myset
]
}
{
a b c
}
test
{
SMOVE basics
}
{
r sadd myset1 a
r sadd myset1 b
r sadd myset1 c
r sadd myset2 x
r sadd myset2 y
r sadd myset2 z
r smove myset1 myset2 a
list
[
lsort
[
r smembers myset2
]]
[
lsort
[
r smembers myset1
]]
}
{{
a x y z
}
{
b c
}}
test
{
SMOVE non existing key
}
{
list
[
r smove myset1 myset2 foo
]
[
lsort
[
r smembers myset2
]]
[
lsort
[
r smembers myset1
]]
}
{
0
{
a x y z
}
{
b c
}}
test
{
SMOVE non existing src set
}
{
list
[
r smove noset myset2 foo
]
[
lsort
[
r smembers myset2
]]
}
{
0
{
a x y z
}}
test
{
SMOVE non existing dst set
}
{
list
[
r smove myset2 myset3 y
]
[
lsort
[
r smembers myset2
]]
[
lsort
[
r smembers myset3
]]
}
{
1
{
a x z
}
y
}
test
{
SMOVE wrong src key type
}
{
test
"SMOVE basics - from regular set to intset"
{
# move a non-integer element to an intset should convert encoding
setup_move
assert_equal 1
[
r smove myset1 myset2 a
]
assert_equal
{
1 b
}
[
lsort
[
r smembers myset1
]]
assert_equal
{
2 3 4 a
}
[
lsort
[
r smembers myset2
]]
assert_encoding hashtable myset2
# move an integer element should not convert the encoding
setup_move
assert_equal 1
[
r smove myset1 myset2 1
]
assert_equal
{
a b
}
[
lsort
[
r smembers myset1
]]
assert_equal
{
1 2 3 4
}
[
lsort
[
r smembers myset2
]]
assert_encoding intset myset2
}
test
"SMOVE basics - from intset to regular set"
{
setup_move
assert_equal 1
[
r smove myset2 myset1 2
]
assert_equal
{
1 2 a b
}
[
lsort
[
r smembers myset1
]]
assert_equal
{
3 4
}
[
lsort
[
r smembers myset2
]]
}
test
"SMOVE non existing key"
{
setup_move
assert_equal 0
[
r smove myset1 myset2 foo
]
assert_equal
{
1 a b
}
[
lsort
[
r smembers myset1
]]
assert_equal
{
2 3 4
}
[
lsort
[
r smembers myset2
]]
}
test
"SMOVE non existing src set"
{
setup_move
assert_equal 0
[
r smove noset myset2 foo
]
assert_equal
{
2 3 4
}
[
lsort
[
r smembers myset2
]]
}
test
"SMOVE from regular set to non existing destination set"
{
setup_move
assert_equal 1
[
r smove myset1 myset3 a
]
assert_equal
{
1 b
}
[
lsort
[
r smembers myset1
]]
assert_equal
{
a
}
[
lsort
[
r smembers myset3
]]
assert_encoding hashtable myset3
}
test
"SMOVE from intset to non existing destination set"
{
setup_move
assert_equal 1
[
r smove myset2 myset3 2
]
assert_equal
{
3 4
}
[
lsort
[
r smembers myset2
]]
assert_equal
{
2
}
[
lsort
[
r smembers myset3
]]
assert_encoding intset myset3
}
test
"SMOVE wrong src key type"
{
r set x 10
catch
{
r smove x myset2 foo
}
err
format $err
}
{
ERR*
}
assert_error
"ERR*wrong kind*"
{
r smove x myset2 foo
}
}
test
{
SMOVE wrong dst key type
}
{
test
"SMOVE wrong dst key type"
{
r set x 10
catch
{
r smove myset2 x foo
}
err
format $err
}
{
ERR*
}
assert_error
"ERR*wrong kind*"
{
r smove myset2 x foo
}
}
}
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录