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操作来修改字符串。

版权声明: 本文为智客工坊「沉晓」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

results matching ""

    No results matching ""