2.2 字符串
字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。如图2-7所示,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。
图2-7 字符串数据结构
2.2.1 命令
字符串类型的命令比较多,本小节将按照常用和不常用两个维度进行说明,但是这里常用和不常用是相对的,希望读者尽可能都去了解和掌握。
1.?常用命令
(1)设置值
set key value [ex seconds] [px milliseconds] [nx|xx]
下面操作设置键为hello,值为world的键值对,返回结果为OK代表设置成功:
127.0.0.1:6379> set hello world
OK
set命令有几个选项:
ex seconds:为键设置秒级过期时间。
px milliseconds:为键设置毫秒级过期时间。
nx:键必须不存在,才可以设置成功,用于添加。
xx:与nx相反,键必须存在,才可以设置成功,用于更新。
除了set选项,Redis还提供了setex和setnx两个命令:
setex key seconds value
setnx key value
它们的作用和ex和nx选项是一样的。下面的例子说明了set、setnx、set xx的
区别。
当前键hello不存在:
127.0.0.1:6379> exists hello
(integer) 0
设置键为hello,值为world的键值对:
127.0.0.1:6379> set hello world
OK
因为键hello已存在,所以setnx失败,返回结果为0:
127.0.0.1:6379> setnx hello redis
(integer) 0
因为键hello已存在,所以set xx成功,返回结果为OK:
127.0.0.1:6379> set hello jedis xx
OK
setnx和setxx在实际使用中有什么应用场景吗 以setnx命令为例子,由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案,Redis官方给出了使用setnx实现分布式锁的方法:http://redis.io/topics/distlock。
(2)获取值
get key
下面操作获取键hello的值:
127.0.0.1:6379> get hello
"world"
如果要获取的键不存在,则返回nil(空):
127.0.0.1:6379> get not_exist_key
(nil)
(3)批量设置值
mset key value [key value ...]
下面操作通过mset命令一次性设置4个键值对:
127.0.0.1:6379> mset a 1 b 2 c 3 d 4
OK
(4)批量获取值
mget key [key ...]
下面操作批量获取了键a、b、c、d的值:
127.0.0.1:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"
如果有些键不存在,那么它的值为nil(空),结果是按照传入键的顺序返回:
127.0.0.1:6379> mget a b c f
1) "1"
2) "2"
3) "3"
4) (nil)
批量操作命令可以有效提高开发效率,假如没有mget这样的命令,要执行n次get命令需要按照图2-8的方式来执行,具体耗时如下:
n次get时间 = n次网络时间 + n次命令时间
图2-8 n次get命令执行模型
使用mget命令后,要执行n次get命令操作只需要按照图2-9的方式来完成,具体耗时如下:
n次get时间 = 1次网络时间 + n次命令时间
图2-9 一次mget命令执行模型
Redis可以支撑每秒数万的读写操作,但是这指的是Redis服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,假设网络时间为1毫秒,命令时间为0.1毫秒(按照每秒处理1万条命令算),那么执行1000次get命令和1次mget命令的区别如表2-1,因为Redis的处理能力已经足够高,对于开发人员来说,网络可能会成为性能的瓶颈。
表2-1 1000次get和1次get对比表
操 作 时 间
1?000次get 1?000 × 1 + 1?000 × 0.1 = 1?100毫秒 = 1.1秒
1次met(组装了1?000个键值对) 1 × 1 + 1?000 × 0.1 = 101毫秒 = 0.101秒
学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成Redis阻塞或者网络拥塞。
(5)计数
incr key
incr命令用于对值做自增操作,返回结果分为三种情况:
值不是整数,返回错误。
值是整数,返回自增后的结果。
键不存在,按照值为0自增,返回结果为1。
例如对一个不存在的键执行incr操作后,返回结果是1:
127.0.0.1:6379> exists key
(integer) 0
127.0.0.1:6379> incr key
(integer) 1
再次对键执行incr命令,返回结果是2:
127.0.0.1:6379> incr key
(integer) 2
如果值不是整数,那么会返回错误:
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(自增浮点数):
decr key
incrby key increment
decrby key decrement
incrbyfloat key increment
很多存储系统和编程语言内部使用CAS机制实现计数功能,会有一定的CPU开销,但在Redis中完全不存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要顺序执行。
2.?不常用命令
(1)追加值
append key value
append可以向字符串尾部追加值,例如:
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"
(2)字符串长度
strlen key
例如,当前值为redisworld,所以返回值为10:
127.0.0.1:6379> get key
"redisworld"
127.0.0.1:6379> strlen key
(integer) 10
下面操作返回结果为6,因为每个中文占用3个字节:
127.0.0.1:6379> set hello "世界"
OK
127.0.0.1:6379> strlen hello
(integer) 6
(3)设置并返回原值
getset key value
getset和set一样会设置值,但是不同的是,它同时会返回键原来的值,例如:
127.0.0.1:6379> getset hello world
(nil)
127.0.0.1:6379> getset hello redis
"world"
(4)设置指定位置的字符
setrange key offeset value
下面操作将值由pest变为了best:
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"
(5)获取部分字符串
getrange key start end
start和end分别是开始和结束的偏移量,偏移量从0开始计算,例如下面操作获取了值best的前两个字符。
127.0.0.1:6379> getrange redis 0 1
"be"
表2-2是字符串类型命令的时间复杂度,开发人员可以参考此表,结合自身业务需求和数据大小选择适合的命令。
表2-2 字符串类型命令时间复杂度
命 令 时间复杂度
set key value O(1)
get key O(1)?
del key [key ...] O(k),k是键的个数
mset key value [key value ...] O(k),k是键的个数
mget key [key ...] O(k),k是键的个数
incr key O(1)
decr key O(1)
incrby key increment O(1)
decrby key decrement O(1)
incrbyfloat key increment O(1)
append key value O(1)
strlen key O(1)
setrange key offset value O(1)
getrange key start end O(n),n是字符串长度,由于获取字符串非常快,所以如果字符串不是很长,可以视同为O(1)
2.2.2 内部编码
字符串类型的内部编码有3种:
int:8个字节的长整型。
embstr:小于等于39个字节的字符串。
raw:大于39个字节的字符串。
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
整数类型示例如下:
127.0.0.1:6379> set key 8653
OK
127.0.0.1:6379> object encoding key
"int"
短字符串示例如下:
#小于等于39个字节的字符串:embstr
127.0.0.1:6379> set key "hello,world"
OK
127.0.0.1:6379> object encoding key
"embstr"
长字符串示例如下:
#大于39个字节的字符串:raw
127.0.0.1:6379> set key "one string greater than 39 byte........."
OK
127.0.0.1:6379> object encoding key
"raw"
127.0.0.1:6379> strlen key
(integer) 40
有关字符串类型的内存优化技巧将在8.3节详细介绍。
2.2.3 典型使用场景
1.?缓存功能
图2-10是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
下面伪代码模拟了图2-10的访问过程:
1)该函数用于获取用户的基础信息:
UserInfo getUserInfo(long id){
...
}
2)首先从Redis获取用户信息:
// 定义键
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秒)过期时间:
// 从MySQL获取用户信息
userInfo = mysql.get(id);
// 将userInfo序列化,并存入Redis
redis.setex(userRedisKey, 3600, serialize(userInfo));
// 返回结果
return userInfo
整个功能的伪代码如下:
UserInfo getUserInfo(long id){
userRedisKey = "user:info:" + id
value = redis.get(userRedisKey);
UserInfo userInfo;?
if (value != null) {
userInfo = deserialize(value);
} else {
userInfo = mysql.get(id);
if (userInfo != null)
redis.setex(userRedisKey, 3600, serialize(userInfo));
}
return userInfo;
}
2.?计数
许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。例如笔者所在团队的视频播放数系统就是使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1:
long incrVideoCounter(long id) {
key = "video:playCount:" + id;
return redis.incr(key);
}
实际上一个真实的计数系统要考虑的问题会很多:防作弊、按照不同维度计数,数据持久化到底层数据源等。
3.?共享Session
如图2-11所示,一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
图2-11 Session分散管理
为了解决这个问题,可以使用Redis将用户的Session进行集中管理,如图2-12所示,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
4.?限速
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,如图2-13所示。
图2-13 短信验证码限速
此功能可以使用Redis来实现,下面的伪代码给出了基本实现思路:
phoneNum = "138xxxxxxxx";
key = "shortMsg:limit:" + phoneNum;
// SET key value EX 60 NX
isExists = redis.set(key,1,"EX 60","NX");
if(isExists != null || redis.incr(key) <=5){
// 通过
}else{
// 限速
}
上述就是利用Redis实现了限速功能,例如一些网站限制一个IP地址不能在一秒钟之内访问超过n次也可以采用类似的思路。
除了上面介绍的几种使用场景,字符串还有非常多的适用场景,开发人员可以结合字符串提供的相应命令充分发挥自己的想象力。