四:Redis的字符串类型命令、源码解析、应用场景

四: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才存储优化。

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

猜你喜欢

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