介绍

String(字符串)是最基本值类型,可以是字符串、整数、浮点数,最多可以容纳的数据长度是 512M

内部实现

String 类型的底层数据结构实现主要是 int 和 SDS(简单动态字符串)

字符串对象的内部编码有 3 种 :int、embstr、raw

int

如果字符串对象保存的是整数值,并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里(将 void* 转成 long),并将字符串对象的编码设置为 int

embstr

如果字符串对象保存的是一个字符串,并且这个字符串的长度小于等于某个临界值,那么字符串对象将使用一个 SDS 来保存这个字符串(通过一次内存分配函数来分配一块连续的内存空间来保存 redisObjectSDS),并将对象的编码设置为 embstr

raw

如果字符串对象保存的是一个字符串,并且这个字符串的长度大于某个临界值,那么字符串对象将使用一个 SDS 来保存这个字符串(调用两次内存分配函数来分别分配两块空间来保存 redisObjectSDS),并将对象的编码设置为 raw

embstr 编码和 raw 编码的临界值在 redis 不同版本中不一样:

  • Redis 2.+:32 字节
  • Redis 3.0-4.0:39 字节
  • Redis 5.0:44 字节

embstr 编码是专门用于保存短字符串的一种优化编码方式:

  • 内存分配次数从两次降低为一次
  • 释放也只需要一次
  • embstr 编码的字符串对象的所有数据都保存在一块连续的内存中,可以更好的利用 CPU 缓存提升性能

但是 embstr 也有缺点:

  • 如果字符串的长度增加需要重新分配内存时,整个 redisObjectSDS 都需要重新分配空间,所以 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 来缓存对象有两种方式:

  1. 直接缓存整个对象的 JSON
SET user:1 '{"name":"xiaolin", "age":18}'
  1. key 分离为 user:ID:属性,用 MSET 存储,用 MGET 获取各属性值
MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20

Hash 也可以存储对象

常规计数

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

# 初始化文章的阅读量
> SET aritcle:readcount:1001 0
OK
 
# 阅读量+1
> INCR aritcle:readcount:1001
(integer) 1
 
# 阅读量+1
> INCR aritcle:readcount:1001
(integer) 2
 
# 获取对应文章的阅读量
> GET aritcle:readcount:1001
"2"

分布式 Session

分布式 Session 有两种常见的解决方案:

  1. 网关始终将相同来源的请求分配到同一个服务器,而不是随机分配到不同的服务器
  2. 将 Session 统一存储,如:使用 Redis

分布式锁

SET 命令的 NX 参数可以实现「 key 不存在才插入」,用它来实现分布式锁:

  • 如果 key 不存在,返回插入成功,用来表示加锁成功
  • 如果 key 存在,返回插入失败,用来表示加锁失败

一般而言,还会对分布式锁加上过期时间

加锁:

SET lock_key unique_value NX PX 10000
  • lock_keykey
  • unique_value 是客户端生成的唯一标识
  • NX 指示加锁成功或失败
  • 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