String
介绍
String 是最基本的key-value结构,key是唯一标识,value是具体的值,value其实不仅是字符串,也可以是数字(整数或浮点数),value最多可以容纳的数据长度是 512M
。
内部实现
String 类型的底层的数据结构实现主要是int 和 SDS(简单动态字符串)。
SDS和我们认识的C字符串不太一样,之所以没有使用C语言的字符串表示,因此SDS相比于C的原生字符串:
- 原生字符串不仅可以保存文本数字,还可以保存二进制数据。因为SDS使用len属性的值而不是空字符串来判断字符串是否结束,并且SDS的所有API都会以处理二进制的方式来处理SDS存放在buf[]数组里的数据,所以SDS不光能存放文本的数据,而且能保存图片、音频、视频、压缩文件这种二进制文件数据。
- SDS获取字符串长度的时间复杂度是O(1)。因为C语言的字符串并不记录长度,所以获取长度的复杂度为O(n),而SDS结构用len属性记录了字符串长度,所以复杂度为O(1)
- Redis的SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为SDS在拼接字符串中去会检查SDS空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题
字符串对象的内部编码有3种:int、raw和embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr
属性里面(将void*
转换为long),并将字符串对象的编码设置为int
。
如果一个字符串对象保存的是一个字符串,并且这个字符串的长度小于等于32字节(redis2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr
,ebstr
编码是专门用于保存段字符串的一种优化编码方式
如果字符串对象保存的是一个字符串,并且这个这个换长度大于32字节(redis2.+版本),那么字符串对象将使用一个简单动态字符串(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都需要重新分配空间,所以ebstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转成raw,然后执行修改命令
常用指令
普通字符串的基本操作:
# 设置 key-value 类型的值
> SET name lin
OK
# 根据 key 获得对应的 value
> GET name
"lin"
# 判断某个 key 是否存在
> EXISTS name
(integer) 1
# 返回 key 所储存的字符串值的长度
> STRLEN name
(integer) 3
# 删除某个 key 对应的值
> DEL name
(integer) 1
批量设置 :
# 批量设置 key-value 类型的值
> MSET key1 value1 key2 value2
OK
# 批量获取多个 key 对应的 value
> MGET key1 key2
1) "value1"
2) "value2"
计数器(字符串的内容为整数的时候可以使用):
# 设置 key-value 类型的值
> SET number 0
OK
# 将 key 中储存的数字值增一
> INCR number
(integer) 1
# 将key中存储的数字值加 10
> INCRBY number 10
(integer) 11
# 将 key 中储存的数字值减一
> DECR number
(integer) 10
# 将key中存储的数字值键 10
> DECRBY number 10
(integer) 0
过期(默认为永不过期):
# 设置 key 在 60 秒后过期(该方法是针对已经存在的key设置过期时间)
> EXPIRE name 60
(integer) 1
# 查看数据还有多久过期
> TTL name
(integer) 51
#设置 key-value 类型的值,并设置该key的过期时间为 60 秒
> SET key value EX 60
OK
> SETEX key 60 value
OK
不存在就插入:
# 不存在就插入(not exists)
>SETNX key value
(integer) 1
应用场景
缓存对象
使用 String 来缓存对象有两种方式:
- 直接缓存整个对象的 JSON,命令例子:
SET user:1 '{"name":"xiaolin", "age":18}'
。 - 采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值,命令例子:
MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20
。
常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
比如计算文章的阅读量:
# 初始化文章的阅读量
> SET aritcle:readcount:1001 0
OK
#阅读量+1
> INCR aritcle:readcount:1001
(integer) 1
#阅读量+1
> INCR aritcle:readcount:1001
(integer) 2
#阅读量+1
> INCR aritcle:readcount:1001
(integer) 3
# 获取对应文章的阅读量
> GET aritcle:readcount:1001
"3"
分布式锁
SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:
- 如果key不存在,则显示插入成功,可以用来表示加锁成功
- 如果key存在,则会显示插入失败,可以用来表示加锁失败
一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下
SET lock_key unique_value NX PX 10000
- lock_key 就是key键
- unique_value 是客户端生成的唯一标识
- NX 标识只在lock_key不存在时,才对lock_key进行设置操作
- PX 10000表示设置lock_key的过期时间为10s,这是为了避免客户端发生异常释放锁
而解锁的过程就是将lock_key删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,加锁的时候,我们要先判断锁的unique_value是否为加锁客户端,是的话,才可以将lock_key删除
可以看到,解锁有两个操作,这是就需要lua脚本来保证解锁的原子性,因为Redis在执行Lua脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这样一来,就通过使用SET命令和Lua脚本在redis单节点上完成了分布式锁的加锁和解锁
共享Session信息
通常我们在开发后台管理系统是,会使用Session来保存用户的登录状态,这些Session信息回保存在服务端,单这只使用与单系统应用,如果是分布式系统此模式将不再适用。
例如用户的一的Session被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的信息,就会出现需要重复登录的情况,问题在于分布式系统每次会把请求随机分配到不同的服务器。
因此,我们需要借助Redis对这些Session信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个Redis获取相关的Session信息,这样就解决了分布式系统下Session存储的问题。