2.4 字符串
字符串string是Redis最简单的数据结构。Redis所有的数据结构都是以唯⼀的key字符串作为名称,然后通过这个唯⼀key值来获取相应的value数据。不同类型的数据结构的差异就在于value的结构不⼀样。
字符串的value值类型有三种:1. 字符串;2.整型;3.二进制。
Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java 的ArrayList,采⽤预分配冗余空间的⽅式来减少内存的频繁分配,当字符串⻓度⼩于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时⼀次只会多扩1M的空间。需要注意的是字符串最⼤⻓度为512M。
我们看一下它的常用API
1. GET¶
通用命令 | GET key pattern(pattern 为正则表达式) |
---|---|
功能描述 | 返回与键 key 相关联的字符串值。如果键key不存在,那么返回特殊值nil;否则,返回键key的值。如果键key的值并非字符串类型,那么返回一个错误,因为GET命令只能用于字符串值 |
时间复杂度 | O(1) |
示例
对不存在的键 key 或是字符串类型的键 key 执行 GET 命令:
redis> GET db (nil) redis> SET db redis OK redis> GET db "redis"
对不是字符串类型的键 key 执行 GET 命令:
redis> DEL db (integer) 1 redis> LPUSH db redis mongodb mysql (integer) 3 redis> GET db (error) ERR Operation against a key holding the wrong kind of value
2.set¶
命令 | SET key value [EX seconds] [PX milliseconds] [NX|XX] |
---|---|
功能描述 | 将字符串值 value 关联到 key 。如果 key 已经持有其他值, SET 就覆写旧值, 无视类型。当 SET 命令对一个带有生存时间(TTL)的键进行设置之后, 该键原有的 TTL 将被清除 |
时间复杂度 | O(1) |
可选参数 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
- EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
- PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
- NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
- XX : 只在键已经存在时, 才对键进行设置操作。
关于返回值
- 在 Redis 2.6.12 版本以前, SET 命令总是返回 OK 。
- 从 Redis 2.6.12 版本开始, SET 命令只在设置操作成功完成时才返回 OK ; 如果命令使用了 NX 或者 XX 选项, 但是因为条件没达到而造成设置操作未执行, 那么命令将返回空批量回复(NULL Bulk Reply)。
3. INCR¶
命令 | INCR key |
---|---|
功能描述 | 为键 key 储存的数字值加上一。如果键 key 不存在, 那么它的值会先被初始化为 0 , 然后再执行 INCR 命令。如果键 key 储存的值不能被解释为数字, 那么 INCR 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。返回值是DECR 命令会返回键 key 在执行减一操作之后的值。 |
时间复杂度 | O(1) |
对储存数字值的键 key 执行 DECR 命令:
redis> SET page_view 20 OK redis> INCR page_view (integer) 21 redis> GET page_view # 数字值在 Redis 中以字符串的形式保存 "21"
对不存在的键执行 DECR 命令:
redis> EXISTS count (integer) 0 redis> DECR count (integer) -1
4. DECR¶
命令 | DECRBY key decrement |
---|---|
功能描述 | 将键 key 储存的整数值减去减量 decrement 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 DECRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。返回值是DECRBY 命令会返回键在执行减法操作之后的值。 |
时间复杂度 | O(1) |
对已经存在的键执行 DECRBY 命令:
redis> SET count 100 OK redis> DECRBY count 20 (integer) 80
对不存在的键执行 DECRBY 命令:
redis> EXISTS pages (integer) 0 redis> DECRBY pages 10 (integer) -10
5. INCRBY¶
命令 | INCRBY key increment |
---|---|
功能描述 | 为键 key 储存的数字值加上增量 increment 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 INCRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 INCRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。返回值为在加上增量 increment 之后, 键 key 当前的值。 |
时间复杂度 | O(1) |
示例演示与上面类似
6. DECRBY¶
命令 | DECRBY key decrement |
---|---|
功能描述 | 将键 key 储存的整数值减去减量 decrement 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 DECRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。 返回值DECRBY 命令会返回键在执行减法操作之后的值 |
时间复杂度 | O(1) |
示例演示与上面类似
使用上面这一些命令,其实我们就可以做一些事情了。
应用¶
1.比如说记录每个用户博文的访问量
incr userid:pageview(单线程:无竞争)
2.缓存用户的基本信息(数据源在 MySQL中),信息被序列化存放在value中。
一般而言,需要通过我们自定义规则的key,从Redis获取value,如果key存在的话,则直接获取value使用;如果不存在的话,从Mysql中读取使用,然后存在Redis中。 主要的命令是 get 和 set
3.分布式id生成器
如果集群规模和运算不太复杂的话,可以用Redis生成分布式id,因为Redis单线程的特点,一次只执行一条指令,保证了id值的唯一。
主要的命令还是incr
7. SETNX¶
命令 | SETNX key value |
---|---|
功能描述 | 只在键key 不存在的情况下,将键key的值设置为value 。若键key已经存在,则SETNX命令不做任何动作。命令在设置成功时返回1,设置失败时返回 0 |
时间复杂度 | O(1) |
redis> EXISTS job # job 不存在 (integer) 0 redis> SETNX job "programmer" # job 设置成功 (integer) 1 redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败 (integer) 0 redis> GET job # 没有被覆盖 "programmer"
8. SETEX¶
命令 | SETEX key value |
---|---|
功能描述 | 将键key的值设置为value ,并将键key的生存时间设置为seconds 秒钟。如果键key已经存在,那么SETEX 命令将覆盖已有的值。命令在设置成功时返回OK。当seconds参数不合法时,命令将返回一个错误 |
时间复杂度 | O(1) |
SETEX 命令的效果和以下两个命令的效果类似:
SET key value EXPIRE key seconds # 设置生存时间
SETEX和这两个命令的不同之处在于SETEX是一个原子(atomic)操作,它可以在同一时间内完成设置值和设置过期时间这两个操作,因此SETEX命令在储存缓存的时候非常实用。
这两个命令的典型应用就是分布式锁了。
⽐如⼀个操作要修改⽤户的状态,修改状态需要先读出⽤户的状态,在内存⾥进⾏修改,改完了再存回去。如果这样的操作同时进⾏了,就会出现并发问题。这个时候就要使⽤到分布式锁来限制程序的并发执⾏。Redis分布式锁使⽤⾮常⼴泛,必须要掌握。
分布式锁本质上要实现的⽬标就是在Redis⾥⾯占⼀个位置,当别的进程也要来占时,发现位置被占了,就只好放弃或者稍后再试。
占位置⼀般是使⽤setnx(set if not exists)指令,只允许被⼀个客户端占据。先来先占,⽤完了,再调⽤del指令释放位置。
如果逻辑执⾏到中间出现异常了,可能会导致del指令没有被调⽤,这样就会陷⼊死锁,锁永远得不到释放。于是我们在拿到锁之后,再给锁加上⼀个过期时间。
但如果在setnx和expire之间服务器进程突然挂掉了,可能是因为机器掉电或者是被⼈为杀掉的,就会导致expire 得不到执⾏,也会造成死锁。
原因是setnx和expire是两条指令⽽不能保证都一定成功执行。如果这两条指令可以⼀起执⾏就不会出现问题(要么成功,要么失败)。所以说setex是最佳的方案
上面就是分布式锁的基本思想。但是在真正投入使用的时候,还会面临一个常见的问题:超时问题
Redis的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执⾏的时间太⻓,超出了锁的超时限制,就会出现问题。这时候第⼀个线程持有的锁过期了,临界区的逻辑没有执⾏完,而第⼆个线程就提前重新持有了这把锁,导致临界区代码不能严格地串⾏执⾏。
为了避免这个问题,Redis分布式锁不要⽤于较⻓时间的任务。
9. MSET¶
命令 | MSET key value [key value …] |
---|---|
功能描述 | 同时为多个键设置值。如果某个给定键已经存在, 那么 MSET 将使用新值去覆盖旧值, 如果这不是你所希望的效果, 请考虑使用 MSETNX 命令, 这个命令只会在所有给定键都不存在的情况下进行设置。MSET 是一个原子性(atomic)操作, 所有给定键都会在同一时间内被设置, 不会出现某些键被设置了但是另一些键没有被设置的情况。MSET 命令总是返回 OK 。 |
时间复杂度 | O(N),其中 N 为被设置的键数量 |
同时对多个键进行设置:
redis> MSET date "2012.3.30" time "11:00 a.m." weather "sunny" OK redis> MGET date time weather 1) "2012.3.30" 2) "11:00 a.m." 3) "sunny"
覆盖已有的值:
redis> MGET k1 k2 1) "hello" 2) "world" redis> MSET k1 "good" k2 "bye" OK redis> MGET k1 k2 1) "good" 2) "bye"
10 . MGET¶
命令 | MGET key [key …] |
---|---|
功能描述 | 返回给定的一个或多个字符串键的值。如果给定的字符串键里面, 有某个键不存在, 那么这个键的值将以特殊值 nil 表示。MGET 命令将返回一个列表, 列表中包含了所有给定键的值。 |
O(N),其中N为被设置的键数量。 |
redis> SET redis redis.com OK redis> SET mongodb mongodb.org OK redis> MGET redis mongodb 1) "redis.com" 2) "mongodb.org" redis> MGET redis mongodb mysql # 不存在的 mysql 返回 nil 1) "redis.com" 2) "mongodb.org" 3) (nil)
下面说说mset和mget的好处
- 不使用mget和mset::
客户端和服务器端可能不在同一个地方
n次get/set=n次网络时间+n次命令时间
- 一次mget/mset:
1次mget/mset=1次网络时间+n次命令时间
随着n的增大,差距一下子就体现出来了。
11. GETSET¶
GETSET key value
将键key的值设为value,并返回键key 在被设置之前的旧值。
12. STRLEN¶
STRLEN key
返回键key储存的字符串值的长度
13. APPEND¶
APPEND key value
如果键key已经存在并且它的值是一个字符串,APPEND命令将把value追加到键key现有值的末尾。
14. INCRBYFLOAT¶
INCRBYFLOAT key increment
为键key储存的值加上浮点数增量increment。
如果键key不存在,那么INCRBYFLOAT会先将键key的值设为0,然后再执行加法操作。
如果命令执行成功,那么键key的值会被更新为执行加法计算之后的新值,并且新值会以字符串的形式返回给调用者。
无论是键key的值还是增量 increment,都可以使用像2.0e7、3e5、90e-2那样的指数符号(exponential notation)来表示,但是,执行INCRBYFLOAT 命令之后的值总是以同样的形式储存,也即是,它们总是由一个数字,一个(可选的)小数点和一个任意长度的小数部分组成(比如 3.14、69.768,诸如此类),小数部分尾随的 0会被移除,如果可能的话,命令还会将浮点数转换为整数(比如3.0会被保存成3)。
此外,无论加法计算所得的浮点数的实际精度有多长,INCRBYFLOAT 命令的计算结果最多只保留小数点的后十七位。当以下任意一个条件发生时,命令返回一个错误:
- 键key的值不是字符串类型(因为Redis中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型);
- 键key当前的值或者给定的增量increment不能被解释(parse)为双精度浮点数。
15. GETRANGE¶
GETRANGE key start end
返回键key储存的字符串值的指定部分,字符串的截取范围由start和end 两个偏移量决定(包括start和end在内)。 负数偏移量表示从字符串的末尾开始计数,-1表示最后一个字符,-2表示倒数第二个字符,以此类推。
GETRANGE通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。
16. SETRANGE¶
SETRANGE key offset value
从偏移量offset开始,用value参数覆写(overwrite)键key储存的字符串值。不存在的键key当作空白字符串处理。
SETRANGE命令会确保字符串足够长以便将value设置到指定的偏移量上,如果键key原来储存的字符串长度比偏移量小(比如字符串只有5个字符长,但你设置的offset是10),那么原字符和偏移量之间的空白将用零字节(zerobytes,“\x00”)进行填充。
因为Redis字符串的大小被限制在512兆(megabytes)以内,所以用户能够使用的最大偏移量为2^29-1(536870911),如果你需要使用比这更大的空间,请使用多个key。
在对字符串类型有了整体的了解之后,我们看看它具体的结构
Redis的字符串名字是SDS(Simple Dynamic String)。它的结构是⼀个带⻓度信息的字节数组。
struct SDS<T> { T capacity; // 数组容量 T len; // 数组⻓度 byte flags; // 特殊标识位,不理睬它 byte[] content; // 数组内容 }
capacity表示所分配数组的⻓度,len表示字符串的实际⻓度。前⾯API提到⽀持append操作(字符串是可修改的)。如果数组没有冗余空间,那么追加操作必然涉及到分配新数组,然后将旧内容复制过来,再append新内容。如果字符串的⻓ 度⾮常⻓,这样的内存分配和复制开销就会⾮常⼤。
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the * end of the specified sds string 's' . * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscatlen(sds s, const void *t, size_t len) { size_t curlen = sdslen(s); // 原字符串⻓度 // 按需调整空间,如果 capacity 不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中 s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; // 内存不⾜ memcpy(s+curlen, t, len); // 追加⽬标字符串的内容到字节数组中 sdssetlen(s, curlen+len); // 设置追加后的⻓度值 s[curlen+len] ='\0'; // 让字符串以\0 结尾,便于调试打印,还可以直接使⽤ glibc 的字符串函数进⾏操作 return s; }
上⾯的SDS结构使⽤了范型 T,这是Redis对内存做出的优化,不同⻓度的字符串使⽤不同的结构体来表示,字符串⽐较短时,len和capacity可以使⽤byte和 short来表示。
Redis规定字符串的⻓度不得超过512M字节。创建字符串时len和capacity⼀样⻓,不会多分配冗余空间,这是因为绝⼤多数场景下我们不会使⽤append操作来修改字符串。