前言
我们都知道 Redis 提供了丰富的数据类型,常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
今天我们就来详细的聊聊 Redis 这五大常见的数据类型之一 String
;
结构类型 | 结构存储的值 | 结构读写能力 |
---|---|---|
String |
可以是字符串,整数以及浮点数; | 对整个字符串或字符串的一部分进行操作; 对整数或者浮点数进行自增或者自减操作; |
应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
概述简介
String
是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。
内部实现
String
类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。
SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:
- SDS 不仅可以保存文本数据,还可以保存二进制数据。因为
SDS
使用len
属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在buf[]
数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。 - SDS 获取字符串长度的时间复杂度是 O(1) 。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用
len
属性记录了字符串长度,所以复杂度为O(1)
。 - Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
字符串对象的内部编码(encoding)有 3 种 :int、raw 和 embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用 long
类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr
属性里面(将 void*
转换成 long),并将字符串对象的编码设置为 int
。
如果字符串对象保存的是一个字符串,并且这个字符串的长度小于等于 32 字节(redis 2.+ 版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为 embstr
, embstr
编码是专门用于保存短字符串的一种优化编码方式:
如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+ 版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为 raw
:
注意,embstr
编码和 raw
编码的边界在 redis 不同版本中是不一样的:
- redis 2.+ 是 32 字节;
- redis 3.0-4.0 是 39 字节;
- redis 5.0 是 44 字节;
可以看到 embstr
和 raw
编码都会使用 SDS
来保存值,但不同之处在于 embstr
会通过一次内存分配函数来分配一块连续的内存空间来保存 redisObject
和 SDS
,而 raw
编码会通过调用两次内存分配函数来分别分配两块空间来保存 redisObject
和 SDS
。Redis这样做会有很多好处:
embstr
编码将创建字符串对象所需的内存分配次数从raw
编码的两次降低为一次;- 释放
embstr
编码的字符串对象同样只需要调用一次内存释放函数; - 因为
embstr
编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。
但是 embstr 也有缺点的:
- 如果字符串的长度增加需要重新分配内存时,整个 redisObject 和 sds 都需要重新分配空间,所以 embstr 编码的字符串对象实际上是只读的,redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序。当我们对 embstr 编码的字符串对象执行任何修改命令(例如 append)时,程序会先将对象的编码从 embstr 转换成 raw,然后再执行修改命令。
常用指令
基本操作:
# 设置 key-value 类型的值
127.0.0.1:6379> SET name sid10t
OK
# 根据 key 获得对应的 value
127.0.0.1:6379> GET name
"sid10t"
# 判断某个 key 是否存在
127.0.0.1:6379> EXISTS name
(integer) 1
# 返回 key 所储存的字符串值的长度
127.0.0.1:6379> STRLEN name
(integer) 6
# 删除某个 key 对应的值
127.0.0.1:6379> DEL name
(integer) 1
AI 代码解读
批量操作:
# 批量设置 key-value 类型的值
# mset key value [key value ...]
127.0.0.1:6379> MSET name sid10t age 18
OK
# 批量获取多个 key 对应的 value
# mget key [key ...]
127.0.0.1:6379> MGET name age
1) "sid10t"
2) "18"
AI 代码解读
计数器(字符串的内容为整数的时候可以使用):
# 设置 key-value 类型的值
127.0.0.1:6379> SET cnt 0
OK
# 将 key 中储存的数字值 +1
127.0.0.1:6379> INCR cnt
(integer) 1
# 将 key 中存储的数字值 +10
# incrby key increment
127.0.0.1:6379> INCRBY cnt 10
(integer) 11
# 将 key 中储存的数字值 -1
127.0.0.1:6379> DECR cnt
(integer) 10
# 将 key 中存储的数字值 -6
# decrby key decrement
127.0.0.1:6379> DECRBY cnt 6
(integer) 4
AI 代码解读
过期(默认为永不过期):
# 设置 key 在 30 秒后过期(该方法是针对已经存在的key设置过期时间)
# expire key seconds [NX|XX|GT|LT]
127.0.0.1:6379> EXPIRE name 30
(integer) 1
# 查看数据还有多久过期
127.0.0.1:6379> TTL name
(integer) 20
# 设置 key-value 类型的值,并设置该 key 的过期时间为 20 秒
# set key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT uni
127.0.0.1:6379> SET name sid10t EX 20
OK
# setex key seconds value
127.0.0.1:6379> SETEX name 20 sid10t
OK
AI 代码解读
不存在就插入:
# 不存在就插入(not exists)
# setnx key value
127.0.0.1:6379> SETNX name sid10t
(integer) 1
AI 代码解读
应用场景
缓存对象
使用 String
来缓存对象有两种方式:
- 直接缓存整个对象的 JSON:
SET user:1 '{"name":"sid10t", "age":18}'
。 - 采用将 key 进行分离为
user:ID:property
,采用MSET
存储,用MGET
获取各属性值:MSET user:1:name sid10t user:1:age 18 user:2:name sidiot user:2:age 20
。
常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String
数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
比如计算文章的阅读量:
# 初始化文章的阅读量
127.0.0.1:6379[3]> SET atc:rcnt:10086 0
OK
#阅读量 +1
127.0.0.1:6379[3]> INCR atc:rcnt:10086
(integer) 1
#阅读量 +1
127.0.0.1:6379[3]> INCR atc:rcnt:10086
(integer) 2
# 获取对应文章的阅读量
127.0.0.1:6379[3]> GET atc:rcnt:10086
"2"
AI 代码解读
分布式锁
SET
命令有个 NX 参数可以实现「key 不存在才插入」,可以用它来实现分布式锁:
- 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
- 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:
127.0.0.1:6379[3]> SET lock uuid NX PX 10000
OK
AI 代码解读
lock
就是 key 键;uuid
是客户端生成的唯一的标识;NX
代表只在lock
不存在时,才对lock
进行设置操作;PX 10000
表示设置lock
的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
而解锁的过程就是将 lock
键删除,但不能乱删,要保证执行该操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 uuid
是否为加锁客户端,是的话,才将 lock
键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 uuid 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
AI 代码解读
这样一来,就通过使用 SET
命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
共享 Session 信息
通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。
例如用户一的 Session 信息被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的 Session 信息,就会出现需要重复登录的问题,问题在于分布式系统每次会把请求随机分配到不同的服务器。
分布式系统单独存储 Session 流程图:
因此,我们需要借助 Redis 对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。
分布式系统使用同一个 Redis 存储 Session 流程图:
后记
Redis 五大常见数据类型之一的 String
就先讲到这里了,后续还会有其他类型的讲解呢,敬请关注!
参考资料:
- 《Redis 核心技术与实战》
- 一文回顾 Redis 五大对象 (数据类型)
- 谈谈Redis五种数据结构及真实应用场景
- Redis的数据结构与应用场景
- Redis 常见数据类型和应用场景
📝 上篇精讲: 【Redis】概述与安装
💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注;
👍 创作不易,请多多支持;
🔥 系列专栏: Redis