四:Redis的字符串类型命令、源码解析、应用场景
命令
set
-
解释 :设置键的值为字符串,并且可以设置过期时间
set key value [ex seconds| px milliseconds] [nx|xx]
- ex seconds: 设置键多少秒后过期
- px milliseconds: 设置键多少毫秒后过期
- nx: 当键不存在,才能设置成功,添加 (可以用来做分布式锁)
- xx: 当键存在,才能设置成功,更新
-
用法 :set key1 value1
-
示例:
//示例:简单设置值
127.0.0.1:6379> get key1
(nil)
127.0.0.1:6379> set key1 value1
ok
127.0.0.1:6379>get key1
"value1"
//示例:设置键的过期时间
127.0.0.1:6379> get key1
(nil)
//设置键10秒后过期
127.0.0.1:6379> set key1 value1 ex 10
ok
//1s后查询
127.0.0.1:6379> get key1
"value1"
//10s后查询
127.0.0.1:6379> get key1
(nil)
//示例:nx 当键不存在才能设置成功
127.0.0.1:6379> set key1 value1 nx
ok
127.0.0.1:6379> set key1 value1 nx
(nil)
//示例:xx 当键存在,才能设置成功
127.0.0.1:6379> set key1 value1 xx
(nil)
127.0.0.1:6379> set key1 value1
ok
127.0.0.1:6379> set key1 value2 xx
ok
- 源码分析
/**
*
* t_string.c
**/
void setCommand(redisClient *c) {
//解析键设置的值
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,0,c->argv[1],c->argv[2],NULL);
}
/**
* t_string.c
**/
void setGenericCommand(redisClient *c, int nx, robj *key, robj *val, robj *expire) {
int retval;
long seconds = 0; /* initialized to avoid an harmness warning */
//如果有设置过期时间
if (expire) {
//如果有设置过期,获取过期时间
if (getLongFromObjectOrReply(c, expire, &seconds, NULL) != REDIS_OK)
return;
//如果过期时间小于0,返回错误
if (seconds <= 0) {
addReplyError(c,"invalid expire time in SETEX");
return;
}
}
//如果键,有过期时间,并且过期了,则删除键(当当前命令执行在master上时)
lookupKeyWrite(c->db,key); /* Force expire of old key if needed */
//如果键已经存在返回REDIS_ERR,否则返回REDIS_OK
retval = dbAdd(c->db,key,val);
if (retval == REDIS_ERR) {
// nx = 0
if (!nx) {
// 则相当于xx ,因为进入到这个if,else表示键已经存在,所以这里更新键值
dbReplace(c->db,key,val);
//引用计数加一
incrRefCount(val);
} else {
//表示当设置键时,使用 nx,键已经存在,设置失败
addReply(c,shared.czero);
return;
}
} else {
//键之前不存在,(使用nx或为没有使用参数)添加键,添加成功,引用计数加一
incrRefCount(val);
}
touchWatchedKey(c->db,key);
server.dirty++;
removeExpire(c->db,key);
if (expire) setExpire(c->db,key,time(NULL)+seconds);
addReply(c, nx ? shared.cone : shared.ok);
}
/**
* util.c
**/
// tryObjectEncoding方法调用 isStringRepresentableAsLongLong
int isStringRepresentableAsLong(sds s, long *longval) {
long long ll;
//设置的值是否是长整形
if (isStringRepresentableAsLongLong(s,&ll) == REDIS_ERR) return REDIS_ERR;
if (ll < LONG_MIN || ll > LONG_MAX) return REDIS_ERR;
*longval = (long)ll;
return REDIS_OK;
}
int isStringRepresentableAsLongLong(sds s, long long *llongval) {
//长整形最大4个字节。
char buf[32], *endptr;
long long value;
int slen;
value = strtoll(s, &endptr, 10);
if (endptr[0] != '\0') return REDIS_ERR;
slen = ll2string(buf,32,value);
/* If the number converted back into a string is not identical
* then it's not possible to encode the string as integer */
if (sdslen(s) != (unsigned)slen || memcmp(buf,s,slen)) return REDIS_ERR;
if (llongval) *llongval = value;
return REDIS_OK;
}
get
- 解释: 获取键的值,如果键不存在则返回nil,如果键存的不是字符串类型,则会返回错误。
- 用法: get key
- 示例
127.0.0.1:6379> set key1 value1
ok
//键存在
127.0.0.1:6379> get key1
"value1"
//键不存在
127.0.0.1:6379> del key1
(integer) 1
127.0.0.1:6379> get key1
(nil)
- 源码分析
/**
* t_string.c
**/
int getGenericCommand(redisClient *c) {
robj *o;
//在dict查找键,如果键的对象为null,直接返回
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
return REDIS_OK;
// 如果键的值类型不为字符串返回错误
if (o->type != REDIS_STRING) {
addReply(c,shared.wrongtypeerr);
return REDIS_ERR;
} else {
//键的值类型为字符串返回值
addReplyBulk(c,o);
return REDIS_OK;
}
}
mset
-
解释:
批量设置多个键的值,如果其中有键已存在则会替代之前的值,
mset是原子的,mset命令不会失败,所以设置结果返回Ok -
用法: mset key1 value1 key2 value2
-
示例
127.0.0.1:6379> mset key1 value1 key2 value2
ok
- 源码分析
/**
* redis.c
*/
struct redisCommand readonlyCommandTable[] = {
{"mset",msetCommand,-3,REDIS_CMD_DENYOOM,NULL,1,-1,2}
}
/**
* t_string.c
*/
void msetCommand(redisClient *c) {
msetGenericCommand(c,0);
}
/**
* t_string.c
**/
void msetGenericCommand(redisClient *c, int nx) {
// nx = 0 false c语言非0即为真,在这个函数里表示,直接设置键值对。
int j, busykeys = 0;
//校验mset命令是否正确,mset命令的参数是 1(mset) + 2n (key1 +value1 + key2 + value2..... )
if ((c->argc % 2) == 0) {
addReplyError(c,"wrong number of arguments for MSET");
return;
}
/* Handle the NX flag. The MSETNX semantic is to return zero and don't
* set nothing at all if at least one already key exists. */
// 这里nx为false不执行执行这里
if (nx) {
//如果设置有nx
for (j = 1; j < c->argc; j += 2) {
//j = 1 从第一个键开始
if (lookupKeyWrite(c->db,c->argv[j]) != NULL) {
//如果键存在,则计数
busykeys++;
}
}
}
//busykeys大于0,表示设置的键已经存在,命令失败,其他键不设置,返回。
//mset是原子的,所有的键设置要么都成功,要么都失败。
if (busykeys) {
addReply(c, shared.czero);
return;
}
//设置键的值
for (j = 1; j < c->argc; j += 2) {
//获取要设置键的值
c->argv[j+1] = tryObjectEncoding(c->argv[j+1]);
//如果键存在更新值,否则添加键值对
dbReplace(c->db,c->argv[j],c->argv[j+1]);
//对应的引用计数加一
incrRefCount(c->argv[j+1]);
//过期的键删除
removeExpire(c->db,c->argv[j]);
touchWatchedKey(c->db,c->argv[j]);
}
server.dirty += (c->argc-1)/2;
//nx =false,执行成功
addReply(c, nx ? shared.cone : shared.ok);
}
mget
-
解释:
批量获取键的值,如果键不存在或者这不是字符串类型将返回nil,
mget命令不会失败。
Redis的性能瓶颈在于网络的时间,使用批量命令能够提高效率。
N次get命令耗时= N次网络时间 + N次get命令耗时
N个键的获取使用mget命令耗时 = 1次网络时间 + N次get命令耗时
如果批量获取键的值非常多,会导致阻塞Redis,影响其他命令的执行。 -
用法: mget key1 key2 key3
-
示例:
127.0.0.1:6379> mset key1 value1 key2 value2
127.0.0.1:6379> megt key1 key2 key3
"value1"
"value2"
(nil)
- 源码分析
/**
* t_string.c
**/
void mgetCommand(redisClient *c) {
int j;
addReplyMultiBulkLen(c,c->argc-1);
//第一个参数是mget命令,所以从第二个参数开始
for (j = 1; j < c->argc; j++) {
//查找键
robj *o = lookupKeyRead(c->db,c->argv[j]);
if (o == NULL) {
//键不存在,返回单个键的结果nil
addReply(c,shared.nullbulk);
} else {
//键存在,不是字符串类型,返回nil
if (o->type != REDIS_STRING) {
addReply(c,shared.nullbulk);
} else {
//是字符串类型,返回键的值
addReplyBulk(c,o);
}
}
}
}
incr
-
解释:
每执行一次键的值加一,如果键不存在,则返回1,
如果键的值不是整型,则返回错误。 -
用法: incr key
-
示例
127.0.0.1:6379> incr key1
(integer) 1
127.0.0.1:6379> set key2 value2
127.0.0.1:6379> incr key2
(error) ERR value is not a integer or out of range
127.0.0.1:6379> set key3 1
ok
127.0.0.1:6379> incr key3
(integer) 2
- 源码分析
/**
* t_string.c
**/
void incrCommand(redisClient *c) {
//键的值增加1
incrDecrCommand(c,1);
}
/**
* t_string.c
**/
void incrDecrCommand(redisClient *c, long long incr) {
long long value, oldvalue;
robj *o;
//查找键
o = lookupKeyWrite(c->db,c->argv[1]);
//如果键值类型不是字符串,则直接返回
if (o != NULL && checkType(c,o,REDIS_STRING)) return;
//如果键的值不是长整型,则直接返回
if (getLongLongFromObjectOrReply(c,o,&value,NULL) != REDIS_OK) return;
//设置键的当前值
oldvalue = value;
//加一
value += incr;
//防止溢出,如果incr为负数,则操作会减少值,如果incr为正数,则操作会增加值,否则失败
if ((incr < 0 && value > oldvalue) || (incr > 0 && value < oldvalue)) {
addReplyError(c,"increment or decrement would overflow");
return;
}
//长整型转换为字符串
o = createStringObjectFromLongLong(value);
//设置或更新值
dbReplace(c->db,c->argv[1],o);
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
addReply(c,shared.colon);
addReply(c,o);
addReply(c,shared.crlf);
}
append
-
解释:
字符串追加,如果键不存在设置值,返回键操作后的长度值。
如果键存在并且是字符串类型则追加并且返回操作后的长度值,
否则返回错误。由于Redis在实现字符串的时候,存在预分配机制,如果大量使用追加
会导致内存浪费,以及碎片化。 -
用法: append key value
-
示例:
127.0.0.1:6379> append key1 1
(integer) 1
127.0.0.1:6379> append key1 1
(integer) 2
127.0.0.1:6379> sadd key2 1
(integer) 1
127.0.0.1:6379> append key2 1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
- 源码分析:
/**
* redis.c
**/
struct redisCommand readonlyCommandTable[] = {
{"append",appendCommand,3,REDIS_CMD_DENYOOM,NULL,1,1,1}
};
/**
* t_string.c
字符串追加
*/
void appendCommand(redisClient *c) {
size_t totlen;
robj *o, *append;
//查找键是否存在
o = lookupKeyWrite(c->db,c->argv[1]);
if (o == NULL) {
//键不存在,添加键值
//键值编码
c->argv[2] = tryObjectEncoding(c->argv[2]);
//添加键值
dbAdd(c->db,c->argv[1],c->argv[2]);
//引用计数加一
incrRefCount(c->argv[2]);
//计算字符串当前的长度
totlen = stringObjectLen(c->argv[2]);
} else {
//如果键不是字符串,返回错误信息
if (checkType(c,o,REDIS_STRING))
return;
//获取追加的值
append = c->argv[2];
//计算追加完后,字符串的长度
totlen = stringObjectLen(o)+sdslen(append->ptr);
//如果追加后字符大小大于512M,返回错误。
if (checkStringLength(c,totlen) != REDIS_OK)
return;
/* If the object is shared or encoded, we have to make a copy */
if (o->refcount != 1 || o->encoding != REDIS_ENCODING_RAW) {
robj *decoded = getDecodedObject(o);
o = createStringObject(decoded->ptr, sdslen(decoded->ptr));
decrRefCount(decoded);
dbReplace(c->db,c->argv[1],o);
}
//添加追加值,如果空间不够,预分配一倍空间(2.2源码)
o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
//计算当前长度
totlen = sdslen(o->ptr);
}
touchWatchedKey(c->db,c->argv[1]);
server.dirty++;
addReplyLongLong(c,totlen);
}
/**
** sds.c
**/
sds sdscatlen(sds s, void *t, size_t len) {
struct sdshdr *sh;
size_t curlen = sdslen(s);
//预分配空间
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
sh = (void*) (s-(sizeof(struct sdshdr)));
memcpy(s+curlen, t, len);
sh->len = curlen+len;
sh->free = sh->free-len;
s[curlen+len] = '\0';
return s;
}
/**
** sds.c
** 是否需要预留一倍空间
**/
static sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
//如果可用空间大于等于本次追加的字符串,则直接返回
if (free >= addlen) return s;
//当前的大小
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
//与预留一倍空间
newlen = (len+addlen)*2;
//申请内存
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
#ifdef SDS_ABORT_ON_OOM
if (newsh == NULL) sdsOomAbort();
#else
if (newsh == NULL) return NULL;
#endif
newsh->free = newlen - len;
return newsh->buf;
}
strlen
-
解释:
返回字符串键的长度,如果键不存在则返回0,否则返回实际长度。
一个英文字符是1个字节,一个中文字符是3个字节。
当键不是字符串类型时,返回错误。小扩展:
ASCII编码下,一个英文字符占1个字节(1byte=8bit),
一个中文字符占2个字节。
UTF8编码下,一个英文字符占1个字节,一个中文字符占3个字节。 -
用法: strlen key
-
示例:
127.0.0.1:6379> strlen key1
integer) 0
127.0.0.1:6379> set key1 1
ok
127.0.0.1:6379> strlen key1
(integer) 1
127.0.0.1:6369> sadd key2 1
(integer)1
127.0.0.1:6379> strlen key2
(error) WRONGTYPE Operation against a key holding the wrong kind of value
- 源码分析:
/**
* redis.c
**/
struct redisCommand readonlyCommandTable[] = {
{"strlen",strlenCommand,2,0,NULL,1,1,1}
};
/**
** t_string.c
**/
void strlenCommand(redisClient *c) {
robj *o;
//如果键不存在或者不是字符串类型,返回错误。
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,REDIS_STRING)) return;
addReplyLongLong(c,stringObjectLen(o));
}
/**
* object.c
**/
size_t stringObjectLen(robj *o) {
//断言,是否是字符串类型,不是字符串类型返回错误
redisAssert(o->type == REDIS_STRING);
//如果编码是raw,直接计算
if (o->encoding == REDIS_ENCODING_RAW) {
return sdslen(o->ptr);
} else {
char buf[32];
//字符串是长整形,转换为字符串,再计算长度
return ll2string(buf,32,(long)o->ptr);
}
}
getset
- 解释:
设置键的值,并且返回之前的值,如果键不是字符串类型返回错误
如果键之前不存在,返回nil - 用法: getset key value
- 示例:
127.0.0.1:6379> getset key1 value1
(nil)
127.0.0.1:6379> getset key1 1
"value1"
127.0.0.1:6379> sadd key2 1
(integer) 1
127.0.0.1:6379> getset key2 1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
setrange
-
解释:
设置键指定偏移后的字符串,返回结果为执行命令后,键的长度
如果偏移位置超过了字符串的长度,则用0字节补齐。
如果键不存在,命令会先给键设置一个空字符,再执行偏移量操作。 -
用法: setrange key offset value
-
示例:
127.0.0.1:6379> set key1 1234567
127.0.0.1:6379> setrange key1 2 5
(integer) 7
127.0.0.1:6379> set key2 1 2
(integer)2
127.0.0.1:6379> get key2
"\x002"
getrange
-
解释:
返回键指定位置的字符串,start=1从第一字符开始。
start=-2从倒数第二个字符,不在范围内返回空字符串。 -
用法: getrange key1 start end
-
示例:
127.0.0.1:6379> set key1 123456789
127.0.0.1:6379> get key1 8 9
"9"
127.0.0.1:6379> get key1 10 100
""
127.0.0.1:6379> get key1 -2 -1
"9"
内部编码
字符串的内部编码有raw,int,embstr(3.0版本才出现)
int最大4个字节,如果不够会使用raw来存储,raw最大512M。
典型使用场景
分布式锁
使用 set key value nx
可以保证只有个操作可以成功。
计数
当请求量过大,一些简单的操作如记录点赞数,评论数,播放数,可以用redis的 ``incr key ```来计数,定时再刷新会数据库,
可以大大减少数据的压力。
缓存
可以缓存用户信息,不过为了加快更新用户信息的时间,和查找用户对象的单个属性,建议使用hash来存储。
限制请求
做一些活动是,为了防止用户不断刷新,几秒内用户只能请求成功一次。或者某个IP某段时间只能请求一次。
set key value ex 2000 nx
字符串优化
对某些场景,减少append的操作,以及像用户信息这种,有多个属性,可以使用hash才存储优化。