string类型
类型介绍
在Redis中的所有的key都是string类型,而value的类型有多种。
Redis中的字符串是直接按照二进制的方式进行存储的,也就是不会做任何的编码转换,存的是什么,取出来的就是什么。这样一般来说,Redis遇见乱码的可能性会变小。因为这里会出现乱码就只跟存入端和读取端有关,而像mysql这样自己会做编码转换的,那么出现的乱码的原因还可能跟mysql自身的编码转换有关。
另外因为Redis存储字符串是按二进制存储的,所以不仅仅只可以存文本数据,
甚至还可以存图片,音频。
set/get
再看一下set的语法:
bash
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
对于后面的选项语法格式:
[expiration EX seconds|PX milliseconds] 也就是在设置key的时候,同时设置超时时间,EX就是以秒级单位的,PX就是毫秒。
[NX|XX] NX:表示 如果key存在,那么就不设置 ,返回nil;如果不存在,那么才设置。
XX:表示 key存在,才设置(相当于更新key);如果key不存在,那么就不设置。
这样的额外选项可以减少网络轮次,在客户端-服务器这样的程序下还是很有用的。 并且对于一条命令来说,它的执行效果是原子的,而分开的话就不好说了。
用命令
bash
flushall
可以进行删库操作,将所有的数据全部删除。这个操作非常危险,几乎不会用到。
mset/mget
mset:⼀次性设置多个 key 的值。
bash
MSET key value [key value ...]
mget:⼀次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。
bash
MGET key [key ...]
这里的时间复杂度官方给的是:O(N),不过这里的N跟我们之前说的一样,指的是key的数目。
这两个命令同样是为了减少请求的网络轮次的。
setnx/setex/psetex
这三个命令其实就是对set的一些常见用法进行了缩写。
incr/incrby/decr/decrby/incrbyfloat
incr只能针对一个整数使用,返回这个数加一后的值。相当于 ++i,其他的命令同理。
incrby可以针对value进行 + n的操作。并且这里的n可以是负数,如果是负数就相当于减法操作。
另外在Redis中的int是64位的。
为什么Redis明明可以使用一套命令就可以实现加减法,为什么要设计两套呢?这一点跟刚刚的setnx那些命令是一样的,是为了让Redis的使用更加符合人的直觉,从而降低用户的使用门槛。
另外,如果我们 incr/incrby的key不存在时,它会帮我们插入一个value为0的值,然后进行对应的加减操作。
剩下三个命令的使用原理跟incr和incrby差不多。这些命令的时间复杂度都是O(1)。
另外这个 incrbyfloat 中的float其实是按double的标准来的。
append
如果 key 已经存在并且是⼀个 string,命令会将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 SET 命令。
bash
APPEND KEY VALUE
时间复杂度:O(1). 追加的字符串⼀般⻓度较短, 可以视为 O(1).
返回值:追加完成之后 string 的字节大小。
注意:append这里的返回值 的单位是字节。
如果我们想让Redis客户端能够自动的把二进制数据尝试翻译,那么在启动客户端的时候要加上一个 --raw的选项
可以看到,当我们启动时没有带 --raw时,我们查出来的汉字是十六进制显示的,其中每个十六进制前的 \x 是转义字符,表示这是一个十六进制。
getrange
返回 key 对应的 string 的⼦串,由 start 和 end 确定(左闭右闭)。可以使⽤负数表⽰倒数。-1 代表倒数第⼀个字符,-2 代表倒数第⼆个,其他的与此类似。超过范围的偏移量会根据 string 的⻓度调整成正确的值。
bash
GETRANGE key start end
在C++和Java中,谈到一个区间大多都是左闭右开的 [x,y) 。而Redis这里都是闭区间 [x,y]
时间复杂度:O(N). N 为 [start, end] 区间的⻓度. 由于 string 通常⽐较短, 可以视为是 O(1)
返回值:string 类型的⼦串 (没有就返回空串)
注意:当我们存的值是汉字时,因为一个汉字占3个字节,所以使用这个切分的时候还需要考虑到这一点,不然切出来的可能是乱码
setrange
覆盖字符串的⼀部分,从指定的偏移开始。 如果从指定偏移位置覆盖的字符串超出了原本能覆盖的大小,那么多余的字符串会以追加的形式覆盖。
bash
SETRANGE key offset value
时间复杂度:O(N), N 为 value 的⻓度. 由于⼀般给的 value ⽐较短, 通常视为 O(1).
返回值:替换后的 string 的⻓度。
注意:当value是一个中文字符串的时候,进行setrange是有可能出问题的。
另外,setrange对于不存在的key也是可以操作的, 不过会把 offset(偏移位置)之前的内容填充成:0x00。
strlen
获取 key 对应的 string 的⻓度。当 key 存放的类似不是 string 时,报错。
bash
STRLEN key
时间复杂度:O(1)
返回值:string 的⻓度,单位依旧是字节。或者当 key 不存在时,返回 0。
string编码方式
int:8 个字节的⻓整型。
•
embstr:⼩于等于 39 个字节的字符串。
•
raw:⼤于 39 个字节的字符串。
Redis 会根据当前值的类型和⻓度动态决定使⽤哪种内部编码实现。
不过不建议记住这样的数字,记住数字是没有意义的。
根据业务场景的不同,最佳的长度不一定是39字节,此时就需要自行修改这个数字的大小
关于修改:
另外,我们发现Redis存储整形会用int类型(8字节大小),但是存储小数其实是用embstr的,也就是说存储小数本质还是当作字符串来存储的。
这样的差别其实是很大的,因为对于整形 进行算术运算的时候是比较方便的,但是如果是字符串存储,那么进行算术运算的时候,要先将字符串转化为小数,然后再进行算术运算,最后还要再转回字符串,开销就大多了。
string的应用场景
作为缓存
作为缓存的思路:应用服务器访问数据时,先查询Redis,如果Redis上有这个数据,那么就直接返回给应用服务器,如果没有,那么就去MySQL中去查找,在MySQL中找到后再返回给应用服务器,同时会把这个数据写入到Redis中,这个写入到Redis中的值,作为key往往被设置了过期时间
Redis这样的缓存经常存储热点数据,也就是高频被访问的数据。这样能够加速查询和缓解MySQL服务器的压力。
伪代码模拟业务数据访问过程:
1.根据用户uid获取用户信息:
cpp
UserInfo getUserInfo(long uid) {
...
}
2.⾸先从 Redis 获取⽤⼾信息,我们假设⽤⼾信息保存在 "user:info:<uid>" 对应的键中:
cpp
// 根据 uid 得到 Redis 的键
String key = "user:info:" + uid;
// 尝试从 Redis 中获取对应的值
String value = Redis 执⾏命令:get key;
// 如果缓存命中(hit)
if (value != null) {
// 假设我们的⽤⼾信息按照 JSON 格式存储
UserInfo userInfo = JSON 反序列化(value);
return userInfo;
}
如果没有从 Redis 中得到⽤⼾信息,及缓存 miss,则进⼀步从 MySQL 中获取对应的信息,随后写⼊缓存并返回:
cpp
// 如果缓存未命中(miss)
if (value == null) {
// 从数据库中,根据 uid 获取⽤⼾信息
UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid =
<uid>
// 如果表中没有 uid 对应的⽤⼾信息
if (userInfo == null) {
响应 404
return null;
}
// 将⽤⼾信息序列化成 JSON 格式
String value = JSON 序列化(userInfo);
// 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
Redis 执⾏命令:set key value ex 3600
// 返回⽤⼾信息
return userInfo;
}
通过增加缓存功能,在理想情况下,每个⽤⼾信息,⼀个⼩时期间只会有⼀次 MySQL 查询,极⼤地提升了查询效率,也降低了 MySQL 的访问数。
注意:
与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,⽽且也没有对键名
有强制要求(除了不能使⽤⼀些特殊字符)。但设计合理的键名,有利于防⽌键冲突和项⽬
的可维护性,⽐较推荐的⽅式是使⽤ "业务名:对象名:唯⼀标识:属性" 作为键名。例如
MySQL 的数据库名为 vs,⽤⼾表名为 user_info,那么对应的键可以使⽤
"vs:user_info:6379"、"vs:user_info:6379:name" 来表⽰,如果当前 Redis 只会被⼀个业务
使⽤,可以省略业务名 "vs:"。如果键名过程,则可以使⽤团队内部都认同的缩写替代,例如
"user:6379:friends:messages:5217" 可以被 "u:6379:fr:m:5217" 代替。毕竟键名过⻓,还
是会导致 Redis 的性能明显下降的。
计数功能
许多应⽤都会使⽤ Redis 作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,同时数
据可以异步处理或者落地到其他数据源。
Redis将播放量同步到其他数据源的方式是异步的。
不过Redis用作计数功能是挺不错的,但是让Redis统计数据就不行了,比如让Redis统计播放量前百的视频就很麻烦,如果让MySQL来的话一行sql就搞定了 (排序加limit 100)
实际中要开发⼀个成熟、稳定的真实计数系统,要⾯临的挑战远不⽌如此简单:防作弊、按
照不同维度计数、避免单点问题、数据持久化到底层数据源等。
比如要防作弊:一个用户对某一个视频播放量的贡献就有几百上千,这明显不合理。
不同的维度:比如视频的完播率,点赞量,转发量,评论数等
共享会话
⼀个分布式 Web 服务将⽤⼾的 Session 信息(例如⽤⼾登录信息)保存在各⾃的服务器中,但这样会造成⼀个问题:出于负载均衡的考虑,分布式服务会将⽤⼾的访问请求均衡到不同的服务器上,并且通常⽆法保证⽤⼾每次请求都会被均衡到同⼀台服务器上,这样当⽤⼾刷新⼀次访问是可能会发现需要重新登录,这个问题是⽤⼾⽆法容忍的。
为了解决这个问题,可以使⽤ Redis 将⽤⼾的 Session 信息进⾏集中管理,如图 2-13 所⽰,在这种模式下,只要保证 Redis 是⾼可⽤和可扩展性的,⽆论⽤⼾被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息。
手机验证码
很多应⽤出于安全考虑,会在每次进⾏登录时,让⽤⼾输⼊⼿机号并且配合给⼿机发送验证码,
然后让⽤⼾再次输⼊收到的验证码并进⾏验证,从⽽确定是否是⽤⼾本⼈。为了短信接⼝不会频繁访问,会限制⽤⼾每分钟获取验证码的频率,例如⼀分钟不能超过 5 次。
伪代码实现思路:
cpp
String 发送验证码(phoneNumber) {
key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟(60 秒)
// 使⽤ NX,只在不存在 key 时才能设置成功
bool r = Redis 执⾏命令:set key 1 ex 60 nx
if (r == false) {
// 说明之前设置过该⼿机的验证码了
long c = Redis 执⾏命令:incr key
if (c > 5) {
// 说明超过了⼀分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过⼿机的验证码;要么次数没有超过 5 次
String validationCode = ⽣成随机的 6 位数的验证码();
validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟(300 秒)内有效
Redis 执⾏命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过⼿机短信发送给⽤⼾
return validationCode ;
}
// 验证⽤⼾输⼊的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
validationKey = "validation:" + phoneNumber;
String value = Redis 执⾏命令:get validationKey;
if (value == null) {
// 说明没有这个⼿机的验证码记录,验证失败
return false;
}
if (value == validationCode) {
return true;
} else {
return false;
}
}
以上介绍了使⽤ Redis 的字符串数据类型可以使⽤的⼏个场景,但其适⽤场景远不⽌于此,开发
⼈员可以结合字符串类型的特点以及提供的命令,充分发挥⾃⼰的想象⼒,在⾃⼰的业务中去找到合适的场景去使⽤ Redis 的字符串类型。
hash类型
类型介绍
⼏乎所有的主流编程语⾔都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数
组、映射。在 Redis 中,哈希类型是指值本⾝⼜是⼀个键值对结构,形如 key = "key",value = { {
field1, value1 }, ..., {fieldN, valueN } }。
注意:
哈希类型中的映射关系通常称为 field-value,⽤于区分 Redis 整体的键值对(key-value),
注意这⾥的 value 是指 field 对应的值,不是键(key)对应的值,请注意 value 在不同上下
⽂的作⽤。
基础命令 1:hset/hget/hexists/hdel
hset
设置 hash 中指定的字段(field)的值(value)。
bash
HSET key field value [field value ...]
时间复杂度:插⼊⼀组 field 为 O(1), 插⼊ N 组 field 为 O(N)
返回值:添加的字段的个数。
hget
获取 hash 中指定字段的值。
bash
HGET key field
时间复杂度:O(1)
返回值:字段对应的值或者 nil。
hexists
判断 hash 中是否有指定的字段。
bash
HEXISTS key field
时间复杂度:O(1)
返回值:1 表⽰存在,0 表⽰不存在。
hdel
删除 hash 中指定的字段。
bash
HDEL key field [field ...]
时间复杂度:删除⼀个元素为 O(1). 删除 N 个元素为 O(N).
返回值:本次操作删除的字段个数。
如果要删除所有字段,那么就是
bash
del key
基础命令2:hkeys / hvals / hgetall
hkeys
获取 hash 中的所有字段。
bash
HKEYS key
时间复杂度:O(N), N 为 field 的个数.
返回值:字段列表。也就是这个key对应的所有的field。
hvals
获取 hash 中的所有的值。
bash
HVALS key
时间复杂度:O(N), N 为 field 的个数.
返回值:所有的值。与hkeys相对,这里返回的是这个key对应的所有的value。
hgetall
获取 hash 中的所有字段以及对应的值。
bash
HGETALL key
时间复杂度:O(N), N 为 field 的个数.
返回值:字段和对应的值。
可以看到,查询出来的结果 是一对 field和value 的形式规律显示的。
上述的操作都是风险比较大的,数据量多大时就可能会导致Redis阻塞。
基础命令3:hmget / hlen / hsetnx / hincrby
hmget
⼀次获取 hash 中多个字段的值
bash
HMGET key field [field ...]
时间复杂度:只查询⼀个元素为 O(1), 查询多个元素为 O(N), N 为查询元素个数.
返回值:字段对应的值或者 nil。
对于hmget命令,同样也还有一个hmset命令,可以同时设置多个 filed value,但是没必要,因为hset本身就已经有这个功能了。
hlen
获取 hash 中的所有字段的个数。
bash
HLEN key
时间复杂度:O(1)
返回值:字段个数。
hsetnx
在字段不存在的情况下,设置 hash 中的字段和值。
bash
HSETNX key field value
时间复杂度:O(1)
返回值:1 表⽰设置成功,0 表⽰失败。
hincrby
将 hash 中字段对应的数值添加指定的值。
bash
HINCRBY key field increment
时间复杂度:O(1)
返回值:该字段变化之后的值。
hincrbyfloat
HINCRBY 的浮点数版本。
bash
HINCRBYFLOAT key field increment
时间复杂度:O(1)
返回值:该字段变化之后的值。
使用示例:
hash的编码方式
哈希的内部编码有两种:
•
ziplist(压缩列表):当哈希类型元素个数⼩于 hash-max-ziplist-entries 配置(默认 512 个)、
同时所有值都⼩于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使⽤ ziplist 作为哈
希的内部实现,ziplist 使⽤更加紧凑的结构实现多个元素的连续存储,所以在节省内存⽅⾯⽐
hashtable 更加优秀。
•
hashtable(哈希表):当哈希类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ hashtable 作为哈希
的内部实现,因为此时 ziplist 的读写效率会下降,⽽ hashtable 的读写时间复杂度为 O(1)。
下⾯的⽰例演⽰了哈希类型的内部编码,以及响应的变化。
压缩的本质就是对数据进行重新编码,不同的数据有不同的特点,结合这些特点,进行精妙的设计,重新编码之后,就可以缩小体积。
示例:
哈希类型的应用场景
作为缓存
之前说过,string类型也可以作为缓存,哈希类型也可以作为缓存。
当存储结构化的数据时,使用hash作为缓存就更合适一些。
比如:
如果用string来存储这样的具有表结构的数据的话,那么就得把数据转化为json的格式。
但是如果我们只想要操作某一对filed value,那么就需要将整个key (json)读取出来,然后解析成对象,然后对其进行修改,修改完之后,又需要将字符串重新转为json,再写回去。
但是如果此时是用hash存储的数据,那么就可以很方便的取出和修改数据了。
总结:使用hash的方式确实可以高效的对filed进行读写,但是hash也付出了更大的空间上的代价
其实还有一种缓存方式,那就是用原生字符串类型。每一个属性用一个键(key)
但是这种方式是不推荐的,因为它把同一个数据的各个属性给分散开了,也就是低内聚。这里不要把内聚和耦合混淆了。
一般写代码追求的是:高内聚,低耦合。
但是需要注意的是哈希类型和关系型数据库有两点不同之处:
•
哈希类型是稀疏的,⽽关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,⽽
关系型数据库⼀旦添加新的列,所有⾏都要为其设置值,即使为 null,如图 所⽰。
•
关系数据库可以做复杂的关系查询,⽽ Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等
基本不可能,维护成本⾼。
还有一个问题就是:
缓存方式对比
string类型缓存
序列化字符串类型,例如JSON格式
优点:针对总是以整体作为操作的信息⽐较合适,编程也简单。同时,如果序列化⽅案选择合适,内存的使⽤效率很⾼。
缺点:本⾝序列化和反序列需要⼀定开销,同时如果总是操作个别属性则⾮常不灵活。
哈希类型缓存
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较⼤消耗。
原生字符串类型
使⽤字符串类型,每个属性⼀个键。
优点:实现简单,针对个别属性变更也很灵活。
缺点:占⽤过多的键,内存占⽤量较⼤,同时⽤⼾信息在 Redis 中⽐较分散,缺少内聚性,所以这种⽅案基本没有实⽤性。