Skip to content

字符串

字符串键是 Redis 最基本的的键值对会在数据库中把单独的一个键和单独的一个值关联起来,被关联的键和值既可以是普通的文字数据,也可以是图片、视频、音频、压缩文件等更为复杂的二进制数据。

字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如 JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过 512 MB。

命令

SET: 为字符串键设置值

创建字符串键最常用的方法就是使用 SET 命令,这个命令可以为一个字符串键设置相应的值。
在最基本的情况下,用户只需要向 SET 命令提供一个键和一个值就可以了

SET key value [ex seconds] [px milliseconds] [nx | xx]

与之前提到过的一样,这里的键和值既可以是文字也可以是二进制数据

SET 命令在成功创建字符串键之后将返回 OK 作为结果。
比如通过执行以下命令,我们可以创建出一个字符串键,它的键为"number",值为"10086":

1
2
127.0.0.1:6379> SET number "10086"
OK

再比如,通过执行以下命令,我们可以创建出一个键为"book",值为"The Design and Implementation of Redis"的字符串键

1
2
127.0.0.1:6379> SET book "The Design and Implementation of Redis"
OK

注意,在实际中,Redis 数据库是以无序的方式存放数据库键的,一个新加入的键可能会出现在数据库的任何位置上,因此我们在使用 Redis 的过程中不应该对键在数据库中的摆放位置做任何假设,以免造成错误。

set 命令有几个选项:

  • ex seconds: 为键设置秒级过期时间
  • px milliseconds: 为键设置毫秒级过期时间
  • nx: 键必须不存在,才可以设置成功,用于添加
  • xx: 与 nx 相反,键必须存在,才可以设置成功,用于更新

除了 set 选项,Redis 还提供了 setex 和 setnx 两个命令:

1
2
setex key seconds value
setnx key value

它们的作用和 ex 和 nx 选项是一样的。
下面的例子说明了 set、setnx、set xx 的区别

当前键 hello 不存在

1
2
127.0.0.1:6379> exists hello
(integer) 0

设置键为 hello,值为 world 的键值对:

1
2
127.0.0.1:6379> set hello world
OK

因为键 hello 已存在,所以 setnx 失败,返回结果为 0:

1
2
127.0.0.1:6379> setnx hello redis
(integer) 0

因为键 hello 已存在,所以 set xx 成功,返回结果为 OK:

1
2
127.0.0.1:6379> set hello jedis xx
OK

setnx 和 setxx 在实际使用中有什么应用场景吗?
以 setnx 命令为例子,由于 Redis 的单线程命令处理机制,如果有多个客户端同时执行 setnx key value,根据 setnx 的特性只有一个客户端能设置成功,setnx 可以作为分布式锁的一种实现方案

在默认情况下,对一个已经设置了值的字符串键执行 SET 命令将导致键的旧值被新值覆盖。

1
2
3
4
127.0.0.1:6379> SET song_title "Get Wild"
OK
127.0.0.1:6379> SET song_title "Running to Horizon"
OK

在第二条 SET 命令执行完毕之后,song_title 键的值将从原来的"Get Wild"变为"Running to Horizon"

从 Redis 2.6.12 版本开始,用户可以通过向 SET 命令提供可选的 NX 选项或者 XX 选项来指示 SET 命令是否覆盖一个已经存在的值:

1
SET key value [NX|XX]

如果用户在执行 SET 命令时给定了 NX 选项,那么 SET 命令只会在键没有值的情况下设置操作,并返回 OK 表示设置成功;
如果键已经存在,那么 SET 命令将放弃执行设置操作,并返回空值 nil 表示设置失败。

以下代码展示了带有 NX 选项的 SET 命令的行为:

1
2
3
4
127.0.0.1:6379> SET password "123456" NX
OK              -- 对尚未有值的 password 键进行设置,成功
127.0.0.1:6379> SET password "999999" NX
(nil)           -- password 键已经有了值,设置失败

因为第二条 SET 命令没有改变 password 键的值,所以 password 键的值仍然是刚开始时设置的 "123456"

如果用户在执行 SET 命令时给定了 XX 选项,那么 SET 命令只会在键已经有值的情况下执行设置操作,并返回 OK 表示设置成功;
如果给定的键并没有值,那么 SET 命令将放弃执行设置操作,并返回空值表示设置失败。

举个例子,如果我们对一个没有值的键 mongodb-homepage 执行以下 SET 命令,那么命令将因为 XX 选项的作用而放弃执行设置操作:

1
2
127.0.0.1:6379> SET mongodb-homepage "mongodb.com" XX
(nil)

相反,如果我们对一个已经有值的键执行带有 XX 选项的 SET 命令,那么命令将使用新值去覆盖已有的旧值:

1
2
3
4
127.0.0.1:6379> SET mysql-homepage "mysql.org"
OK
127.0.0.1:6379> SET mysql-homepage "mysql.com" XX
OK

在第二条 SET 命令执行之后,mysql-homepage 键的值将从原来的"mysql.org"更新为"mysql.com"

GET: 获取字符串键的值

用户可以使用 GET 命令从数据库中获取指定字符串键的值:

GET key

GET 命令接受一个字符串键作为参数,然后返回与该键相关联的值。

1
2
3
4
5
6
127.0.0.1:6379> GET message
"hello world"
127.0.0.1:6379> GET number
"10086"
127.0.0.1:6379> GET homepage
"redis.io"

另外,如果用户给定的字符串键在数据库中并没有与之相关联的值,那么 GET 命令将返回一个空值:

1
2
127.0.0.1:6379> GET date
(nil)

上面这个 GET 命令的执行结果表示数据库中并不存在 date 键,也没有与之相关联的值

因为 Redis 的数据库要求所有键必须拥有与之相关联的值,所以如果一个键有值,那么我们就说这个键存在于数据库;
相反,如果一个键没有值,那么我们就说这个键不存在于数据库。
比如对于上面展示的几个键来说,date 键就不存在数据库,而 message 键、number 键和 homepage 键则存在于数据库

批量设置/获取值

批量设置值:

mset key value [key value ...]

下面操作通过 mset 命令一次性设置 4 个键值对:

1
2
127.0.0.1:6379> mset a 1 b 2 c 3 d 4
OK

批量获取值:

mget key [key ...

下面操作批量获取了键 a、b、c、d 的值:

1
2
3
4
5
127.0.0.1:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"

如果有些键不存在,那么它的值为 nil(空),结果是按照传入键的顺序返回:

1
2
3
4
5
127.0.0.1:6379> mget a b c f
1) "1"
2) "2"
3) "3"
4) (nil)

批量操作命令可以有效提高开发效率,假如没有 mget 这样的命令,要执行 n 次 get 命令需要按照下图的方式来执行,具体耗时如下:

n次get 时间 = n次网络时间 + n次命令时间

使用 mget 命令后,要执行 n 次 get 命令操作只需要按照下图的方式来完成,具体耗时如下:

n次get时间= 1次网络时间 + n次命令时间

Redis 可以支撑每秒数万的读写操作,但是这指的是 Redis 服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,
假设网络时间为 1 毫秒,命令时间为 0.1 毫秒(按照每秒处理 1 万条命令算),那么执行 1000 次 get 命令和 1 次 mget 命令的区别如表,
因此 Redis 的处理能力已经足够高,对于开发人员来说,网络可能会成为性能的瓶颈

操作 时间
1000次 get 1000 * 1 + 1000 * 0.1 = 1100ms
1次mset 1 * 1 + 1000 * 0.1 = 101ms

学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成 Redis 阻塞或者网络拥塞

计数

incr key

incr 命令用于对值做自增操作,返回结果分为三种情况:

  • 值不是整数,返回错误
  • 值是整数,返回自增后的结果
  • 键不存在,按照值为 0 自增,返回结果为 1

例如对一个不存在的键执行 incr 操作后,返回结果是 1:

1
2
3
4
127.0.0.1:6379> exists key
(integer) 0
127.0.0.1:6379> incr key
(integer) 1

再次对键执行 incr 命令,返回结果是 2:

1
2
127.0.0.1:6379> incr key
(integer) 2

如果值不是整数,那么会返回错误:

1
2
3
4
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range

除了 incr 命令,Redis 提供了 decr(自减)、incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数):

1
2
3
4
decr key
incryby key increment
decrby key decrement
incrbyfloat key increment

很多存储系统和编程语言内部使用 CAS 机制实现计数功能,会有一定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执行

追加值

append key value

append 可以向字符串尾部追加值,例如:

1
2
3
4
5
6
7
8
127.0.0.1:6379> set key redis
OK
127.0.0.1:6379> get key
"redis"
127.0.0.1:6379> append key world
(integer) 10
127.0.0.1:6379> get key
"redisworld"

字符串长度

strlen key

例如,当前值为 redisworld,所以返回值为 10

1
2
3
4
127.0.0.1:6379> get key
"redisworld"
127.0.0.1:6379> strlen key
(integer) 10

下面操作返回结果为 6,因为每个中文占用 3 个字节:

1
2
3
4
127.0.0.1:6379> set hello "世界"
OK
127.0.0.1:6379> strlen hello
(integer) 6

GETSET: 获取旧值并设置新值

GETSET 命令就像 GET 命令和 SET 命令的组合版本,GETSET 首先获取字符串键目前已有的值,接着为键设置新值,最后把之前获取到的旧值返回给用户:

GETSET key new_value

以下代码展示了如何使用 GETSET 命令去获取 number 键的旧值并为它设置新值:

1
2
3
4
5
6
127.0.0.1:6379> GET number
"10086"
127.0.0.1:6379> GETSET number "12345"
"10086"
127.0.0.1:6379> GET number
"12345"

如果被设置的键并不存在于数据库,那么 GETSET 命令将返回空值作为键的旧值

1
2
3
4
5
6
127.0.0.1:6379> GET counter
(nil)
127.0.0.1:6379> GETSET counter 50
(nil)
127.0.0.1:6379> GET counter
"50"

示例: 缓存

对数据进行缓存是 Redis 最常见的用法之一,因为缓存操作是指把数据存储在内存而不是硬盘上,而访问内存远比访问硬盘的速度要快得多,所以用户可以通过把需要快速访问的数据存储在 Redis 来提升应用程序的速度。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from redis import Redis

class Cache:
    def __init__(self, client):
        self.client = client

    def set(self, key, value):
        self.client.set(key, value)

    def get(self, key):
        return self.client.get(key)

    def update(self, key, new_value):
        return self.client.getset(key, new_value)

除了用于设置缓存的 set() 方法以及用于获取缓存的 get() 方法之外,缓存程序还提哦给你了由 gETSET 命令实现的 update() 方法,这个方法可以让用户在对缓存进行设置的同时,获得之前被缓存的旧值。
用户可以根据自己的需要决定是使用 set() 方法还是 update() 方法对缓存进行设置

1
2
3
4
5
6
7
8
# 使用文本编码方式打开客户端
client = Redis(decode_responses=True)

cache = Cache(client)
cache.set("greeting-page", "<html><p>hello world</p></html>")
print(cache.get("greeting-page"))
print(cache.update("greeting-page", "<html><p>good morning</p></html>"))
print(cache.get("greeting-page"))

输出:

1
2
3
<html><p>hello world</p></html>
<html><p>hello world</p></html>
<html><p>good morning</p></html>

因为 Redis 的字符串键不仅可以存储文本数据,还可以存储二进制数据,所以这个缓存程序不仅可以用来缓存网页等文本数据,还可以用来缓存图片和视频等二进制数据。
比如,如果你正在运营一个图片网站,那么你同样可以使用这个缓存程序来缓存网站上的热门图片,从而提高用户访问这些热门图片的速度。

作为例子,以下代码展示了将 Redis 的 Logo 图片缓存到键 redis-logo.jpg 中的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 使用二进制编码方式打开客户端
client = Redis()

# 以二进制只读方式打开图片文件
image = open("redis-logo.jpg", "rb")
# 读取文件内容
data = image.read()
# 关闭文件
image.close()
cache = Cache(client)
# 将内存缓存到键 redis-logo.jpg 中
cache.set("redis-logo.jpg", data)
# 读取二进制数据的前 20 个字节
print(cache.get("greeting-page")[:20])

示例: 锁

锁是一种同步机制,用于保证一项资源在任何时候只能被一个进程使用,如果有其他进程想要使用相同的资源,那么就必须等待,直到正在使用资源的进程放弃使用权为止。

一个锁的实现通常会有获取(acquire)和释放(release)这两种操作:

  • 获取操作用于取得资源的独占使用权。在任何时候,最多只能有一个进程取得锁,我们把成功取得锁的这个进程称为锁的持有者。在锁已经被持有的情况下,所有尝试再次获取锁的操作都会失败
  • 释放操作用于放弃资源的独占使用权,一般由锁的持有者调用。在锁被释放之后,其他进程就可以再次尝试获取这个锁了。

下面展示了一个使用字符串键实现的锁程序,这个程序会根据给定的字符串键是否有值来判断锁是否已经被获取,而针对锁的获取操作和释放操作则是分别通过设置字符串键和删除字符串键来完成的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from redis import Redis

VALUE_OF_LOCK = "locking"

class Lock:
    def __init__(self, client, key):
        self.client = client
        self.key = key

    def acquire(self):
        result = self.client.set(self.key, VALUE_OF_LOCK, nx=True)
        return result

    def release(self):
        return self.client.delete(self.key) == 1

设置指定位置的字符

setrange key offeset value

下面操作将值由 pest 变为了 best:

1
2
3
4
5
6
127.0.0.1:6379> set redis pest
OK
127.0.0.1:6379> setrange redis 0 b
(integer) 4
127.0.0.1:6379> get redis
"best"

获取部分字符串

getrange key start end

start 和 end 分别是开始和结束的偏移量,偏移量从 0 开始计算,例如下面操作获取了值 best 的前两个字符

1
2
127.0.0.1:6379> getrange redis 0 1
"be"

python中使用

方法 作用 示例 示例结果
set(name, value) 给数据库中键名为name的string赋予值value redis.set('name', 'Bob') True
get(name) 返回数据库中键名为name的string的value redis.get('name') b'Bob'
mset(mapping) 设置多个键值对 redis.mset({'name1': 'Durant', 'name2': 'James'}) True
mget(keys) 返回多个键对应的value组成的列表 redis.mget(['name1', 'name2']) [b'Durant', b'James']
append(key, value) 键名为key的string的值附加value redis.append('name', '666') 6(修改后的字符串长度)
delete(key) 删除键名为key的string redis.delete('name') 1
incr(name, amount=1) 键名为name的value增值操作,默认为1,键不存在则被创建并设为amount redis.incr('age', 1) 1
decr(name, amount=1) 键名为name的value减值操作,默认为1,键不存在则被创建并设为-amount redis.decr('age', 1) 0
setex(name, time, value) 设置name对应的值为string类型的value,并指定此键值对应的有效期 redis.setex('name', 5, 'James') True
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from redis import StrictRedis

redis_cli = StrictRedis("127.0.0.1")
redis_cli.set("name", "zyz")

val = redis_cli.get("name")
print(val, type(val))
print(val.decode(), type(val.decode()))

redis_cli.set("num", 233.45)

val = redis_cli.get("num")
print(type(val.decode()))
val = float(val.decode())
print(val)

输出结果:

1
2
3
4
b'zyz' <class 'bytes'>
zyz <class 'str'>
<class 'str'>
233.45

内部编码

字符串类型的内部编码有 3 种:

  • int: 8 个字节的长整形
  • embstr: 小于等于 39 个字节的字符串
  • raw: 大于 39 个字节的字符串

Redis 会根据当前值的类型和长度决定使用哪种内部编码实现。

整数类型示例如下:

1
2
3
4
127.0.0.1:6379> set key 8653
OK
127.0.0.1:6379> object encoding key
"int"

短字符串示例如下:

小于等于 39 个字节的字符串: embstr

1
2
3
4
127.0.0.1:6379> set key "hello,world"
OK
127.0.0.1:6379> object encoding key
"embstr"

长字符串示例:

大于 39 个字节的字符串: raw

1
2
3
4
5
6
127.0.0.1:6379> set key "one string greater than 39 byte................"
OK
127.0.0.1:6379> strlen key
(integer) 47
127.0.0.1:6379> object encoding key
"raw"

典型使用场景

缓存功能

下图是比较典型的缓存使用场景,其中 Redis 作为缓存层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。
由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用

下面伪代码模拟了访问过程:

(1) 该函数用户获取用户的基础信息

1
2
3
UserInfo getUserInfo(long id) {
    ...
}

(2) 首先从 Redis 获取用户信息

1
2
3
4
5
6
7
8
9
// 定义键
userRedisKey = "user:info:" + id;
// 从 Redis 获取值
value = redis.get(userRedisKey);
if (value != null) {
    // 将值进行反序列化为 UserInfo 并返回结果
    userInfo = deserialize(value);
    return userInfo;
}

开发提示:

与 MySQL 等关系型数据库不同的是,Redis 没有命令空间,而且也没有对键名有强制要求(除了不能使用一些特殊字符)。
但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用 "业务名:对象名:id:[属性]" 作为键名。
例如 MySQL 的数据库名为 vs,用户表名为 user,那么对应的键可以用 "vs:user:1","vs:user:1:name" 来表示,如果当前 Redis 只被一个业务使用,甚至可以去掉 "vs"。
如果键名比较长,例如 "user:{uid}:friends:messages:{mid}",可以在能描述键含义的前提下适当减少键的长度,例如变为 "u:{uid}:fr:m:{mid}",从而减少由于键过长的内存浪费

(3) 如果没有从 Redis 获取到用户信息,需要从 MySQL 中进行获取,并将结果回写到 Redis,添加 1 小时(3600 秒)过期时间:

1
2
3
4
5
6
// 从 MySQL 获取用户信息
userInfo = mysql.get(id);
// 将 userInfo 序列化,并存入 Redis
redis.setex(userRedisKey, 3600, serialize(userInfo));
// 返回结果
return userInfo;

整个功能的伪代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
UserInfo getUserInfo(long id) {
    userRedisKey = "user:info:" + id
    value = redis.get(userRedisKey);
    UserInfo userInfo;
    if (value != null) {
        return deserialize(value)
    }
    userInfo = mysql.get(id);
    if (userInfo != null) {
        redis.setex(userRedisKey, 3600, serialize(userInfo));
    }
    return userInfo;
}

计数

许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。
例如作者所在团队的视频播放数系统就是使用 Redis 作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增 1:

1
2
3
4
long incrVideoCounter(long id) {
    key = "video:playCount:" + id;
    return redis.incr(key);
}

开发提示:
实际上一个真实的计数系统要考虑的问题会很多: 防作弊、按照不同维度计数,数据持久化到底层数据源等。

共享 Session

一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。

为了解决这个问题,可以使用 Redis 将用户的 Sessionn 进行集中管理,在这种模式下只要保证 Redis 是高可用和扩展性的,每次用户更新或者查询登录信息都直接从 Redis 中集中获取

限速

很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。
但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次

此功能可以使用 Redis 来实现,下面的伪代码给出了基本实现思路:

1
2
3
4
5
6
7
8
9
phoneNum = "138xxxxxxxx";
key = "shortMsg:limit:" + phoneNum;
// SET key value EX 60 NX
ifExists = redis.set(key, 1, "EX 60", "NX");
if (isExists != null || redis.incr(key) <= 5) {
    // 通过
} else {
    // 限速
}

上述就是利用 Redis 实现了限速功能,例如一些网站限制一个 IP 地址不能在一秒钟之内访问超过 n 次也可以采用类似的思路