图解Redis 02 | String数据类型的原理及应用场景

介绍

在 Redis 中,String 是一种重要的数据类型,是最基本的 key-value 结构,在这个结构中, value 是一个字符串。value 所能容纳的数据最大长度为512M

需要注意的是,这里的字符串不只指文本数据,它还可以是数字、JSON 对象、二进制数据等。

内部实现

String 类型的底层数据结构在 Redis 中主要由整数(int)和 SDS(Simple Dynamic String)实现。

SDS 与我们熟知的 C 语言字符串有所不同。Redis 选择 SDS 而非 C 语言的字符串实现,主要是因为 SDS 具有以下特点:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。这是因为 SDS 使用 len 属性来判断字符串的结束位置,而不是依靠空字符 (\0) 标记结束。同时,SDS 的所有 API 都会将 buf[] 数组中的数据作为二进制来处理。因此,SDS 不仅可以存储文本,还能存储图片、音频、视频、压缩文件等二进制数据。

  • SDS获取字符串长度的时间复杂度为O(1)。由于C语言的字符串没有记录自身的长度,所以获取长度的复杂度为O(n);而SDS结构体中的len属性记录了字符串长度,所以复杂度为O(1)。

  • Redis 的 SDS API 是安全的,拼接字符串不会导致缓冲区溢出。因为 SDS 会在拼接字符串前检查空间是否足够,如果空间不足,会自动扩展,避免缓冲区溢出的问题。

在 Redis 中,字符串对象的内部编码(encoding)有三种:int、raw 和 embstr,关系如下:

如果字符串对象存储的是一个整数值,并且这个整数值可以用 long 类型表示,那么该字符串对象会将这个整数值存储在RedisObject的 ptr 属性中(将 void* 转换为 long),并将其编码设置为 int。

注意:redisObject是Redis中用于表示数据结构(如字符串、列表、集合、哈希表和群体集合)的一个核心数据结构。通俗来讲,可以把它redisObject想象成一个“包装器”或“容器”,用于封装和管理存储在Redis内存中的数据。后面会有文章详细介绍。

如果字符串对象存储的是一个长度不超过 32 字节的字符串(在 Redis 3.2 版本及以后改成44字节),Redis 会使用简单动态字符串(SDS)来存储这个字符串,并将编码设置为 embstr。

embstr 编码是一种专门为存储短字符串而优化的编码方式,这种编码方式将字符串数据直接嵌入到 redisObject 结构体内部,redisObject 和 SDS 数据都是在同一块内存区域内,这意味着 Redis 只需分配一次内存空间,从而提高了内存管理效率和操作性能。

如果字符串对象存储的是一个长度超过 32 字节的字符串(在 Redis 3.2 版本及以后改成44字节),Redis 会使用简单动态字符串(SDS)来存储这个字符串,并将编码设置为 raw。raw 编码需要分配两次内存空间(分别为redisObject和sds)。

注意,不同版本的 Redis 中,embstr 编码与 raw 编码的界限长度有所不同:

  • 在 Redis 2.x 版本中,界限为 32 字节。
  • 在 Redis 3.0 到 4.0 版本中,界限为 39 字节。
  • 在 Redis 5.0 版本中,界限为 44 字节。

这里再总结一下,embstr 编码和 raw 编码都使用 SDS来保存字符串值,但它们在内存分配和数据存储方式上有所不同:

1. 内存分配
  • “raw”编码:需要进行两次内存分配,分别为Redis对象(“redisObject”)和SDS(Simple Dynamic String)分配内存空间。
  • “embstr”编码:只进行一次内存分配,同时为Redis对象和SDS分配连续的内存空间。
2. 数据存储
  • “raw”编码:Redis对象和SDS分别存储在不同的内存区域。
  • “embstr”编码:Redis对象和SDS存储在连续的内存区域中。
3. 修改操作
  • “embstr”编码的字符串是只读的,一旦需要修改字符串,就会转换为“raw”编码。
  • 可以直接修改“raw”编码的字符串。
4. 性能影响
  • “embstr“ 编码:在创建和释放内存时的开销较小,因为只需要进行一次内存操作。
  • “raw“ 编码:在修改字符串时性能更好,因为不需要进行编码转换。

选择使用哪种编码取决于具体的应用场景和需求。如果字符串长度较短且不经常修改,embstr 编码可能更为适合;而如果字符串较长或需要频繁修改,则 raw 编码可能是更好的选择。

常用命令

基本操作
# 设置value
 > SET name Sea 
OK 
# 根据key获取对应的value。
 > GET name 
"Sea" 
# 判断某个key是否存在。
 > EXISTS name 
( integer ) 1 
# 返回key存储的字符串值的长度。
 > STRLEN name 
( integer ) 3 
# 删除某个key对应的value。
 > DEL name 
( integer ) 1

#不存在则设置
>SETNX key value
(integer) 1
批量操作
# 批量设置key-value类型的值
> MSET k1 v1 k2 v2 k3 v3 
OK 
# 批量获取多个key对应的值
> MGET k1 k2 
1) "v1"
2) "v2"
计数器操作(当字符串内容为整数时可使用)
# 设置键值类型的值。
 > SET number 0 
OK 
# 将键中存储的数字值增加一。
 > INCR number 
( integer ) 1 
# 将键中存储的数字值加 100。
 > INCRBY number 100 
( integer ) 101 
# 将键中存储的数字值减少一。
 > DECR number 
( integer ) 100 
# 将键中存储的数字值减少 50。
 > DECRBY number 50 
( integer ) 50
# 设置key的 过期时间为30秒(此命令是 为已经存在的key设置 过期时间)
> EXPIRE name 30 
(integer) 1
# # 检查数据还有多长时间过期
> TTL name 
(integer) 23
# 设置key- value的同时设置过期时间
> SET key value EX 30
OK
> SETEX key 30 value
OK

应用场景

1. 统计计数

由于 Redis 是单线程处理命令的,执行命令的过程是原子的,因此 String 数据类型非常适合用于计数场景,如计算访问次数、点赞次数等。等等。

例如,我们可以使用 Redis 的 INCR 命令来计算一篇文章的访问次数:每当用户访问这篇文章时,我们就调用 INCR 命令增加该文章的访问次数。由于 Redis 的 INCR 命令是原子的,这保证了每次访问都会正确地更新计数值,而不会出现并发冲突的问题。

# 初始化文章访问次数为  0
> SET aritcle:visit:count:1000001 0 
OK 
# 访问量 +1
 > INCR aritcle:readcount:1000001 
( integer ) 1 
# 访问量 +1
 > INCR aritcle:readcount:1000001 
( integer ) 2 
# 获取相应文章的访问量
> GET aritcle:readcount:1000001 
"2"

2. 缓存对象

使用 String 缓存对象的常见方式有两种:

1. 直接缓存整个对象的 JSON

将对象转换为 JSON 字符串,然后将这个 JSON 字符串作为值存储在 Redis 的 String 类型中。这种方式简单直观,但当需要修改对象的某个字段时,就必须将整个 JSON 字符串取出,进行反序列化,修改字段后再重新序列化存储。

这可能会导致较大的性能开销,因为每次修改都涉及到整个对象的序列化和反序列化过程。

SET user:1 '{"name":"Bob", "age":18}'
2. 将对象的属性分开存储

将对象的各个属性分别存储在不同的 key 中,每个 key 对应一个属性的值。这种方式提供了更大的灵活性,允许你单独访问和修改对象的某个属性,而不需要处理整个对象。然而,这也意味着需要对多个 key 进行操作,从而可能增加操作的复杂性和开销。

MSET user:1:name Bob user:1:age 18 user:2:name Jerry user:2:age 20

3. 分布式锁

在 Redis 中,可以使用 String 数据结构来实现分布式锁。利用 SET 命令的 NX 参数,可以实现“仅当 key 不存在时才插入”的功能,从而用来实现分布式锁:

  • 如果 key 不存在,Redis 会插入 key 并返回成功,这表示锁获取成功。
  • 如果 key 已经存在,Redis 会插入失败,表示锁获取失败。

通常,为了确保锁不会因异常情况而长时间占用,还会给分布式锁设置一个过期时间。分布式锁的命令如下:

SET lock_key unique_value NX PX 10000
  • lock_key 是锁对应的key;
  • unique_value 是客户端生成的唯一标识,用于标识锁的拥有者;
  • NX 表示仅在 lock_key 不存在时才设置;
  • PX 10000 表示将 lock_key 的过期时间设置为 10 秒,以防客户端异常无法释放锁。

在解锁时,操作的步骤是删除 lock_key 键。但是,为了确保只有持有锁的客户端才能删除 lock_key,需要先验证 unique_value 是否匹配。如果匹配,则可以删除该键。

由于解锁过程涉及两个操作(验证和删除),需要确保这两个操作是原子的。这可以通过使用 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节点上分布式锁的加锁与解锁。

4. 共享会话信息

通常我们在开发登录模块的时候,都会使用Session来保存用户的登录状态,这些Session信息会保存在服务器端,但是这只适用于单系统应用。

在分布式系统中,这种模式就不再适用了。由于用户的请求可能会被分配到不同的服务器上,这会导致 Session 信息丢失,导致用户需要频繁重新登录。

例如,假设用户1第一次的登录请求是落到服务器A上,因此其 Session 信息保存在服务器A上。但是当用户1第二次访问的时候,可能会被分配到了服务器B上,而服务器B没有用户1的Session信息,那么就会提示用户1需要登录,这样的用户体验是极度不友好的。

为了解决这个问题,通常需要采用集中式的 Session 存储方案,例如使用 Redis 来存储 Session 信息,这样无论用户的请求被分配到哪台服务器,服务器都会去同一个Redis中获取相关的Session信息,样就解决了分布式系统中Session存储的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值