对于Redis:string类型的解析

开篇介绍:

hello 大家,那么在大家学习完了Redis的单线程模型之后,我们接下来就要再进一步的学习Redis的相关知识点,首当其冲的就是string类型,所以,接下来我们就来学习一下Redis中的string类型。

作为 Redis最基础、最核心、使用频率最高的数据类型,String 类型是所有 Redis 使用者的入门必修课,更是后续学习 list、hash、set、zset 的基础 ------Redis 中所有键的类型都是 String,其他四种数据结构的元素也均为 String 类型。

在这最上面,给大家讲一下,如何在Redis命令行客户端中实现get汉字是不出现16进制,而是纯正的汉字,那这就需要大家在redis-cli的时候,后面加上--raw,即:

bash 复制代码
redis-cli --raw

一、Redis String 类型的核心基础认知:你必须先懂的底层特性

在学习命令和场景之前,我们首先要搞清楚Redis 的 String 到底是什么 、有哪些关键特性,这是理解后续所有内容的前提,也是避开使用误区的关键。

1.1 不是普通字符串:Redis String 的本质定义

Redis 中的 String 类型并非传统编程语言中的字符串(如 Java 的 String、C++ 的 std::string) ,而是一种动态的二进制字节数组。

简单来说,Redis 不关心你存的是什么数据,它只把数据当作二进制流来存储和读取 ------ 这也是 Redis String 最核心的特性来源。

官方对 String 类型的定义有两个关键要点:

  1. 存储的值可以是字符串 (普通文本、JSON/XML 格式、任意字符集)、数字 (整型、浮点型)、二进制流(图片、音频、视频、序列化对象等);
  2. 单个 String 类型值的最大容量为 512MB,满足绝大多数业务的存储需求。

1.2 核心特性 1:二进制安全,不处理字符集编码

这是 Redis String 最易被忽略但极其重要的特性,也是它能存储任意二进制数据的原因。

  • 二进制安全:指 Redis 在存储和读取数据时,不会对数据的二进制内容做任何修改、解析,输入什么二进制流,输出就是什么二进制流,不会因为遇到\0(空字符)就截断数据(而 C 语言的字符串会以\0作为结束标识);
  • 不处理字符集编码:Redis 服务端不会对客户端传入的字符串做任何编码 / 解码操作,客户端用什么字符集(如 UTF-8、GBK、GB2312)编码,Redis 就原封不动存储,后续读取时也由客户端自行解码。

通俗举例:你用 Java 客户端以 UTF-8 编码将中文 "Redis 字符串" 存入 Redis,Redis 只会存储该字符串对应的 UTF-8 二进制流;如果后续用 Python 客户端以 GBK 解码读取,就会出现乱码 ------ 这是客户端编码不一致的问题,与 Redis 无关。

1.3 核心特性 2:Redis 中所有键都是 String 类型

Redis 是键值对数据库,其中键(Key)的类型固定为 String,而值(Value)可以是 String、list、hash 等五种数据类型。

这意味着:

  • 无论你的值是哪种类型,键都必须是符合 String 规范的字符串(如user:1001、video:play:5253);
  • 键的命名没有强制语法限制(仅不能包含少数特殊字符如\0),但合理的键名设计是 Redis 使用的基础(后续实战部分会详细讲键名设计规范)。

1.4 核心特性 3:值的灵活存储,支持数字原生操作

Redis String 可以存储数字(整型 / 浮点型),并且提供了incr/decr/incrbyfloat等原生数字操作命令,无需客户端先读取、再计算、最后写入,直接在服务端完成原子性操作 ------ 这也是 Redis 能高效实现计数功能的核心原因。

需要注意的是:Redis 存储数字时,并非直接存为数值类型,而是先以字符串形式存储,再通过内部编码优化为整型(后续内部编码部分详细讲),但对开发者来说,这个过程是完全透明的,我们可以直接对存储的数字执行自增 / 自减操作。

1.5 一张表总结 Redis String 的基础特性

特性 具体说明 核心价值
二进制安全 按二进制流存储,不修改、不解析数据,不处理编码 支持存储任意数据(文本、数字、二进制流)
键固定为 String 所有 Redis 键都是 String 类型,值支持多类型 统一键的规范,为其他数据结构奠定基础
最大 512MB 单个 String 值的最大容量为 512MB 满足绝大多数业务的存储需求
支持数字原生操作 提供原子性的自增 / 自减 / 浮点运算命令 高效实现计数、累加等业务场景
动态存储 可根据存储内容自动调整内部编码 平衡内存占用和操作性能

二、Redis String 类型的核心命令:分模块深度解析,附使用场景与示例

Redis 为 String 类型提供了丰富且灵活的命令体系,覆盖基础增删查改、批量操作、数字计数、字符串操作四大类,所有命令的时间复杂度均为 O (1)(批量操作为 O (N),N 为键的数量),性能极致。

2.1 基础增删查改命令:SET/GET/DEL,String 类型的入门三板斧

这是 String 类型最基础的命令,也是使用频率最高的命令,其中SET命令支持多个核心选项,功能远超 "简单设置值",是 Redis String 的核心命令。

2.1.1 SET:设置键值对,支持过期、条件设置

核心作用 :将 String 类型的 value 关联到 key,如果 key 已存在,直接覆盖原有值(无论原有值是什么数据类型),且该 key 的过期时间(TTL)会被清空。

语法 :SET key value [EX seconds] [PX milliseconds] [NX|XX]

有效版本:1.0.0+

时间复杂度:O(1)

核心选项(重点):Redis 的 SET 命令通过选项实现了 SETNX、SETEX、PSETEX 等命令的功能,官方后续可能合并这些单独命令,推荐优先使用带选项的 SET:

  • EX seconds:为 key 设置秒级过期时间,等价于SETEX命令;
  • PX milliseconds:为 key 设置毫秒级过期时间,等价于PSETEX命令;
  • NX:仅当 key 不存在时才设置值,要是原先该key值存在,那么就不会设置新的值(value)去覆盖原来的值(value),等价于SETNX命令,核心用于实现分布式锁;
  • XX:仅当 key 存在时才设置值,要是原先该key值不存在,那么就不会设置值,核心用于实现安全更新,避免创建不存在的 key。

返回值:

  • 设置成功:返回OK;
  • 条件不满足(NX/XX):返回(nil),不执行任何操作。

实操示例:覆盖原有值、条件设置、过期设置全场景

bash 复制代码
# 1. 基础设置:key不存在,设置成功
127.0.0.1:6379> EXISTS mykey  # 检查key是否存在
(integer) 0
127.0.0.1:6379> SET mykey "Hello Redis"
OK
127.0.0.1:6379> GET mykey
"Hello Redis"

# 2. NX选项:key已存在,设置失败
127.0.0.1:6379> SET mykey "Hello World" NX
(nil)

# 3. XX选项:删除key后,key不存在,设置失败
127.0.0.1:6379> DEL mykey
(integer) 1
127.0.0.1:6379> SET mykey "Hello World" XX
(nil)

# 4. NX选项:key不存在,设置成功
127.0.0.1:6379> SET mykey "Hello World" NX
OK

# 5. EX选项:设置值并指定10秒过期
127.0.0.1:6379> SET mykey "Expire in 10s" EX 10
OK
127.0.0.1:6379> GET mykey  # 10秒内读取
"Expire in 10s"
127.0.0.1:6379> GET mykey  # 10秒后读取,key已过期
(nil)

# 6. 组合选项:NX+EX,key不存在时设置值并过期,分布式锁核心用法
127.0.0.1:6379> SET lock:order 1 NX EX 30
OK

核心使用场景:

  • 基础的键值对存储:如缓存用户昵称、商品名称;
  • 分布式锁:SET key value NX EX seconds(最经典的 Redis 分布式锁实现,后续实战部分详细讲);
  • 安全更新:SET key value XX,仅更新已存在的 key,避免误创建;
  • 带过期时间的缓存:SET key value EX/PX,无需单独执行EXPIRE命令,减少网络请求。
2.1.2 GET:获取键对应的值,类型校验

核心作用 :获取指定 key 对应的 String 值,若 key 不存在返回nil,若 key 的值不是 String 类型(如 list、hash),直接报错。

语法 :GET key

有效版本:1.0.0+

时间复杂度:O(1)

返回值:

  • key 存在且为 String 类型:返回对应 value;
  • key 不存在:返回(nil);
  • key 的值非 String 类型:返回WRONGTYPE错误。

实操示例:基础读取、不存在、类型错误场景

bash 复制代码
# 1. key不存在,返回nil
127.0.0.1:6379> GET nonexist_key
(nil)

# 2. key存在且为String类型,返回值
127.0.0.1:6379> SET mykey "Hello Redis"
OK
127.0.0.1:6379> GET mykey
"Hello Redis"

# 3. key的值为hash类型,读取报错
127.0.0.1:6379> DEL mykey
(integer) 1
127.0.0.1:6379> HSET mykey name "Bob" age 20
(integer) 2
127.0.0.1:6379> GET mykey
(error) WRONGTYPE Operation against a key holding the wrong kind of value

核心使用场景:所有 String 类型值的读取操作,如从缓存中读取用户信息、验证码等。

2.1.3 DEL:删除指定键,通用命令

核心作用:删除指定的一个或多个 key,无论 key 的值是什么数据类型,删除成功返回删除的 key 数量。

语法 :DEL key [key ...]

有效版本:1.0.0+

时间复杂度:O (N),N 为删除的 key 数量

返回值:成功删除的 key 的数量(注意:删除不存在的 key,不计入返回值)。

实操示例:

bash 复制代码
127.0.0.1:6379> SET key1 "a" key2 "b" key3 "c"
OK
127.0.0.1:6379> DEL key1 key2 nonexist_key  # 删除2个存在的key,1个不存在的
(integer) 2

核心使用场景:清理无用的键值对,如缓存失效、用户退出后清理 Session。

2.2 批量操作命令:MSET/MGET,提升 Redis 访问性能

在实际业务中,我们经常需要同时设置 / 读取多个键值对,如果使用多次SET/GET,会产生多次网络请求,而网络 IO 是 Redis 性能的主要瓶颈之一 ------批量操作命令 MSET/MGET 通过一次网络请求完成多个键的设置 / 读取,大幅减少网络开销,提升性能。

这是 Redis 开发的核心优化技巧,必须掌握!

2.2.1 MSET:批量设置多个键值对

核心作用:一次性设置多个 key-value 键值对,若部分 key 已存在,直接覆盖,原子性操作(要么全部设置成功,要么全部失败)。

语法 :MSET key value [key value ...],其实也就是mset后面跟着多个key value,然后每个组合都用空格隔开,也就形成了批量设置

有效版本:1.0.1+

时间复杂度:O (N),N 为键的数量

返回值 :永远返回OK(原子性,无部分成功)。

实操示例:

bash 复制代码
127.0.0.1:6379> MSET user:1001:name "张三" user:1001:age "25" user:1001:gender "男"
OK
127.0.0.1:6379> GET user:1001:name
"张三"
127.0.0.1:6379> GET user:1001:age
"25"
2.2.2 MGET:批量获取多个键的值

核心作用 :一次性获取多个 key 的值,返回值的顺序与传入 key 的顺序一致;若 key 不存在 / 值非 String 类型,对应位置返回nil。

语法 :MGET key [key ...],其实也就是mget后面跟着多个key value,然后每个组合都用空格隔开,也就形成了批量查询,和mset的使用方法可以说是一模一样

有效版本 :1.0.0+时间复杂度:O (N),N 为键的数量

返回值 :对应 key 的值组成的列表,不存在 / 类型错误的 key 对应位置为nil。

实操示例:

复制代码
127.0.0.1:6379> MGET user:1001:name user:1001:age user:1001:gender nonexist_key
1) "张三"
2) "25"
3) "男"
4) (nil)
2.2.3 批量操作 vs 单次操作:性能对比(重点)

我们用实际时间计算来看看批量操作的性能优势,假设:

  • 单次网络请求耗时:1 毫秒(网络 IO 的固定开销);
  • 单次命令执行耗时:0.1 毫秒(Redis 纯内存操作,耗时极低);
  • 需操作的键数量:1000 个。

1000 次单次 GET 操作总耗时 :1000×1ms(网络) + 1000×0.1ms(执行) = 1100ms = 1.1秒

1 次 MGET 操作总耗时 :1×1ms(网络) + 1000×0.1ms(执行) = 101ms = 0.101秒

性能提升约 10 倍 !这就是批量操作的核心价值 ------将多次网络请求合并为一次,大幅减少网络 IO 开销(Redis 的纯内存操作耗时可以忽略,性能瓶颈主要在网络)。

2.2.4 批量操作的使用注意事项
  1. 原子性:MSET 是原子操作,要么全部设置成功,要么全部失败,适合需要同时设置多个关联键的场景(如用户的多个属性);
  2. 键数限制 :批量操作的键数不宜过多(建议不超过 1000 个),否则会导致单个命令执行时间过长,阻塞 Redis 单线程(Redis 单线程模型,长命令会阻塞后续请求);
  3. 类型统一 :MGET 仅能正确读取 String 类型的 key,若批量传入的 key 中有非 String 类型,对应位置返回nil,不影响其他 key 的读取。

核心使用场景:

  • 批量缓存 / 读取结构化数据:如用户的姓名、年龄、性别等多个属性,用 MSET 批量设置,MGET 批量读取;
  • 批量操作热点数据:如电商首页的多个商品名称、价格,一次性读取,提升接口响应速度。

2.3 数字计数命令:INCR/INCRBY/DECR/DECRBY/INCRBYFLOAT,原子性数值运算

Redis String 支持存储数字(整型 / 浮点型),并提供了原子性的数值运算命令,无需客户端参与计算,直接在服务端完成 ------ 这是 Redis 实现计数功能的核心,也是相比其他缓存(如 Memcached)的优势之一。

核心特性 :所有计数命令都是原子操作(Redis 单线程模型,命令串行执行,无并发竞争),无需加锁,可直接用于高并发场景(如视频播放量、文章阅读量、接口访问量)。

通用规则:

  1. 若 key 不存在,执行命令时会先将 key 的值初始化为0,再进行运算;
  2. 若 key 的值不是数字(如普通字符串),或整型超出64 位有符号整型范围,直接报错;
  3. 浮点运算命令INCRBYFLOAT支持科学计数法,且可以传入负数实现减法。
2.3.1 整型自增:INCR/INCRBY

incr,其实就是英语increase的前4个字母啦

  • INCR :将 key 对应的整型值自增 1 ,语法INCR key;
  • INCRBY :将 key 对应的整型值自增指定的整数 n ,语法INCRBY key n,这里要注意,n也可以为负数,为负数的话就是减去指定的整数哦
  • 时间复杂度:O(1);
  • 返回值:运算后的整型值。

实操示例:

bash 复制代码
# INCR:key不存在,初始化为0,自增1
127.0.0.1:6379> EXISTS counter
(integer) 0
127.0.0.1:6379> INCR counter
(integer) 1

# INCR:继续自增1
127.0.0.1:6379> INCR counter
(integer) 2

# INCRBY:自增10
127.0.0.1:6379> INCRBY counter 10
(integer) 12

# 非数字值,报错
127.0.0.1:6379> SET counter "abc"
OK
127.0.0.1:6379> INCR counter
(error) ERR value is not an integer or out of range

# 超出64位有符号整型范围,报错
127.0.0.1:6379> SET counter "9223372036854775807"  # 64位有符号整型最大值
OK
127.0.0.1:6379> INCR counter
(error) ERR value is not an integer or out of range
2.3.2 整型自减:DECR/DECRBY
  • DECR :将 key 对应的整型值自减 1 ,语法DECR key;
  • DECRBY :将 key 对应的整型值自减指定的整数 n ,语法DECRBY key n,一样的,这里的n也可以是负数,就相当于是key 对应的整型值-(-n)而已,这个大家还是很好理解的
  • 时间复杂度:O(1);
  • 返回值:运算后的整型值。

实操示例:

bash 复制代码
# DECR:key不存在,初始化为0,自减1
127.0.0.1:6379> EXISTS counter
(integer) 0
127.0.0.1:6379> DECR counter
(integer) -1

# DECR:继续自减1
127.0.0.1:6379> DECR counter
(integer) -2

# DECRBY:自减5
127.0.0.1:6379> DECRBY counter 5
(integer) -7
2.3.3 浮点型运算:INCRBYFLOAT

核心作用 :将 key 对应的浮点型值增加指定的浮点数 n ,传入负数则实现减法,支持科学计数法(如5.0e3表示 5000),实际这里的key对应的浮点数是以字符串形成存储的,但是当我们使用incrbyfloat的时候,redis就会把字符串转换为浮点型之后,再去进行计算哦,所以相对整型计算而言,浮点型的计算效率还是较低的

语法 :INCRBYFLOAT key increment,这里我们要注意,redis并没有提供decrbyfloat,所以我们要是想使用减法的话,就是得incrbyfloat后面跟着负数哦,此时就是进行减法运算

有效版本:2.6.0+

时间复杂度:O(1)

返回值:运算后的浮点型值(字符串形式)。

实操示例:

bash 复制代码
# 基础浮点自增
127.0.0.1:6379> SET num 10.50
OK
127.0.0.1:6379> INCRBYFLOAT num 0.1
"10.6"

# 浮点自减(传入负数)
127.0.0.1:6379> INCRBYFLOAT num -5
"5.6"

# 支持科学计数法
127.0.0.1:6379> SET num 5.0e3
OK
127.0.0.1:6379> INCRBYFLOAT num 2.0e2
"5200"
2.3.4 计数命令的核心使用场景
  • 视频 / 文章播放量 :用户每播放一次视频,执行INCR video:play:5253;
  • 接口访问量统计 :接口每被访问一次,执行INCR api:visit:user:list;
  • 点赞 / 收藏数 :用户点赞,执行INCR article:like:1001;取消点赞(业务层控制),执行DECR article:like:1001;
  • 浮点型统计:如商品价格调整、数据指标累加(如销售额、用户增长率)。

2.4 字符串操作命令:APPEND/STRLEN/GETRANGE/SETRANGE,对字符串的原生操作

Redis 还提供了对 String 类型值的原生字符串操作命令,无需客户端读取后再处理,直接在服务端完成,减少网络请求和客户端计算开销。

2.4.1 APPEND:追加字符串到原有值末尾

核心作用 :若 key 存在且为 String 类型,将 value 追加到原有值的末尾;若 key 不存在,等价于SET key value。

语法 :APPEND key value ,这里要注意,后面value内容不能有空格,一旦有的话,就会报错,也就是说,value必须是一个连续的字符串

有效版本:2.0.0+

时间复杂度:O (1)(追加的字符串长度较短时,可视为 O (1))

返回值:追加后的字符串总长度。

实操示例:

bash 复制代码
# key不存在,等价于SET
127.0.0.1:6379> EXISTS mykey
(integer) 0
127.0.0.1:6379> APPEND mykey "Hello"
(integer) 5
127.0.0.1:6379> GET mykey
"Hello"

# key存在,追加字符串
127.0.0.1:6379> APPEND mykey " Redis"
(integer) 11
127.0.0.1:6379> GET mykey
"Hello Redis"

使用场景:动态拼接字符串,如日志记录、拼接用户行为轨迹。

2.4.2 STRLEN:获取字符串长度

核心作用:获取 key 对应的 String 值的长度;若 key 不存在,返回 0;若 key 的值非 String 类型,报错。

语法 :STRLEN key

有效版本:2.2.0+

时间复杂度:O(1)

返回值:字符串长度(key 不存在为 0)。

实操示例:

bash 复制代码
127.0.0.1:6379> SET mykey "Hello Redis"
OK
127.0.0.1:6379> STRLEN mykey
(integer) 11

# key不存在,返回0
127.0.0.1:6379> STRLEN nonexist_key
(integer) 0

使用场景:校验字符串长度,如验证码长度、用户昵称长度。

2.4.3 GETRANGE:获取字符串的子串(左闭右闭)

核心作用 :获取字符串中从start到end的子串,左闭右闭区间;支持负数索引(-1 表示最后一个字符,-2 表示倒数第二个,以此类推);超出范围的索引会自动调整为字符串的起始 / 结束位置,这里大家要和C++区分开来,C++采取的是左闭右开区间,大家要注意哦,还有就是,redis中,默认字符串的第一个字符下标是0哦,和C++数组一样的规则

语法 :GETRANGE key start end

有效版本:2.4.0+

时间复杂度:O (N)(N 为子串长度,字符串较短时视为 O (1))

返回值:截取的子串。

实操示例:

bash 复制代码
127.0.0.1:6379> SET mykey "This is a Redis string"
OK

# 正索引:0-3,截取前4个字符
127.0.0.1:6379> GETRANGE mykey 0 3
"This"

# 负索引:-6到-1,截取最后6个字符
127.0.0.1:6379> GETRANGE mykey -6 -1
"string"

# 0到-1,截取整个字符串
127.0.0.1:6379> GETRANGE mykey 0 -1
"This is a Redis string"

# 索引超出范围,自动调整
127.0.0.1:6379> GETRANGE mykey 20 100
"string"

使用场景:截取字符串的指定部分,如获取手机号的后 4 位、截取商品名称的前 10 个字符。

2.4.4 SETRANGE:从指定偏移量覆盖字符串

核心作用 :从指定的offset(偏移量,从 0 开始)位置开始,用新的 value 覆盖原有字符串的部分内容;若偏移量超过原有字符串长度,中间用\0(空字符)填充

语法 :SETRANGE key offset value

有效版本:2.2.0+

时间复杂度:O (N)(N 为新 value 的长度,较短时视为 O (1))

返回值:覆盖后的字符串总长度。

实操示例:

bash 复制代码
127.0.0.1:6379> SET mykey "Hello World"
OK

# 从偏移量6开始,用"Redis"覆盖原有内容
127.0.0.1:6379> SETRANGE mykey 6 "Redis"
(integer) 11
127.0.0.1:6379> GET mykey
"Hello Redis"

# 偏移量超过原有长度,中间补空字符
127.0.0.1:6379> SETRANGE mykey 20 "test"
(integer) 24
127.0.0.1:6379> GET mykey
"Hello Redis\x00\x00\x00\x00\x00\x00\x00\x00\x00test"

使用场景:修改字符串的指定部分,如更新缓存中的部分数据、替换固定位置的字符。

2.5 所有 String 命令汇总表:快速查询,按需使用

为了方便大家日常开发快速查询,我们将所有 Redis String 核心命令按功能、语法、时间复杂度、核心作用整理成表,涵盖开发中 99% 的使用场景:

功能模块 命令 语法 时间复杂度 核心作用
基础操作 SET SET key value EX/PX NX/XX O(1) 设置键值对,支持过期、条件设置
基础操作 GET GET key O(1) 获取 String 类型值,类型校验
基础操作 DEL DEL key key ... O(N) 删除一个 / 多个键,通用命令
批量操作 MSET MSET key value key value ... O(N) 批量设置多个键值对,原子性
批量操作 MGET MGET key key ... O(N) 批量获取多个键值对,顺序一致
整型计数 INCR INCR key O(1) 整型值自增 1,key 不存在初始化为 0
整型计数 INCRBY INCRBY key n O(1) 整型值自增指定整数 n
整型计数 DECR DECR key O(1) 整型值自减 1,key 不存在初始化为 0
整型计数 DECRBY DECRBY key n O(1) 整型值自减指定整数 n
浮点计数 INCRBYFLOAT INCRBYFLOAT key increment O(1) 浮点型值增减指定浮点数,支持科学计数法
字符串操作 APPEND APPEND key value O(1) 追加字符串到末尾,key 不存在等价于 SET
字符串操作 STRLEN STRLEN key O(1) 获取字符串长度,key 不存在返回 0
字符串操作 GETRANGE GETRANGE key start end O(N) 截取子串,左闭右闭,支持负索引
字符串操作 SETRANGE SETRANGE key offset value O(N) 从偏移量开始覆盖字符串,超长度补空字符

三、Redis String 类型的内部编码:智能优化,平衡内存与性能

Redis 的对外数据类型(如 String、list)和底层内部编码是分离的 ------ 对外暴露统一的 String 接口,底层会根据存储的数据类型、字符串长度 自动选择最优的内部编码,实现内存占用 和操作性能的平衡。

这是 Redis 的设计精髓之一,对开发者完全透明,无需手动干预,但理解内部编码能帮助我们更好地优化 Redis 的内存使用和性能。

3.1 String 类型的三种内部编码

Redis String 类型提供了三种内部编码 ,分别适配不同的存储场景,可通过object encoding key命令查看指定 key 的内部编码:

  1. int:8 字节的 64 位有符号整型;
  2. embstr :用于存储小于等于 39 字节的短字符串(Redis 3.2 + 版本,旧版本为 44 字节);
  3. raw :用于存储大于 39 字节的长字符串,或需要修改的短字符串。

3.2 每种内部编码的深度解析:适用场景 + 核心优势

3.2.1 int:整型值的极致内存优化

适用场景 :key 的值为64 位有符号整型(范围:-9223372036854775808 ~ 9223372036854775807)。

核心实现 :直接将值存储为 8 字节的 64 位整型,而非字符串形式,极致节省内存 (存储整型1000仅需 8 字节,存储字符串"1000"需要 4 字节)。

核心优势:

  • 内存占用极低,适合大量计数场景(如播放量、访问量);
  • 整型运算命令(incr/decr)无需字符串转整型,性能极致。

实操示例:

bash 复制代码
127.0.0.1:6379> SET counter 6379
OK
127.0.0.1:6379> object encoding counter
"int"

# 超出64位有符号整型范围,自动转为raw
127.0.0.1:6379> SET big_num 9223372036854775808
OK
127.0.0.1:6379> object encoding big_num
"raw"
3.2.2 embstr:短字符串的连续内存存储

适用场景 :key 的值为小于等于 39 字节 的短字符串,且无需修改(修改会触发编码转换)。

核心实现 :将Redis 对象的元数据 (如对象类型、引用计数、长度)和字符串内容 存储在同一块连续的内存中,一次性完成内存分配和释放,减少内存碎片。

核心优势:

  • 连续内存存储,缓存命中率高(CPU 缓存能一次性读取元数据和内容);
  • 内存分配 / 释放仅需一次,操作效率高;
  • 相比 raw 编码,减少了内存指针的开销。

实操示例:

bash 复制代码
# 短字符串(5字节),embstr编码
127.0.0.1:6379> SET str "hello"
OK
127.0.0.1:6379> object encoding str
"embstr"

# 39字节字符串,embstr编码
127.0.0.1:6379> SET str39 "123456789012345678901234567890123456789"
OK
127.0.0.1:6379> STRLEN str39
(integer) 39
127.0.0.1:6379> object encoding str39
"embstr"

# 40字节字符串,自动转为raw编码
127.0.0.1:6379> SET str40 "1234567890123456789012345678901234567890"
OK
127.0.0.1:6379> STRLEN str40
(integer) 40
127.0.0.1:6379> object encoding str40
"raw"
3.2.3 raw:长字符串 / 可修改字符串的灵活存储

适用场景 :key 的值为大于 39 字节 的长字符串,或需要修改的短字符串 (如APPEND、SETRANGE操作)。

核心实现 :将Redis 对象的元数据 和字符串内容 存储在两块不同的内存中,通过指针关联,支持动态扩容和修改。

核心优势:

  • 支持任意长度的字符串(最大 512MB),灵活度高;
  • 支持频繁的修改操作,扩容逻辑成熟;
  • 避免大尺寸的连续内存块,减少内存碎片。

关键特性 :embstr 编码的字符串修改后会直接转为 raw 编码 ,且不可逆(即使后续将字符串缩短至 39 字节以内,也不会再转回 embstr)。

实操示例:

bash 复制代码
# 短字符串,embstr编码
127.0.0.1:6379> SET str "hello"
OK
127.0.0.1:6379> object encoding str
"embstr"

# 追加字符串后,自动转为raw编码
127.0.0.1:6379> APPEND str " redis"
(integer) 11
127.0.0.1:6379> object encoding str
"raw"

# 即使缩短字符串,也不会转回embstr
127.0.0.1:6379> SET str "hello"
OK
127.0.0.1:6379> object encoding str
"embstr"

3.3 String 类型内部编码的自动转换规则(重点)

Redis 会根据数据类型、字符串长度、操作行为 自动完成内部编码的转换,转换规则为单向不可逆(仅从更优的编码向通用的编码转换,不会反向转换),具体规则如下:

  1. int → embstr/raw:当将整型值修改为非整型字符串时,若字符串长度≤39 字节转为 embstr,否则转为 raw;
  2. embstr → raw :
    • 字符串长度超过 39 字节;
    • 对字符串执行修改操作(APPEND、SETRANGE);
    • 转换后不可逆;
  3. 无 raw → embstr/int:raw 编码不会再转回 embstr 或 int,即使数据满足条件。

一张表总结转换规则:

原编码 触发条件 目标编码 可逆性
int 修改为非整型短字符串(≤39 字节) embstr 不可逆
int 修改为非整型长字符串(>39 字节) raw 不可逆
embstr 字符串长度 > 39 字节 / 执行修改操作 raw 不可逆
raw 任何条件 无转换 -

3.4 内部编码的设计价值:为什么 Redis 要做这样的优化?

Redis 作为内存数据库,内存利用率 和操作性能是核心指标,而内部编码的智能优化正是为了平衡这两个指标:

  1. 针对场景做极致优化:整型用 int 编码省内存,短字符串用 embstr 编码提升缓存命中率,长字符串用 raw 编码保证灵活性;
  2. 对开发者透明:无需手动指定编码,Redis 自动适配,降低使用成本;
  3. 简化底层实现:不同的编码对应不同的底层数据结构,按需选择,避免单一结构的短板;
  4. 提升整体性能:通过内存优化减少内存占用,通过连续内存提升缓存命中率,最终提升 Redis 的整体处理能力。

四、Redis String 类型的典型实战场景

Redis String 类型的使用场景覆盖了缓存、计数、分布式锁、共享 Session、手机验证码等绝大多数互联网业务场景,也是后续其他数据结构的基础。

4.1 核心场景 1:缓存层(最经典使用场景)

4.1.1 业务痛点

传统的数据库(如 MySQL)是磁盘存储,访问速度慢(毫秒级),无法支撑高并发请求(如电商秒杀、首页访问),导致接口响应慢、数据库崩溃。

4.1.2 解决方案

用 Redis String 作为缓存层,将高频访问的热点数据(如用户信息、商品详情、首页数据)缓存到 Redis 中,绝大多数请求直接从 Redis 读取,避免直接访问数据库,加速读写、降低数据库压力。

4.1.3 核心设计思路
  1. 缓存命中:请求先访问 Redis,若 Redis 中有数据(缓存命中),直接返回,无需访问数据库;
  2. 缓存未命中 :若 Redis 中无数据(缓存未命中),访问数据库获取数据,将数据写入 Redis 并设置过期时间(防止缓存数据与数据库不一致),再返回给客户端;
  3. 键名设计 :遵循业务名:对象名:唯一标识[:属性]的规范,避免键冲突,提升可维护性;
  4. 数据序列化:将结构化数据(如用户对象、商品对象)序列化为 JSON 字符串存储(Redis String 支持存储任意字符串)。
4.1.4 伪代码实现(Java 风格,通用思路)

以根据用户 ID 获取用户信息为例:

java 复制代码
/**
 * 根据用户ID获取用户信息,Redis缓存+MySQL兜底
 * @param uid 用户ID
 * @return 用户信息
 */
UserInfo getUserInfo(Long uid) {
    // 1. 设计Redis键名:业务名:对象名:唯一标识
    String redisKey = "user:info:" + uid;
    // 2. 先从Redis读取缓存
    String userJson = redis.execute("GET", redisKey);
    // 3. 缓存命中:反序列化为对象并返回
    if (userJson != null && !userJson.isEmpty()) {
        return JSON.parseObject(userJson, UserInfo.class);
    }
    // 4. 缓存未命中:从MySQL读取数据
    UserInfo userInfo = mysql.query("select * from user_info where uid = ?", uid);
    // 5. 数据库中无数据:返回404
    if (userInfo == null) {
        return null;
    }
    // 6. 将数据序列化后写入Redis,设置1小时过期时间(防止缓存雪崩)
    String userJsonStr = JSON.toJSONString(userInfo);
    redis.execute("SET", redisKey, userJsonStr, "EX", 3600);
    // 7. 返回用户信息
    return userInfo;
}
4.1.5 关键注意事项
  1. 设置过期时间 :必须为缓存设置过期时间(EX/PX),避免缓存永久有效,导致数据与数据库不一致(缓存脏数据);
  2. 键名设计规范 :拒绝使用无意义的键名(如user1001),遵循业务名:对象名:唯一标识,如order:detail:10001、goods:name:5253;
  3. 避免大字符串缓存:单个缓存值不宜过大(建议不超过 10KB),否则会增加 Redis 的内存占用和网络传输时间;
  4. 缓存更新策略 :数据库数据更新时,需及时删除 Redis 缓存 (而非更新),避免缓存与数据库不一致(如执行DEL user:info:1001);
  5. 缓存穿透 / 雪崩 / 击穿:这是缓存的三大经典问题,后续可通过布隆过滤器、过期时间随机化、分布式锁解决(基础场景先保证核心功能)。

4.2 核心场景 2:计数功能(高并发原子计数)

4.2.1 业务痛点

高并发场景下的计数功能(如视频播放量、文章阅读量、接口访问量),若用数据库实现(如update video set play_count = play_count +1 where id=5253),会产生大量的行锁竞争,导致数据库性能下降,甚至无法支撑高并发。

4.2.2 解决方案

用 Redis String 的原子计数命令(incr/incrby)实现计数,Redis 单线程模型保证计数操作的原子性,无并发竞争,且纯内存操作,性能极致(每秒可支撑数十万次计数),后续可通过异步任务将计数数据同步到数据库中。

4.2.3 核心设计思路
  1. 实时计数 :用户触发计数行为(如播放视频),直接执行 Redis 的INCR命令,完成实时计数;
  2. 键名设计 :业务名:计数类型:唯一标识,如video:play:5253、article:read:1001;
  3. 异步落地:将高频的计数操作放在 Redis 中,通过定时任务 / 消息队列将 Redis 中的计数数据异步同步到数据库,减少数据库压力;
  4. 防作弊:业务层增加防作弊逻辑(如同一用户短时间内多次播放不计入),避免恶意刷量。
4.2.4 伪代码实现(Java 风格)

以视频播放量计数为例:

java 复制代码
/**
 * 视频播放量计数,Redis实时计数,异步同步到MySQL
 * @param vid 视频ID
 * @return 最新播放量
 */
Long incrVideoPlayCount(Long vid) {
    // 1. 设计Redis键名:业务名:计数类型:唯一标识
    String redisKey = "video:play:" + vid;
    // 2. 执行原子自增,获取最新播放量
    Long newPlayCount = redis.execute("INCR", redisKey);
    // 3. 异步将播放量同步到MySQL(如通过消息队列发送任务,不阻塞主线程)
    messageQueue.send("video_play_count_sync", vid, newPlayCount);
    // 4. 返回最新播放量
    return newPlayCount;
}

/**
 * 异步同步播放量到MySQL的消费方法
 * @param vid 视频ID
 * @param playCount 最新播放量
 */
void syncVideoPlayCountToMysql(Long vid, Long playCount) {
    mysql.execute("update video set play_count = ? where id = ?", playCount, vid);
}
4.1.5 关键注意事项
  1. 原子性保证 :直接使用 Redis 的原生计数命令,避免客户端先GET再SET(非原子操作,会导致计数错误);
  2. 异步落地:实时计数只在 Redis 中完成,异步同步到数据库,避免数据库成为性能瓶颈;
  3. 数据持久化:Redis 开启 RDB/AOF 持久化,防止 Redis 重启后计数数据丢失;
  4. 计数清零 :若需要清零计数,执行SET key 0而非DEL key(避免INCR时重新初始化,导致计数错误)。

4.3 核心场景 3:分布式锁(SET NX EX 经典实现)

4.3.1 业务痛点

分布式系统中,多个服务实例同时操作同一资源(如秒杀下单、库存扣减),会产生并发竞争,导致数据不一致(如超卖、重复下单),需要一种跨服务的锁机制来保证操作的原子性。

4.3.2 解决方案

用 Redis String 的SET key value NX EX seconds命令实现分布式锁,这是 Redis 最经典、最常用的分布式锁实现方式,简单高效,能满足绝大多数分布式场景的锁需求。

4.3.3 核心设计思路
  1. 加锁 :执行SET lock:xxx value NX EX seconds,其中:
    • lock:xxx:锁的键名,如lock:order:10001(针对具体资源的锁);
    • NX:仅当锁不存在时才设置,保证只有一个服务实例能获取锁;
    • EX seconds:设置锁的过期时间,防止服务实例挂掉后锁永久有效(死锁);
    • value:建议设置为唯一标识(如服务实例 ID + 线程 ID),用于解锁时的身份校验;
  2. 解锁 :通过 Lua 脚本实现原子解锁(先判断锁的 value 是否为当前实例的唯一标识,再删除锁),避免误删其他实例的锁;
  3. 执行业务:获取锁的服务实例执行具体业务(如扣减库存、创建订单);
  4. 释放锁:业务执行完成后,执行 Lua 脚本解锁。

4.3.4 伪代码实现(Java 风格,含 Lua 解锁脚本)

以电商秒杀下单为例:

java 复制代码
// 分布式锁键名模板:lock:业务名:资源标识
private static final String LOCK_KEY_TPL = "lock:order:%s";
// 锁的过期时间:30秒(根据业务实际耗时调整,避免业务未执行完锁过期)
private static final int LOCK_EXPIRE_SECONDS = 30;
// 锁的唯一标识:服务实例ID+线程ID(防止不同实例/线程误删对方的锁)
private static final String LOCK_VALUE = UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
// 原子解锁Lua脚本:先判断锁的value是否为当前实例的标识,再删除,保证原子性
private static final String UNLOCK_LUA_SCRIPT = 
    "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
    "return redis.call('DEL', KEYS[1]) " +
    "else " +
    "return 0 " +
    "end";

/**
 * 秒杀下单,Redis分布式锁保证单资源原子操作
 * @param goodsId 商品ID
 * @param userId 用户ID
 * @return 是否下单成功
 */
public boolean seckillOrder(Long goodsId, Long userId) {
    // 1. 生成当前商品的分布式锁键名
    String lockKey = String.format(LOCK_KEY_TPL, goodsId);
    Jedis jedis = null;
    try {
        jedis = getJedis();
        // 2. 加锁:SET key value NX EX seconds,原子性加锁+设置过期时间
        String lockResult = jedis.set(lockKey, LOCK_VALUE, SetParams.setParams().nx().ex(LOCK_EXPIRE_SECONDS));
        // 3. 加锁失败:其他实例已获取锁,直接返回下单失败
        if (!"OK".equals(lockResult)) {
            return false;
        }
        // 4. 加锁成功:执行核心秒杀业务(扣减库存、创建订单)
        // 4.1 检查商品库存是否充足(模拟数据库/Redis库存校验)
        Long stock = jedis.incrBy("goods:stock:" + goodsId, 0);
        if (stock <= 0) {
            return false;
        }
        // 4.2 扣减库存
        jedis.decr("goods:stock:" + goodsId);
        // 4.3 创建订单(模拟数据库操作)
        createOrder(goodsId, userId);
        return true;
    } catch (Exception e) {
        log.error("秒杀下单失败", e);
        return false;
    } finally {
        // 5. 最终解锁:执行Lua脚本原子解锁,避免误删锁
        if (jedis != null) {
            jedis.eval(UNLOCK_LUA_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(LOCK_VALUE));
            jedis.close();
        }
    }
}

// 模拟创建订单的数据库操作
private void createOrder(Long goodsId, Long userId) {
    // 执行insert into order(goods_id, user_id) values(?, ?)
}

// 获取Redis连接
private Jedis getJedis() {
    return new Jedis("127.0.0.1", 6379);
}
4.3.5 分布式锁的关键注意事项
  1. 原子性加锁 :必须使用SET key value NX EX seconds单命令 加锁,禁止先SETNX再EXPIRE(两个命令非原子,若加锁后服务挂掉,会导致死锁);
  2. 设置锁过期时间:必须为锁设置合理的过期时间,避免服务实例获取锁后挂掉,导致锁永久有效(死锁),过期时间需大于业务最大执行耗时;
  3. 唯一标识防误删:锁的 value 必须是当前实例 / 线程的唯一标识,解锁时先判断再删除,避免其他实例删除当前实例的锁;
  4. 原子性解锁 :必须用Lua 脚本 实现解锁,禁止先GET再DEL(两个命令非原子,若GET后锁过期,会删除其他实例的锁);
  5. 避免锁重入 :基础的SET NX EX分布式锁不支持重入(同一线程多次获取同一把锁会失败),若需要重入锁,需基于 Redis Hash 实现或使用 Redisson 框架;
  6. 锁竞争优化 :若锁竞争激烈,可引入自旋锁(加锁失败后短暂休眠再重试),避免直接返回失败,提升下单成功率。

4.4 核心场景 4:共享 Session(分布式 Web 服务统一会话)

4.4.1 业务痛点

分布式 Web 服务中,用户的 Session 默认存储在单个 Web 服务器的内存中,而负载均衡会将用户的请求随机分发到不同的 Web 服务器上。用户第一次请求到服务器 A 并创建 Session,第二次请求可能被分发到服务器 B,服务器 B 无该用户的 Session,导致用户需要重新登录,严重影响体验。

4.4.2 解决方案

用 Redis String 作为分布式 Session 的统一存储介质,将所有 Web 服务器的 Session 集中存储在 Redis 中,无论用户的请求分发到哪台 Web 服务器,都从 Redis 中读取、更新 Session,实现 Session 的全局共享。

4.4.3 核心设计思路
  1. Session 集中存储:将用户的 Session 对象序列化为 JSON 字符串,存储到 Redis 中,替代服务器本地内存;
  2. 键名设计 :session:id:${sessionId},其中sessionId为用户的会话标识(如浏览器 Cookie 中的 JSESSIONID);
  3. 设置过期时间:为 Session 设置与原生 Session 一致的过期时间(如 30 分钟),Redis 自动过期清理,无需手动维护;
  4. Session 操作 :Web 服务器的 Session 创建、读取、更新、删除操作,全部转为对 Redis 的SET/GET/DEL操作;
  5. Cookie 传递 :将sessionId通过 Cookie 返回给用户浏览器,用户后续请求携带该 Cookie,Web 服务器通过sessionId从 Redis 中获取 Session。
4.4.4 伪代码实现(Java Web 风格,模拟 Session 操作)
java 复制代码
/**
 * 分布式Session工具类,基于Redis String实现
 */
public class RedisSessionUtil {
    // Redis连接
    private static final Jedis JEDIS = new Jedis("127.0.0.1", 6379);
    // Session过期时间:30分钟(1800秒),与原生Web Session一致
    private static final int SESSION_EXPIRE_SECONDS = 1800;
    // Session键名模板:session:id:${sessionId}
    private static final String SESSION_KEY_TPL = "session:id:%s";

    /**
     * 创建Session:生成sessionId,将Session对象存入Redis
     * @param sessionData Session数据(如用户ID、登录状态)
     * @return sessionId 会话标识
     */
    public static String createSession(Map<String, Object> sessionData) {
        // 1. 生成唯一的sessionId
        String sessionId = UUID.randomUUID().toString().replace("-", "");
        // 2. 将Session数据序列化为JSON字符串
        String sessionJson = JSON.toJSONString(sessionData);
        // 3. 存入Redis并设置过期时间
        String sessionKey = String.format(SESSION_KEY_TPL, sessionId);
        JEDIS.set(sessionKey, sessionJson, SetParams.setParams().ex(SESSION_EXPIRE_SECONDS));
        return sessionId;
    }

    /**
     * 获取Session:通过sessionId从Redis中读取Session数据
     * @param sessionId 会话标识
     * @return Session数据,不存在返回null
     */
    public static Map<String, Object> getSession(String sessionId) {
        if (sessionId == null || sessionId.isEmpty()) {
            return null;
        }
        String sessionKey = String.format(SESSION_KEY_TPL, sessionId);
        String sessionJson = JEDIS.get(sessionKey);
        if (sessionJson == null) {
            return null;
        }
        // 重置Session过期时间(用户活跃时刷新过期时间)
        JEDIS.expire(sessionKey, SESSION_EXPIRE_SECONDS);
        // 反序列化为Map返回
        return JSON.parseObject(sessionJson, new TypeReference<Map<String, Object>>() {});
    }

    /**
     * 更新Session:修改Redis中的Session数据
     * @param sessionId 会话标识
     * @param sessionData 新的Session数据
     * @return 是否更新成功
     */
    public static boolean updateSession(String sessionId, Map<String, Object> sessionData) {
        if (sessionId == null || sessionData == null) {
            return false;
        }
        String sessionKey = String.format(SESSION_KEY_TPL, sessionId);
        if (JEDIS.exists(sessionKey) == 0) {
            return false;
        }
        String sessionJson = JSON.toJSONString(sessionData);
        JEDIS.set(sessionKey, sessionJson, SetParams.setParams().ex(SESSION_EXPIRE_SECONDS));
        return true;
    }

    /**
     * 删除Session:用户退出登录时,从Redis中删除Session
     * @param sessionId 会话标识
     * @return 是否删除成功
     */
    public static boolean deleteSession(String sessionId) {
        if (sessionId == null || sessionId.isEmpty()) {
            return false;
        }
        String sessionKey = String.format(SESSION_KEY_TPL, sessionId);
        return JEDIS.del(sessionKey) > 0;
    }
}

// 业务层使用示例:用户登录创建Session
public String userLogin(String username, String password) {
    // 1. 校验用户名密码(模拟数据库校验)
    if (!"admin".equals(username) || !"123456".equals(password)) {
        return null;
    }
    // 2. 构造Session数据
    Map<String, Object> sessionData = new HashMap<>();
    sessionData.put("userId", 1001L);
    sessionData.put("username", username);
    sessionData.put("loginTime", System.currentTimeMillis());
    sessionData.put("isLogin", true);
    // 3. 创建分布式Session,获取sessionId
    String sessionId = RedisSessionUtil.createSession(sessionData);
    // 4. 将sessionId写入Cookie,返回给浏览器
    // response.addCookie(new Cookie("JSESSIONID", sessionId));
    return sessionId;
}
4.4.5 共享 Session 的关键注意事项
  1. Session 序列化:建议使用 JSON 序列化 Session 对象,避免使用 Java 原生序列化(序列化后字节数大、兼容性差);
  2. 刷新过期时间 :用户每次发起有效请求时,需重置 Session 的过期时间(expire),保证用户活跃时 Session 不会过期;
  3. Cookie 安全 :将存储sessionId的 Cookie 设置为HttpOnly(防止 XSS 攻击)、Secure(仅 HTTPS 传输)、SameSite=Strict(防止 CSRF 攻击);
  4. Session 过期时间:设置合理的过期时间,过短会导致用户频繁重新登录,过长会占用 Redis 内存,建议 30 分钟~2 小时;
  5. 避免大 Session:Session 中仅存储用户 ID、登录状态、权限等核心信息,不存储大体积数据(如用户详细信息),减少 Redis 内存占用和网络传输;
  6. Redis 高可用:Session 存储在 Redis 中,Redis 必须保证高可用(主从复制、哨兵模式、集群),避免 Redis 单点故障导致所有用户 Session 丢失。

4.5 核心场景 5:手机验证码(限流 + 有效期控制)

4.5.1 业务痛点

用户登录 / 注册时的手机验证码功能,存在两个核心问题:

  1. 短信接口限流:若不限制用户获取验证码的频率,恶意用户会频繁请求获取验证码,导致短信接口被刷、短信费用激增;
  2. 验证码有效期:验证码需要设置有效期(如 5 分钟),过期后自动失效,避免验证码被他人盗用。
4.5.2 解决方案

用 Redis String 实现验证码的限流 + 有效期控制:

  1. 频率限流:用SET key 1 NX EX 60限制用户每分钟只能获取 1 次验证码,后续用INCR计数,超过阈值则拒绝请求;
  2. 验证码存储:用SET key 验证码 EX 300将验证码存储到 Redis 中,并设置 5 分钟过期时间,过期后自动失效;
  3. 验证逻辑:用户输入验证码后,从 Redis 中读取验证码进行比对,验证成功后立即删除 Redis 中的验证码(防止重复使用)。
4.5.3 核心设计思路
  1. 限流键名 :sms:limit:${phoneNumber},记录手机号每分钟的获取次数,60 秒过期;
  2. 验证码键名 :sms:code:${phoneNumber},存储手机号对应的验证码,300 秒过期;
  3. 限流规则:每分钟最多获取 5 次验证码,超过则拒绝,防止短信轰炸;
  4. 原子性限流 :用SET NX初始化限流计数,用INCR累加计数,单命令保证原子性;
  5. 验证码一次性:用户验证成功后,立即删除 Redis 中的验证码,避免同一验证码被重复使用。
4.5.4 伪代码实现(Java 风格)
java 复制代码
/**
 * 手机验证码工具类,基于Redis String实现限流+有效期控制
 */
public class SmsCodeUtil {
    private static final Jedis JEDIS = new Jedis("127.0.0.1", 6379);
    // 验证码限流键名模板:sms:limit:${phoneNumber},60秒过期(1分钟)
    private static final String SMS_LIMIT_KEY_TPL = "sms:limit:%s";
    // 验证码存储键名模板:sms:code:${phoneNumber},300秒过期(5分钟)
    private static final String SMS_CODE_KEY_TPL = "sms:code:%s";
    // 每分钟最大获取次数:5次
    private static final int MAX_GET_TIMES_PER_MIN = 5;
    // 限流过期时间:60秒
    private static final int LIMIT_EXPIRE_SECONDS = 60;
    // 验证码过期时间:300秒
    private static final int CODE_EXPIRE_SECONDS = 300;

    /**
     * 发送手机验证码:先限流,再生成验证码,最后存储并发送
     * @param phoneNumber 手机号
     * @return 验证码,限流失败返回null
     */
    public static String sendSmsCode(String phoneNumber) {
        // 1. 校验手机号格式(省略)
        if (phoneNumber == null || phoneNumber.length() != 11) {
            return null;
        }
        String limitKey = String.format(SMS_LIMIT_KEY_TPL, phoneNumber);
        // 2. 初始化限流计数:SET key 1 NX EX 60,仅当key不存在时设置(1分钟内第一次获取)
        String limitInit = JEDIS.set(limitKey, "1", SetParams.setParams().nx().ex(LIMIT_EXPIRE_SECONDS));
        Long currentTimes;
        if ("OK".equals(limitInit)) {
            // 第一次获取,计数为1
            currentTimes = 1L;
        } else {
            // 非第一次获取,计数累加
            currentTimes = JEDIS.incr(limitKey);
            // 3. 超过限流阈值,返回null
            if (currentTimes > MAX_GET_TIMES_PER_MIN) {
                return null;
            }
        }
        // 4. 生成6位随机验证码
        String smsCode = String.format("%06d", new Random().nextInt(999999));
        // 5. 将验证码存入Redis,设置5分钟过期时间
        String codeKey = String.format(SMS_CODE_KEY_TPL, phoneNumber);
        JEDIS.set(codeKey, smsCode, SetParams.setParams().ex(CODE_EXPIRE_SECONDS));
        // 6. 调用短信接口发送验证码(模拟)
        sendSms(phoneNumber, smsCode);
        return smsCode;
    }

    /**
     * 验证手机验证码:比对验证码,验证成功后删除
     * @param phoneNumber 手机号
     * @param inputCode 用户输入的验证码
     * @return 是否验证成功
     */
    public static boolean verifySmsCode(String phoneNumber, String inputCode) {
        // 1. 校验参数
        if (phoneNumber == null || inputCode == null || inputCode.length() != 6) {
            return false;
        }
        String codeKey = String.format(SMS_CODE_KEY_TPL, phoneNumber);
        // 2. 从Redis中获取验证码
        String redisCode = JEDIS.get(codeKey);
        // 3. 验证码不存在/不一致,验证失败
        if (redisCode == null || !redisCode.equals(inputCode)) {
            return false;
        }
        // 4. 验证成功,立即删除验证码(防止重复使用)
        JEDIS.del(codeKey);
        return true;
    }

    // 模拟调用短信接口发送验证码
    private static void sendSms(String phoneNumber, String smsCode) {
        log.info("向手机号{}发送验证码:{}", phoneNumber, smsCode);
        // 调用第三方短信接口:如阿里云短信、腾讯云短信
    }
}

// 业务层使用示例
public static void main(String[] args) {
    // 1. 发送验证码
    String smsCode = SmsCodeUtil.sendSmsCode("13800138000");
    System.out.println("发送的验证码:" + smsCode);
    // 2. 验证验证码(用户输入正确)
    boolean verifySuccess = SmsCodeUtil.verifySmsCode("13800138000", smsCode);
    System.out.println("验证码验证结果:" + verifySuccess);
    // 3. 重复验证(验证码已被删除,验证失败)
    boolean verifyFail = SmsCodeUtil.verifySmsCode("13800138000", smsCode);
    System.out.println("重复验证结果:" + verifyFail);
}
4.5.5 手机验证码的关键注意事项
  1. 验证码格式:建议使用 6 位数字验证码,简单易记,避免字母 + 数字的复杂格式;
  2. 限流规则:根据短信接口的限流能力设置合理的获取频率,建议每分钟≤5 次,每小时≤20 次,防止恶意刷量;
  3. 验证码一次性:验证成功后必须立即删除 Redis 中的验证码,避免同一验证码被他人重复使用;
  4. 短信接口容错:调用第三方短信接口时,需增加重试、熔断机制,避免短信接口故障导致业务阻塞;
  5. 防止验证码泄露:Redis 中存储的验证码无需加密(短期有效,且为随机数),但需保证 Redis 的访问安全,禁止外部网络直接访问;
  6. 空值处理:用户输入的验证码为空、格式错误时,直接返回验证失败,无需访问 Redis。

五、Redis String 类型的避坑指南与性能优化

Redis String 类型虽然简单易用,但在生产环境中,若使用不当,会导致内存占用过高、性能下降、服务阻塞等问题。本节总结了开发中最常见的坑,以及对应的优化方案,帮助你规范使用 String 类型,发挥 Redis 的极致性能。

5.1 坑 1:键名设计不规范,导致键冲突、可维护性差

问题表现

使用无意义的键名(如user1001、code13800138000)、不同业务的键名无区分,导致不同业务的键冲突,后期难以维护和排查问题。

优化方案

遵循统一的键名设计规范,让键名 "见名知意",核心规范:

复制代码
【可选】业务名:对象名:唯一标识[:属性]
  • 业务名:若 Redis 为多业务共享,添加业务名(如order、user、goods),隔离不同业务的键;
  • 对象名:表示键对应的业务对象(如info、stock、play、code);
  • 唯一标识:区分同一对象的不同实例(如用户 ID、商品 ID、手机号);
  • 属性:可选,标识对象的具体属性(如name、age、price)。

规范示例:

  • 正确:user:info:1001、goods:stock:5253、sms:code:13800138000、video:play:10086;
  • 错误:user1001、stock5253、code13800138000、play10086。

额外优化 :若键名过长,可使用团队内部约定的缩写 (如u:info:1001代替user:info:1001),减少键名的内存占用。

5.2 坑 2:存储大字符串,导致内存占用高、网络传输慢

问题表现

将大体积数据(如超过 100KB 的图片、大文本、完整的业务对象)存储为 String 类型,导致:

  1. Redis 内存占用快速飙升,频繁触发内存淘汰,影响性能;
  2. 网络传输大字符串耗时久,接口响应慢;
  3. 单命令执行时间过长,阻塞 Redis 单线程。
优化方案
  1. 拆分大字符串 :将大的结构化对象拆分为多个小的 String 键值对,按需读取,避免一次性传输大数据;
    • 示例:将user:info:1001(存储完整的用户对象 JSON)拆分为user:1001:name、user:1001:age、user:1001:gender;
  2. 避免存储二进制大文件:不要将图片、音频、视频等二进制大文件存储到 Redis 中,建议使用 OSS、MinIO 等对象存储,Redis 仅存储文件的 URL;
  3. 控制字符串大小 :单个 String 值的大小建议不超过 10KB ,高频访问的热点数据建议不超过 1KB;
  4. 使用压缩算法:若必须存储较长的字符串(如大文本),先通过 Gzip、Snappy 等压缩算法压缩后再存储,减少内存占用和网络传输量。

5.3 坑 3:滥用单次操作,忽视批量操作,导致网络 IO 过高

问题表现

需要操作多个 String 键时,使用多次SET/GET命令,而非MSET/MGET,导致多次网络请求,网络 IO 成为性能瓶颈(Redis 的纯内存操作耗时可忽略,性能瓶颈主要在网络)。

优化方案
  1. 优先使用批量操作 :同时操作多个键时,一律使用MSET/MGET,将多次网络请求合并为一次,大幅减少网络 IO 开销;
  2. 控制批量操作的键数 :批量操作的键数不宜过多(建议≤1000 个),否则会导致单个命令执行时间过长,阻塞 Redis 单线程;
  3. 分批批量操作 :若需要操作的键数超过 1000 个,将其拆分为多个批次的MSET/MGET,每批次 1000 个键以内,避免单命令阻塞;
  4. 客户端优化:使用支持 ** 管道(Pipeline)** 的 Redis 客户端(如 Jedis、Redisson),将多个命令通过管道批量发送到 Redis,减少 TCP 握手和挥手的开销。

5.4 坑 4:使用 GET+SET 实现计数,而非原生计数命令,导致计数错误

问题表现

实现计数功能时,先通过GET读取当前值,在客户端进行加减计算,再通过SET写回 Redis,即:

复制代码
// 错误示例:非原子操作,高并发下会导致计数错误
Long count = Long.parseLong(jedis.get("counter"));
count += 1;
jedis.set("counter", count.toString());

高并发下,多个客户端同时GET到相同的计数值,最终写回的结果会覆盖,导致计数少加 / 少减。

优化方案
  1. 使用原生原子计数命令 :实现整型计数用INCR/INCRBY/DECR/DECRBY,实现浮点型计数用INCRBYFLOAT,所有计算在 Redis 服务端完成,单命令保证原子性,无计数错误;
  2. 禁止客户端参与计数计算:计数的核心逻辑必须在 Redis 服务端完成,客户端仅负责发起计数命令和获取结果;
  3. 计数清零用 SET 而非 DEL :需要将计数值清零时,使用SET counter 0,而非DEL counter,避免后续INCR时重新初始化计数值为 0,导致计数逻辑异常。

5.5 坑 5:未设置过期时间,导致缓存脏数据、内存泄漏

问题表现

使用 String 类型做缓存时,未设置过期时间(EX/PX),导致:

  1. 缓存脏数据:数据库中的数据更新后,Redis 中的缓存数据未及时更新,长期有效,导致用户读取到旧数据;
  2. 内存泄漏:Redis 中的缓存数据永久有效,内存占用持续飙升,最终触发内存淘汰,甚至导致 Redis 服务崩溃。
优化方案
  1. 为所有缓存键设置过期时间 :使用SET key value EX/PX或EXPIRE为缓存键设置合理的过期时间,避免缓存永久有效;
  2. 设置随机过期时间 :多个同类缓存键的过期时间建议增加随机偏移量 (如 3600±60 秒),避免大量缓存键在同一时间过期,导致缓存雪崩 (大量缓存同时失效,请求全部涌入数据库);
    • 示例:jedis.set(key, value, SetParams.setParams().ex(3600 + new Random().nextInt(120)));
  3. 主动删除脏缓存 :数据库中的数据更新 / 删除时,立即通过DEL命令删除 Redis 中对应的缓存键,而非更新缓存,避免缓存与数据库不一致;
  4. 使用过期策略:根据业务特性设置合理的过期策略,热点数据设置较长的过期时间(如 1 小时),冷数据设置较短的过期时间(如 10 分钟)。

5.6 坑 6:忽视内部编码,导致内存利用率低

问题表现

不了解 Redis String 的内部编码规则,导致 Redis 无法使用最优的编码(int/embstr),而是使用通用的 raw 编码,内存利用率低。

优化方案
  1. 计数场景用整型:计数功能的键值直接存储为整型,让 Redis 使用 int 编码,极致节省内存;
  2. 控制短字符串长度 :高频访问的短字符串尽量控制在39 字节以内,让 Redis 使用 embstr 编码,提升缓存命中率和操作性能;
  3. 避免频繁修改短字符串:embstr 编码的字符串修改后会转为 raw 编码,且不可逆,若需要频繁修改字符串,提前考虑使用 raw 编码,避免编码转换的开销;
  4. 批量初始化短字符串 :对多个短字符串进行初始化时,使用MSET,减少 Redis 的内存分配开销。

5.7 坑 7:使用非原子命令实现分布式锁,导致死锁、误删锁

问题表现

实现分布式锁时,使用SETNX+EXPIRE(非原子)加锁,使用GET+DEL(非原子)解锁,导致:

  1. 死锁 :SETNX加锁后,服务挂掉,未执行EXPIRE,锁永久有效;
  2. 误删锁 :GET读取锁后,锁过期,其他实例获取了锁,当前实例执行DEL,删除了其他实例的锁。
优化方案
  1. 原子性加锁 :一律使用SET key value NX EX seconds单命令加锁,原子性完成加锁 + 设置过期时间,避免死锁;
  2. 原子性解锁 :使用Lua 脚本实现解锁,先判断锁的 value 是否为当前实例的唯一标识,再删除,避免误删锁;
  3. 设置合理的锁过期时间:过期时间需大于业务的最大执行耗时,避免业务未执行完锁已过期;
  4. 使用成熟的锁框架:若需要更复杂的分布式锁(如重入锁、公平锁、红锁),直接使用 Redisson 框架,无需手动实现,避免造轮子。

5.8 坑 8:滥用 APPEND/SETRANGE,导致 embstr 转为 raw

问题表现

对 embstr 编码的短字符串频繁执行APPEND/SETRANGE等修改操作,导致 embstr 编码转为 raw 编码,失去 embstr 的性能优势(连续内存、高缓存命中率)。

优化方案
  1. 避免频繁修改短字符串:若字符串需要频繁修改,提前规划为 raw 编码,无需追求 embstr;
  2. 一次性设置字符串 :尽量在SET时直接设置完整的字符串,避免后续通过APPEND拼接;
  3. 拆分修改操作 :若需要修改字符串的部分内容,可将字符串拆分为多个小字符串,修改单个小字符串,而非通过SETRANGE修改原字符串。

六、Redis String 类型的核心总结

Redis String 类型作为最基础、最核心、使用频率最高的数据类型,是 Redis 的入门必修课,也是后续学习 list、hash、set、zset 的基础。

6.1 基础特性:掌握本质,避开认知误区

  1. Redis String 是二进制安全的动态字节数组,非普通编程语言的字符串,支持存储字符串、数字、二进制流,最大容量 512MB;
  2. Redis 中所有键的类型都是 String,其他数据结构的元素也均为 String;
  3. Redis 不处理字符集编码,客户端用什么编码存入,就用什么编码读取,乱码问题由客户端解决;
  4. String 支持数字原生原子操作,是实现计数功能的核心优势。

6.2 核心命令:按模块记忆,按需使用

将 String 命令分为4 大模块,重点掌握高频命令和核心选项:

  1. 基础操作 :SET(NX/XX/EX/PX 选项)、GET、DEL,SET的选项是实现分布式锁、缓存的核心;
  2. 批量操作 :MSET、MGET,减少网络 IO,提升性能,核心优化技巧;
  3. 计数操作 :INCR/INCRBY/DECR/DECRBY/INCRBYFLOAT,原子计数,高并发下无计数错误;
  4. 字符串操作 :APPEND、STRLEN、GETRANGE、SETRANGE,服务端原生操作,减少客户端开销。

6.3 内部编码:智能优化,透明适配

Redis 根据存储内容自动选择3 种内部编码,单向不可逆,平衡内存和性能:

  1. int:8 字节 64 位有符号整型,极致节省内存,适合计数场景;
  2. embstr:≤39 字节的短字符串,连续内存存储,高缓存命中率,不可修改;
  3. raw:>39 字节的长字符串或可修改的短字符串,灵活存储,支持动态扩容。

可通过object encoding key命令查看键的内部编码,开发中无需手动干预,只需根据编码规则优化数据存储。

6.4 实战场景:覆盖 90% 的互联网业务

String 类型的实战场景覆盖了绝大多数互联网业务,核心 5 大场景必须掌握:

  1. 缓存层:最经典场景,加速读写,降低数据库压力,核心是缓存命中 / 未命中逻辑 + 过期时间;
  2. 计数功能:视频播放量、接口访问量、点赞数,原生原子计数命令,高性能;
  3. 分布式锁 :SET key value NX EX seconds+Lua 脚本解锁,简单高效,解决分布式并发竞争;
  4. 共享 Session:分布式 Web 服务的统一会话,解决 Session 分散问题,提升用户体验;
  5. 手机验证码:限流 + 有效期控制,防止短信轰炸,保证验证码安全。

6.5 避坑优化:规范使用,发挥极致性能

生产环境中,String 类型的坑主要集中在键名设计、数据大小、网络 IO、原子操作、过期时间五个方面,核心优化原则:

  1. 键名设计遵循规范,见名知意,隔离业务;
  2. 控制 String 值的大小,避免存储大字符串和二进制大文件;
  3. 优先使用批量操作和管道,减少网络 IO;
  4. 计数、锁等场景使用 Redis 原生原子命令,避免客户端非原子操作;
  5. 所有缓存键必须设置过期时间,增加随机偏移量,避免缓存雪崩;
  6. 了解内部编码规则,按需优化数据存储,提升内存利用率。

结语

到这里,Redis String 类型的全方位解析就圆满结束了。作为 Redis 最基础、最核心、使用频率最高的数据类型,String 看似简单,却藏着 Redis 设计的精髓 ------以二进制安全的动态字节数组为底层,用智能内部编码平衡内存与性能,靠原子命令支撑高并发场景。

回顾整篇内容,我们从 "二进制安全""键固定为 String" 等基础特性入手,拆解了四大类核心命令(基础增删查改、批量操作、原子计数、字符串操作),深入剖析了 int/embstr/raw 三种内部编码的适配逻辑,最后落地到缓存、计数、分布式锁、共享 Session、手机验证码五大实战场景,还总结了 8 个高频避坑点。这些内容层层递进,本质上是在传递一个核心思想:Redis String 的强大,在于它的 "灵活与极致"------ 既能存储文本、数字、二进制等任意数据,又能通过内部优化和原子命令,在高并发场景下保持高性能。

对开发者而言,String 类型的学习价值不仅在于 "会用命令",更在于 "理解设计逻辑":比如内部编码的自动切换,是 Redis "场景化优化" 的体现;批量操作和原子命令,是 Redis 应对高并发的核心手段;而避坑指南中的规范(如键名设计、过期时间设置),则是生产环境稳定运行的保障。这些逻辑和规范,同样适用于后续其他数据结构的学习。

String 类型是 Redis 学习之路的 "基石",学好它,你已经掌握了 Redis 80% 的常用场景。接下来,我们将陆续学习 list、hash、set、zset 等其他数据结构,它们本质上都是基于 String 构建的,很多命令逻辑和设计思想可以直接复用。建议你在学习后续内容时,多回头对比 String 类型的特性,比如 hash 与 String 存储结构化数据的差异、zset 与 String 计数功能的互补,这样能更快形成完整的 Redis 知识体系。

最后再叮嘱一句:Redis 的学习离不开实操。建议你把文中的命令、场景案例在自己的 Redis 环境中反复演练,尤其是原子计数、分布式锁、批量操作这些核心功能,亲手验证内部编码的转换、感受批量操作的性能优势、踩一踩未设置过期时间的坑,才能真正把知识内化为能力。

String 类型的学习是 Redis 探索之旅的重要一站,它让我们看到 "基础组件" 如何通过优秀设计支撑起复杂业务。接下来,让我们带着对 String 类型的理解,继续深入 Redis 其他数据结构的世界,解锁更多高性能场景的解决方案,我们下次博客再见~

相关推荐
YYYing.2 小时前
【设计模式系列 (七) 】桥接模式
c++·后端·设计模式·桥接模式
高山有多高3 小时前
【Linux笔记】Linux自定义Shell
linux·c++
三8443 小时前
Fastjson 漏洞 · 02 · autoType 机制与 checkAutoType
java·fastjson
悠仁さん3 小时前
【C++】vector 类
开发语言·c++
_upupup3 小时前
智能指针的原理及简单模拟实现(C++)
开发语言·c++
l1t3 小时前
DeepSeek总结的OpenZL压缩变换器
c++·算法
无限码力3 小时前
小红书笔试真题 9.17 - 待发热度重排(C++/Py/Java /Js/Go)
java·小红书·小红书笔试真题·小红书技术岗笔试真题
CoLiuRs4 小时前
电商价格是怎样算出来的
数据库·redis·缓存