Redis 常见数据类型-String 类型

Redis 常见数据类型-String 类型

七月 07, 2025 次阅读

类型简介

不同于 cpp 里面的 String,Redis 中的 String 更像是一种比特数组,也就是说,Redis 中的 String 是直接将用户输入的数据按照比特的方式存放到数组中的,
在 Redis 中,这些比特的编码方式不会像 MySQL 那样存在所谓的编码转化,用户在写入数据时使用的是哪种编码方式,在 Redis 中同样也会采用对应的编码方式,从而
避免了频繁的编码方式转化所带来的不必要的开销

相关指令

针对 String, 有着专门的设置和查询指令,即 set 和 get,首先我们都知道,Redis 中的 key 都是 String 类型的,而通过 set 命令可以设置一个 key 所对应的 String 类型
的 value 值,通过 get 也可以查询到 String 类型的 value,因此,若通过 get 命令查询一个类型不是 String 的value,就会报错,如下所示:

127.0.0.1:6379> lpush k2 11 22 33
(integer) 3
127.0.0.1:6379> get k2
(error) WRONGTYPE Operation against a key holding the wrong kind of value

当然,除此之外,还有一些专门针对 String 类型的便捷指令:

1、setex 指令

用法:

setex key seconds value

该指令其实可以直接通过 set 指令直接实现,但通过 setex 实现会更加简洁便利,下面是两个代码的对比:

127.0.0.1:6379> set key1 123 ex 5
127.0.0.1:6379> setex key1 5 123

使用 setex 命令可以避免写 ex 来表示附加过期时间设置,而且这种命令看起来非常直白,一看就能知道这个命令的用法是什么

2、psetex 指令

与之对应的就是 psetex,没错,相信你看到这个命令就已经知道这个命令是用来干什么的了,这不就多了一个 p 嘛,那这个指令肯定就是以 ms 为单位来设置过期时间的了

3、setnx 指令

该指令是用来设置不存在的键值对的,存在的键值是无法修改的,用法如下:

setnx key value

具体例子如下:

127.0.0.1:6379> setnx key1 value1
(integer) 1
127.0.0.1:6379> setnx key1 value1
(integer) 0

4、incr 及 incrby 指令

String 类型若存放一个64位以内的整数,这个数就会在底层被以 int 类型存储,而这时就可以通过 incr 指令来对某个数进行加一操作了,同样的,通过incrby就可以给一个数
加上一个特定的值,当然也可以是一个负数,同时,它们两者可以对一个不存在的键值对进行操作,会将这个值设定为0,语法如下:

incr key
incrby key increment

下面是使用举例:

# 设置一个已经存在的 key 值 key1
127.0.0.1:6379> set key1 1
OK
# 让 key1 自增 
127.0.0.1:6379> incr key1
(integer) 2
# 让不存在的 key2 自增
127.0.0.1:6379> incr key2
(integer) 1
# 对存在的 key1 加10
127.0.0.1:6379> incrby key1 10
(integer) 12
# 对不存在的 key3 加20
127.0.0.1:6379> incrby key3 20
(integer) 20
# 对 key3 加-10(减10)
127.0.0.1:6379> incrby key3 -10
(integer) 10

5、decr 及 decrby 指令

这个指令用法和 incr、incrby 完全一致,你们可能会疑惑,incr 和 incrby 已经可以处理减法操作了,那为啥还要有减法指令呢?道理很简单,假如说
你去银行取100块钱,银行跟你说已存钱-100块,你听着舒服吗,这样设计是为了让命令用起来更加人性化,当然,decr一类指令也可以通过减负数来实现
加法运算,下面是使用示例:

127.0.0.1:6379> set key1 1
OK
127.0.0.1:6379> decr key1
(integer) 0
127.0.0.1:6379> decr key2
(integer) -1
127.0.0.1:6379> decrby key1 10
(integer) -10
127.0.0.1:6379> decrby key3 20
(integer) -20
127.0.0.1:6379> decrby key3 -10
(integer) -10

6、append 指令

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

该命令的时间复杂度为O(1),追加的字符串⼀般⻓度较短, 可以视为 O(1),其返回值为追加完成之后 string 的⻓度,下面是使用示例:

# 给一个不存在的 key 值使用 append 指令,效果等同于 set 指令
127.0.0.1:6379> append k1 hello
(integer) 5
# 追加字符串 world
127.0.0.1:6379> append k1 world
(integer) 10
127.0.0.1:6379> get k1
"helloworld"
# 追加带有空格的字符串(使用引号) ' world'
127.0.0.1:6379> append k1 ' world'
(integer) 16
127.0.0.1:6379> get k1
"helloworld world"

7、getrange 指令

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

使用示例如下:

127.0.0.1:6379> set key aaabbbcccddd
OK
# 截取从下标1到下标3的字符串
127.0.0.1:6379> getrange key 1 3
"aab"
# 截取从下标1到倒数第一个的字符串
127.0.0.1:6379> getrange key 1 -1
"aabbbcccddd"
# 非正常操作,范围不可能前面大后面小
127.0.0.1:6379> getrange key -1 -3
""
127.0.0.1:6379> getrange key -3 -1
"ddd"

8、setrange 指令

该指令可以从指定偏移量开始覆盖字符串,其返回值为覆盖后字符串的长度

127.0.0.1:6379> set k1 aaabbbcccddd
OK
# 从0开始覆盖写入 eee
127.0.0.1:6379> setrange k1 0 eee
(integer) 12
127.0.0.1:6379> get k1
"eeebbbcccddd"
# 偏移量不能采用负数表示
127.0.0.1:6379> setrange k1 -1 fff
(error) ERR offset is out of range
# 写入字符串末尾,实现效果和 append 指令相同
127.0.0.1:6379> setrange k1 12 fff
(integer) 15
127.0.0.1:6379> get k1
"eeebbbcccdddfff"

对于不存在的键值,利用该指令覆盖字符串,偏移量前面会用\x00来表示

127.0.0.1:6379> setrange k2 3 aaaaaa
(integer) 9
127.0.0.1:6379> get k2
"\x00\x00\x00aaaaaa"

9、strlen 指令

该指令用于计算字符串的长度(按字节计算)

127.0.0.1:6379> set k1 aaabbbccc
OK
127.0.0.1:6379> strlen k1
(integer) 9
127.0.0.1:6379> set k2 你好
OK
127.0.0.1:6379> strlen k2
(integer) 6

String 相关指令汇总表

命令 执行效果 时间复杂度
set key value [key value…] 设置 key 的值是 value O(k), k 是键个数
get key 获取 key 的值 O(1)
del key [key …] 删除指定的 key O(k), k 是键个数
mset key value [key value …] 批量设置指定的 key 和 value O(k), k 是键个数
mget key [key …] 批量获取 key 的值 O(k), k 是键个数
incr key 指定的 key 的值 +1 O(1)
decr key 指定的 key 的值 -1 O(1)
incrby key n 指定的 key 的值 +n O(1)
decrby key n 指定的 key 的值 -n O(1)
incrbyfloat key n 指定的 key 的值 +n O(1)
append key value 指定的 key 的值追加 value O(1)
strlen key 获取指定 key 的值的长度 O(1)
setrange key offset value 覆盖指定 key 的从 offset 开始的部分值 O(n), n 是字符串长度, 通常视为 O(1)
getrange key start end 获取指定 key 的从 start 到 end 的部分值 O(n), n 是字符串长度, 通常视为 O(1)

内部编码规则

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

  • int:8 个字节的⻓整型
  • embstr:⼩于等于 39 个字节的字符串
  • raw:⼤于 39 个字节的字符串

39这个数字并不是固定的,众所周知,Redis 是开源的。你若想要修改这个数值是完全没有问题的,在 Redis 高版本数值也是不一样的
,因此我们重点需要知道的是这两种编码方式的区别(下面以高版本 Redis 为例进行讲解):

  1. embstr vs raw
    特性 embstr (嵌入式字符串) raw (原始字符串)
    适用长度 44字节(Redis 5+) > 44字节
    内存分配 单次分配(连续内存) 两次分配redisObject + SDS
    内存局部性 更好(CPU缓存友好) 稍差(可能内存碎片)
    访问速度 稍快(减少指针跳转) 稍慢(需解引用)
    修改时的行为 修改时会转为 raw 直接修改
  2. 为什么短字符串用 embstr?
    (1)内存效率更高
    embstr 将 Redis对象头(redisObject) 和 字符串数据(SDS,Simple Dynamic String) 存储在 一块连续内存 中,减少内存碎片。
    raw 需要分别分配 redisObject 和 SDS,占用更多内存,且可能产生内存碎片。
    (2)CPU缓存友好
    embstr 的数据在内存中是连续的,CPU 缓存命中率更高,访问速度更快。
    raw 需要额外解引用指针,可能触发 CPU 缓存未命中(Cache Miss)。
    (3)减少内存分配次数
    embstr 只需 1次内存分配,而 raw 需要 2次(先分配 redisObject,再分配 SDS)。
  3. 为什么长字符串用 raw?
    (1)避免大块连续内存分配
    Redis 的 jemalloc 内存分配器对 小内存块(≤64字节) 有优化,但大内存块分配效率较低。
    如果长字符串也用 embstr,会导致 大块连续内存请求,可能引发内存碎片问题。
    (2)修改时不需转换
    embstr 是 只读的,如果修改它,Redis 会先将其转为 raw 再修改。
    长字符串更可能被修改(如 APPEND 操作),直接用 raw 避免转换开销。

下面我们利用命令来查看一下不同的字符串存储方式的不同:

127.0.0.1:6379> set k1 123456789
OK
127.0.0.1:6379> object encoding k1
"int"
127.0.0.1:6379> set k2 abc
OK
127.0.0.1:6379> object encoding k2
"embstr"
127.0.0.1:6379> set k3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OK
127.0.0.1:6379> object encoding k3
"raw"

典型业务场景

缓存功能

String 在 Redis 缓存中的典型流程如下:

sequenceDiagram
    participant 用户
    participant 业务层
    participant Redis缓存
    participant MySQL

    用户->>业务层: 请求数据
    业务层->>Redis缓存: 查询缓存(GET key)
    alt 缓存命中
        Redis缓存-->>业务层: 返回数据(String 反序列化)
        业务层-->>用户: 返回数据
    else 缓存未命中
        业务层->>MySQL: 查询数据库
        MySQL-->>业务层: 返回数据
        业务层->>Redis缓存: 写入缓存(SET key 序列化后的String)
        业务层-->>用户: 返回数据
    end

Redis 一般处理的是网络通信,因此在发送信息或接受信息的时候序列化和反序列化是必要的,因此最终在网络中传输的数据本质上就是 Redis 中的 String

缓存一般分为两步:

  • 缓存查询(GET)

Redis 返回的是 序列化后的 String(如 JSON、Protocol Buffers、MessagePack 等格式)。
业务层需要 反序列化 后才能使用(例如 JSON.parse())。

  • 缓存写入(SET)

业务层从 MySQL 获取数据后,需先 序列化成 String(如 JSON.stringify()),再存入 Redis。
如果数据较大,Redis 会使用 raw 编码存储。

缓存实现方案示例:基于Redis和MySQL的用户信息查询优化

1、业务需求
根据用户uid获取用户信息
接口定义:

UserInfo getUserInfo(long uid) {
    ...
}

2、从Redis获取用户信息

// 根据uid生成Redis键
String key = "user:info:" + uid;

// 尝试从Redis获取值
String value = Redis执行命令: get key;

// 缓存命中处理
if (value != null) {
    // 反序列化JSON格式的用户信息
    UserInfo userInfo = JSON反序列化(value);
    return userInfo;
}

3、 缓存未命中时从MySQL获取

// 缓存未命中处理
if (value == null) {
    // 从数据库查询
    UserInfo userInfo = MySQL执行SQL: select * from user_info where uid = <uid>;
    
    // 用户不存在处理
    if (userInfo == null) {
        响应404;
        return null;
    }
    
    // 序列化用户信息
    String value = JSON序列化(userInfo);
    
    // 写入Redis并设置1小时(3600秒)过期时间
    Redis执行命令: set key value ex 3600;
    
    // 返回用户信息
    return userInfo;
}

通过上面的优化,理想情况下,每个用户信息每小时只会有1次MySQL查询,显著提升了查询效率,且有效降低了MySQL访问压力
注意:在 Redis 中命名需要言简意赅,为了提高内存访问速度,在保证业务正常运转的前提下,字符串长度肯定是越短越好,因此有时候可以利用缩写来代替全名

计数功能

String 类型不仅能作为缓存,还能通过其原子性操作(如 INCRINCRBY)和高性能内存读写实现计数功能

Redis String 在计数场景的优势

1、原子性操作

  • Redis 的 INCRINCRBY 是 原子操作,无需加锁即可避免并发冲突。
    2、超高吞吐量
  • Redis 单机可支持 10万+ QPS 的写入,适合高频计数(如热门视频的播放量统计)。
  • 对比 MySQL:
    • MySQL 的 UPDATE counters SET value=value+1 WHERE id=123 在高并发下可能成为瓶颈。
    • Redis 的 INCR 直接操作内存,性能碾压。

3、灵活过期控制
我们可以利用 expire 命令设置过期时间(如按天统计):

INCR daily:uv:20240501  # 记录2025年7月8日的UV
EXPIRE daily:uv:20240501 86400  # 1天后自动删除

技术场景中的典型示例

视频播放量统计

sequenceDiagram
    participant 用户
    participant 业务层
    participant Redis
    participant 消息队列
    participant MySQL

    用户->>业务层: 请求播放视频ID=123
    业务层->>Redis: INCR video:views:123
    Redis-->>业务层: 返回当前播放量(如42)
    业务层-->>用户: 返回视频数据+播放量
    loop 异步持久化
        Redis->>消息队列: 定时推送增量数据(如video:123 +1)
        消息队列->>MySQL: 消费并更新数据库
    end

当然,实际中要开发⼀个成熟、稳定的真实计数系统,要⾯临的挑战远不⽌如此简单:防作弊、按
照不同维度计数、避免单点问题、数据持久化到底层数据源等。

会话功能

一个分布式 Web 服务将⽤⼾的 Session 信息(例如⽤⼾登录信息)保存在各⾃的服务器中,但这样会造成⼀个问题:出于负载均衡的考虑,
分布式服务会将⽤⼾的访问请求均衡到不同的服务器上,并且通常⽆法保证⽤⼾每次请求都会被均衡到同⼀台服务器上,这样当⽤⼾刷新⼀
次访问是可能会发现需要重新登录,这个问题是⽤⼾⽆法容忍的。

为了解决这个问题,可以使⽤ Redis 将⽤⼾的 Session 信息进⾏集中管理,在这种模式下,只要保证 Redis 是⾼可⽤和可扩展性的,⽆论⽤⼾
被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息:

Redis 集中式会话管理(基于 String 数据类型)

graph TD
    A[用户] --> B[负载均衡器]
    B --> C[Web 服务器 1]
    B --> D[Web 服务器 2]
    B --> E[Web 服务器 N]
    C --> F[Redis]
    D --> F
    E --> F
    
    subgraph 会话存储
    F[("Redis (String 数据类型)")]
    end

手机验证码

许多应⽤出于安全考虑,会在每次进⾏登录时,让⽤⼾输⼊⼿机号并且配合给⼿机发送验证码,然后让⽤⼾再次输⼊收到的验证码
并进⾏验证,从⽽确定是否是⽤⼾本⼈。为了短信接⼝不会频繁访问,会限制⽤⼾每分钟获取验证码的频率,实现的伪代码如下:

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 中字符串数据类型的应用场景还远不止于此,这里介绍的是几个非常典型的应用场景