Redis string类型&&hash类型

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 中⽐较分散,缺少内聚性,所以这种⽅案基本没有实⽤性。

相关推荐
Dlwyz2 小时前
redis-击穿、穿透、雪崩
数据库·redis·缓存
Theodore_10223 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
工业甲酰苯胺4 小时前
Redis性能优化的18招
数据库·redis·性能优化
冰帝海岸4 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象4 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了5 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
jokerest1235 小时前
web——sqliabs靶场——第十三关——报错注入+布尔盲注
mybatis
小二·5 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王5 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq