【Redis】String 字符串

目录

🐼Redis中的字符串是如何保存的?

🐼Redis中的字符串的常见命令

🐼string类型的内部编码

🐼string类型的典型使用场景

🐰缓存(Cache)功能

🐰计数(Counter)功能

🐰共享会话(Session)

🐰手机验证码

🐰什么是业务


下面,我们就开始进入redis中数据类型的学习了。

在学习redis数据类型的时候,我们学习的是什么?数据类型的增删查改!以及在合适场景下使用redis的数据类型来模拟MySQL的行为,能够表示MySQL的字段,因为不要忘记了,redis就是为了MySQL服务的,为MySQL负重前行!

这里必须先说明一点误区,就是我们以为我们操作redis数据类型的操作时,这些数据都保存在我们本机?No!Redis 中对所有数据类型(字符串、哈希、列表、集合、有序集合等) 的增删查改(CRUD)操作,其数据最终都持久化存储在 Redis Server 进程对应的内存 / 磁盘中(客户端仅做指令发送,不存储数据),所以并不是在我们自已的电脑内存的,而是redis-server中!Redis 设计为 "内存数据库",所有 CRUD 操作的实时数据都存储在 Redis Server 的内存中

redis中所有的key都是字符串类型,value的类型是存在差异的,是不同的数据结构,所以我们下面学习的数据类型都是针对不同的value!

🐼Redis中的字符串是如何保存的?

redis中的字符串,直接以二进制数据的形式进行存储的,有点类似于Qt中的QByteArrary。即不会做任何编码转换,你存进去的时候用的啥编码类型,取出来的时候就是啥编码类型。这点和MySQL不同。

字符串可以保存什么数据?

字符串类型的值实际可以是字符串,包含⼀般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚⾄是⼆进制流数据,例如图⽚、⾳频、视频等。不过⼀个字符串的最⼤值不能超过 512 MB,因为单线程,希望单词操作的速度尽快~

字符串类型是 Redis 最基础的数据类型,也是最关键的数据类型,有了它,尽管我们没有学好后面的hash,但是我们是不是依旧可以把key value以JSON的形式作为字符串,所以,学会了redis字符串的操作,就能完成绝大部分任务~


🐼Redis中的字符串的常见命令

以下命令都可以查询具体文档进行查找,文末附上字符串常见命令操作表:

✅SET

将 string 类型的 value 设置到 key 中。如果 key 之前存在,则覆盖,⽆论原来的数据类型是什么。之前关于此 key 的 TTL 也全部失效。

cpp 复制代码
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

我们之前对于set没有介绍选项,其实可以带选项。SET 命令⽀持多种选项来影响它的⾏为

  1. EX seconds ⸺ 使⽤秒作为单位设置 key 的过期时间。
  2. • PX milliseconds ⸺ 使⽤毫秒作为单位设置 key 的过期时间。
  3. • NX ⸺ 只在 key 不存在时才进⾏设置,即如果 key 之前已经存在,设置不执⾏。
  4. • XX ⸺ 只在 key 存在时才进⾏设置,即如果 key 之前不存在,设置不执⾏。

由于带选项的 SET 命令可以被 SETNX 、 SETEX 、 PSETEX 等命令代替,所以之后的版本中,Redis 可能进⾏合并。
如果set成功,返回OK,否则返回nil

✅GET

获取 key 对应的 value。如果 key 不存在,返回 nil。如果 value 的数据类型不是 string,会报错

✅MGET

⼀次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。

cpp 复制代码
MGET key [key ...]

✅MSET

也是一次性操作多个键值对。

cpp 复制代码
MSET key value [key value ...]

那为什么我们不能一次一次设置呢?其实之前也说过:

多次 get vs 单次 mget,使⽤ mget / mset 由于可以有效地减少了⽹络时间,所以性能相较更⾼。
学会使⽤批量操作,可以有效提⾼业务处理效率,但是要注意,每次批量操作所发送的键的数量也不是⽆节制的,否则可能造成单⼀命令执⾏时间过⻓,导致 Redis 阻塞

✅SETNX

设置 key-value 但只允许在 key 之前不存在的情况下,当然也可以在set选项设置

cpp 复制代码
SETNX key value

返回值:1 表示设置成功。0 表示没有设置

✅SETEX

给key设置过期时间,当然也可以在set选项设置

cpp 复制代码
SETEX key seconds value

单位是s

✅PSETEX

cpp 复制代码
PSETEX key milliseconds value

和SETEX类似,单位是ms,我们使用pttl来查看还有几秒过期


一些计数命令:

以下计数操作如果key不存在,都会新建一个key,并把value当为0来操作

✅INCR

将 key 对应的 string 表⽰的数字加⼀。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。

cpp 复制代码
INCR key

返回值:integer 类型的加完后的数值

✅INCRBY

将 key 对应的 string 表⽰的数字加上对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错

cpp 复制代码
INCRBY key decrement

返回值:integer 类型的加完后的数值。

✅DECR和DECRBY和INCR和INCRBY使用类似, 主要是为了接口的完整性,其实使用INCR也能完成DECR类似的效果,只不过是将数改为负数即可。

✅INCRBYFLOAT

将 key 对应的 string 表⽰的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的不是 string,或者不是⼀个浮点数,则报错。允许采⽤科学计数法表⽰浮点数。

cpp 复制代码
NCRBYFLOAT key increment

返回值:加/减完后的数值
很多存储系统和编程语⾔内部使⽤ CAS 机制实现计数功能,会有⼀定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执⾏
,并且也不会有"线程安全问题"


其他命令

✅APPEND

如果 key 已经存在并且是⼀个 string,命令会将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 SET 命令。

cpp 复制代码
APPEND KEY VALUE

O(1). 追加的字符串⼀般⻓度较短, 可以视为 O(1)
返回值:追加完成之后 string 的长度。

并且我们发现,如果追加的是中文字符,如果我们当前环境是utf-8,那么一个字符就被当成了3个字节,具体要看你当时是什么环境将字符存进去的。如果我们取出来要还原之前的字符,需要在启动客户端时带上--raw选项

✅GETRANGE

有点类似于substr

返回 key 对应的 string 的子串,由 start 和 end 确定(左闭右闭),注意这里不是左闭右开区间 。可以使⽤负数表示倒数。-1 代表倒数第⼀个字符,-2 代表倒数第⼆个,其他的与此类似。超过范围的偏移量会根据 string 的⻓度调整成正确的值。

cpp 复制代码
GETRANGE key start end

时间复杂度:O(N). N 为 [start, end] 区间的⻓度. 由于 string 通常比较短, 可以视为是 O(1)
返回值:string 类型的子串

不过需要注意,如果切分中文字符,切分不完整,可能导致结果和预期不一致~

✅SETRANGE

覆盖字符串的⼀部分,从指定的偏移开始。

cpp 复制代码
SETRANGE key offset value

返回值:替换后的 string 的⻓度。

如果当前是一个中文字符串,你再setrange时也可能会搞出问题的~

✅STRLEN

获取 key 对应的 string 的长度,单位是字节。当 key 存放的类似不是 string 时,报错

返回值:string 的长度。或者当 key 不存在时,返回 0。

最后用一张表来总结一下string中常见的命令吧:


🐼string类型的内部编码

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

int:8 个字节的长整型。embstr:小于等于 39 个字节的字符串,raw:⼤于 39 个字节的字符串

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

我们可以使用 object encoding key来查看key所对应的内部编码

✅这里有一个问题,39这样的数字怎么来的?

其实就是redis配置文件中一个数字罢了,在加载redis时就规定好了,所以,我们不需要记这样的数字,只需要理解,结合以后得业务需求进行适配


🐼string类型的典型使用场景

🐰缓存(Cache)功能

这个就是redis需要做的,redis作为热数据的缓冲,为了使得数据库压力不那么大,可以将redis理解为MySQL的反向代理,就是为了保护数据库的。而redis具有的特性,快!极大地减少了数据库的压力。

具体如何做的?如图:

该过程大概为:应用服务器查询的时候,先去redis中看看热点数据有没有自已所要的,如果有,直接返回;如果miss,那么再去存储层查询,查询完再将这个数据写回redis并且返回。

✅我们所谓的热点数据,也就是高频被访问的数据,如何定义?

这个定义方式,也是需要根据业务场景来的。比如,统计经常被访问的数据作为热点数据。或者刚刚被访问到的数据就作为热点数据。

可是一直在向redis中插入key,不会导致redis空间溢出吗?其实,我们在向redis中写入热点数据的时候,往往都会带一个EX过期时间,这样key就不会积压太多;或者如果在内存不足的时候,也会有自已的淘汰策略,这个后面会说~

✅现在我们来模拟一下上述过程,看看String类型的用武之地:

假设业务是根据用户 uid 获取用户信息,因为用户信息基本登录就会访问,如果直接访问数据库,那么效率太低了,而是增加到redis缓存中,尽量用户访问命中redis

⾸先从 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 的访问数。

✅为什么我们在构造string类型的键时,需要带上user:info?直接uid不就完了?

与 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 的性能明显下降的。


🐰计数(Counter)功能

如果我们现在想实现一个计数功能,是不是得在MySQL中创建一张计数表,然后用户每播放⼀次视频,就会去Update一次,相应的视频播放数就会自增1

上述可行,但是慢!

redis的string类型不是有自增功能吗??INCR key即可

如图:

✅redis作为计数功能,很优秀,也很快,可是会带来两个问题:

1.如果一个reids服务器挂了,那么是不是数据就没了,因为redis都是内存级别的缓存。所以我们需要考虑到单点性和数据持久化到底层数据源等

  1. redis并不擅长数据统计

比如统计前100,那么MYSQL一行sql语句就搞定了,只需要排个序limit100.但是对于redis,它保存的是键值对,没有排序功能,就很麻烦。

✅为什么企业喜欢统计?

可以根据用户的喜爱拓展丰富和修改自已的业务,进行更新迭代

✅真实的计数有这么简单吗?还要考虑同步到其他数据源通过异步方式,防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等


🐰共享会话(Session)

我们大多数应用层协议都是使用大佬们写好的HTTP协议,由于HTTP是无连接无状态的。客户端在一次请求后,是不是客户端的行为会发生点什么?比如浏览记录,点赞数,发表文章...这就有了cookie和session,服务器给客户端创建一个session,来保存客户端的状态信息。可是有那么多服务器,我们怎么保证负载均衡后的服务器是上一次给我们服务的服务器,(医院看病的例子),也就是保证他知道我上一次的session信息,能正确处理我本次请求session,是同一个。因此就需要后端服务器访问的是同一个session!所有服务器共享session

如图:

在这种模式下,只要保证 Redis 是⾼可⽤和可扩展性的,无论用户被均衡到哪台 Web 服务器上,都集中从 Redis 中查询、更新 Session 信息


🐰手机验证码

很多应用出于安全考虑,会在每次进⾏登录时,让用户输⼊⼿机号并且配合给⼿机发送验证码,然后让用户再次输⼊收到的验证码并进⾏验证,从而确定是否是用户本⼈。为了短信接⼝不会频繁访问,服务器压力不那么大,会限制用户每分钟获取验证码的频率

假设⼀分钟不能超过 5 次,验证时间300秒,伪代码:

cs 复制代码
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中。

可是具体什么是业务?业务重要吗?

业务其实就是一个公司/产品,要解决一系列过程,这个解决过程,就可以叫做业务。而技术就是为了满足业务所诞生的,所有技术都要围绕业务展开,以后在考虑问题时,要先考虑到业务,业务是具体干嘛的,再去想想这个技术行不行,根据业务来开展,做一些技术的调整。

比如12306分时段买票的例子,对于这样一个超高并发的请求,由于技术已经达到上限了,那么提升不了技术,技术解决不了的问题,就去优化业务,比如分时段放票~

相关推荐
C++业余爱好者3 小时前
Java开发中Entity、VO、DTO、Form对象详解
java·开发语言
瀚高PG实验室3 小时前
拼接符“II”在Oracle和HGDB中使用的差异
数据库·oracle·瀚高数据库
心态还需努力呀3 小时前
当时序数据不再“只是时间”:金仓数据库如何在复杂场景中拉开与 InfluxDB 的差距
数据库
宇灬宇3 小时前
oracle误drop表,通过回收站恢复
数据库·oracle
Albert Tan3 小时前
Oracle EBS 12.2/12.1 开放本地或远程访问Weblogic
数据库·oracle
一个处女座的程序猿O(∩_∩)O3 小时前
从InfluxDB到金仓:时序数据库性能拐点已至?
数据库·时序数据库
超级大只老咪3 小时前
“和”与“或”逻辑判断与条件取反(Java)
java·算法
青云交3 小时前
Java 大视界 -- 基于 Java+Flink 构建实时电商交易风控系统实战(436)
java·redis·flink·规则引擎·drools·实时风控·电商交易
程序员卷卷狗3 小时前
Java 单例模式的五种实现:饿汉式、懒汉式、DCL、静态内部类、枚举单例
java·开发语言·单例模式