
理解:字符串对象是 Redis 中最基本的数据类型,也是我们工作中最常用的数据类型。redis中的键都是字符串对象,而且其他几种数据结构都是在字符串对象基础上构建的。字符串对象的值实际可以是字符串、数字、甚至是二进制,最大不能超过512MB

Key:所有 key 都是二进制安全的字符串(binary-safe string)。可以包含任意字符(包括中文、空格、特殊符号等),例如:
SET user:1:name "张三"
SET log:2024-01-01 "\x01\x02\x03"Value:虽然 Redis 的底层统一使用 robj(Redis Object)来表示对象,但 value 可以是:string、list、hash、set等
⚠️ 但所有这些数据结构,其“元素”本质上也都是字符串,比如列表中的每个元素是字符串,集合中的每个成员也是字符串
🔥 由于Redis内部存储字符串完全是按照 二进制流 的形式保存的,所以Redis是 不处理字符集编码问题 的,客户端传入的命令中使用的是什么字符集编码,就存储什么字符集编码
① 二进制安全(Binary Safe)
"hello"、"你好"、"{"name":"Tom"}"123, 3.14② Redis 不处理字符集编码
❗ 因此,在使用 Redis 存储文本数据时,务必确保客户端使用的字符集与读取时一致,否则会出现乱码(避免乱码)

数据类型 | 示例 | 特点 |
|---|---|---|
文本数据 | "Hello World","{\"id\":1}" | 可以是普通文本或 JSON/XML 等结构化文本 |
数字 | 123,3.14 | Redis 自动识别为整数或浮点数,并优化存储为int编码 |
二进制数据 | 图片、视频、序列化对象 | Redis 会以原始字节形式存储 |
对比项 | Redis | MySQL |
|---|---|---|
字符集处理 | 不处理字符集,原样存储 | 默认使用特定字符集(如 latin1 或 utf8mb4) |
编码转换 | 不做任何转换 | 插入/查询时可能自动进行编码转换 |
乱码问题 | 客户端控制,Redis 不参与 | 如果配置不当容易出现乱码 |
二进制存储 | 支持任意二进制数据 | BLOB 类型可存二进制,但操作不如 Redis 简便 |
最大容量 | 单个 value 最大 512MB | TEXT/LONGTEXT/BLOB 有大小限制,但通常更大 |
Redis 是单线程模型(核心命令处理)
⚠️ 不建议:
KEYS *、SMEMBERS 等)⛵️ 将 string 类型的 value 设置到key中。**如果key之前存在,则覆盖,无论原来的数据类型是什么。**之前关于此key的TTL也全部失效。命令如下:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]SET 命令支持多种选项来影响其行为,如下:
EX seconds:以秒为单位,设置超时时间PX milliseconds:以毫秒为单位,设置超时时间NX:只有key不存在才设置,如果存在返回 nilXX:只有key存在就更新,如果不存在返回 nil注意:由于带选项的SET命令可以被 SETNX 、 SETEX 、 PSETEX 等命令代替,所以之后的版本中,Redis可能进行合并
示例
127.0.0.1:6379> set mykey "Hello"
OK
127.0.0.1:6379> get mykey
"Hello"
127.0.0.1:6379> set mykey "World" NX
(nil)
127.0.0.1:6379> set mykey "World" XX
OK
127.0.0.1:6379> ttl mykey
(integer) -1
127.0.0.1:6379> set mykey "Island" EX 10
OK
127.0.0.1:6379> ttl mykey
(integer) 8获取key对应的value。如果key不存在,返回nil。如果value的数据类型不是string,会报错。
127.0.0.1:6379> hset mykey name Bob
(integer) 1
127.0.0.1:6379> get mykey
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> del mykey
(integer) 1
127.0.0.1:6379> set mykey Bob
OK
127.0.0.1:6379> get mykey
"Bob"⼀次性获取多个key的值。如果对应的key不存在或者对应的数据类型不是string,返回 nil,语法如下:
MGET key [key ...]示例
127.0.0.1:6379> sey key1 1
(error) ERR unknown command `sey`, with args beginning with: `key1`, `1`,
127.0.0.1:6379> set key1 1
OK
127.0.0.1:6379> set key2 2
OK
127.0.0.1:6379> MGET key1 key2 key3
1) "1"
2) "2"
3) (nil)⼀次性设置多个key的值。语法如下:
MSET key value [key value ...]示例
127.0.0.1:6379> MSET key1 1 key2 2
OK
127.0.0.1:6379> MGET key1 key2
1) "1"
2) "2"多次 GET 和单次MGET比较:使用 MGET/MSET 可有效减少网络时间,性能较高

结论:学会使用批量操作,可以有效提高业务处理效率
Redis 阻塞下面将介绍 2 个针对 set 的一些常见用法, 进行了缩写.
setnx
命令:setnx key value
功能:如果键不存在,则设置键值对。可以理解为 no exist 设置~
示例:
setnx key1 value1 # 返回 0,因为 key1 已存在setex
命令:setex key seconds value
功能:设置键值对并指定过期时间(秒)。
示例:
setex key4 10 value4
ttl key4 # 返回剩余时间由于string内部还可以存储数字,所以Redis还提供了数字操作的命令。时间复杂度:O(1)
命令:incr key
功能:将键的值加1,如果键不存在则创建键并初始化为0。如果 key 对应的string不是⼀个整型或者范围超过了64位有符号整型,则报错。
示例
127.0.0.1:6379> set key bar
127.0.0.1:6379> incr key # 非整形
(error) ERR value is not an integer or out of range
127.0.0.1:6379> del key
127.0.0.1:6379> set key 1
127.0.0.1:6379> incr key # 存在
(integer) 2
127.0.0.1:6379> del key
127.0.0.1:6379> incr key # 不存在
(integer) 1命令:incrby key increment
功能:和 INCR 使用类似,将键的值增加指定的整数。
示例:
incrby key2 7 # 返回 8命令:deby key
功能:和 INCR 使用类似,将键的值减1,如果键不存在则创建键并初始化为0。
示例:
set key2 8
decr key2 # 返回 7命令:decrby key decrement
功能:将键的值减少指定的整数。
示例:
set key2 8
decrby key2 2 # 返回 6命令:incrbyfloat key increment
功能:将键的值增加指定的浮点数(允许采用 科学计数法 表示浮点数)
示例
set key1 1
INCRBYFLOAT key1 0.5 # 返回 1.5注意:
很多存储系统和编程语言内部使用 CAS 机制实现计数功能,会有⼀定的CPU开销
命令:append key value
功能:如果key已经存在并且是⼀个string,命令会将value追加到原有string的后边。如果key不存在,则效果等同于SET命令(返回追加完成之后string的长度)
示例:
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"
127.0.0.1:6379> append mykey " World"
(integer) 11
127.0.0.1:6379> get mykey
"Hello World"在启动 redis 客户端的时候,加上一个 --raw 这样的选项就可以使 redis 客户端能够自动的把二进制数据尝试翻译
演示如下:
127.0.0.1:6379> set name "张三"
OK
127.0.0.1:6379> get name
"\xe5\xbc\xa0\xe4\xb8\x89"
lighthouse@VM-8-10-ubuntu:~$ redis-cli --raw
127.0.0.1:6379> get name
张三命令:getrange key start end(左闭右闭,[0, len - 1])
功能:获取键值在指定范围内的子字符串。
注意:
示例:
127.0.0.1:6379> get mykey
Hello World
127.0.0.1:6379> getrange mykey 0 3
Hell
127.0.0.1:6379> getrange mykey -3 -1
rld
127.0.0.1:6379> getrange mykey 0 -1
Hello World
127.0.0.1:6379> getrange mykey 20 100
127.0.0.1:6379> getrange mykey 5 10
World命令:setrange key offset value
功能:从指定偏移量开始设置键值的一部分。返回 string 长度
注意:针对不存在的key,也可以操作,不过会把offset之前的内容填充成 0x00
127.0.0.1:6379> get mykey
Hello World
127.0.0.1:6379> setrange mykey 6 "Redis"
11
127.0.0.1:6379> get mykey
Hello Redis命令:strlen key
功能:获取键值的字节长度。
127.0.0.1:6379> strlen mykey
11
127.0.0.1:6379> strlen non
0下表是字符串类型命令的效果、时间复杂度,开发人员可以参考此表,结合自身业务需求和数据大小选择合适的命令。

Redis字符串对象底层的数据结构实现主要是int和简单动态字符串SDS(这个字符串,和我们认识的C字符串不太一样,其通过不同的编码方式映射到不同的数据结构

字符串对象的内部编码有3种 :int、raw和embstr。Redis会根据当前值的类型和长度来决定使用哪种编码来实现。
默认情况下,值以字符串形式传入,如果Redis检测到字符串为数字,则转换为int存储,从而节省空间。例如,字符串"1234567890"若作为字符串存储需要10字节,而转换为int后仅需8字节
① int:当存储的值为整数,且值的大小可以用 long 类型表示时,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成1ong),并将字符串对象的编码设置为int

② raw:当存储的值为字符串,且长度小于等于 44 字节时,Redis 使用 raw 编码。在 raw 编码中,String 对象的实际值会被存储在一个简单的 字符串对象(SDS) 中,该对象包含了字符串的长度和字符数组的指针。

③ embstr:当存储的值为字符串,且长度大于 44 字节时,Redis 使用 embstr 编码。在 embstr 编码中,String 对象的实际值会被存储在一个特殊的字符串对象中,该对象包含了字符串的长度和字符数组的指针,但是不包含额外的空间。

embstr 编码 是专门用于保存短字符串的一种优化编码方式,我们可以看到embstr和raw编码都会使用SDS来保存值,但不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS。而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS。Redis这样做会有很多好处。
embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次embstr编码的字符串对象同样只需要调用一次内存释放函数embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用CPU缓存提升性能。明明没有超过阈值,为什么变成raw?
embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先 转化为 raw 再进行修改。因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44 个字节。Redis中根据数据类型和长度来使用不同的编码和数据结构存储存在于Redis中的每一种对象类型上。其这种小细节上的优化令我叹服不止,后续我们会看到Redis中到处都是这种内存与性能上的小细节优化!
Redis会根据当前值的类型和长度 动态决定使用哪种内部编码实现
# 整形
> set key 2333
OK
> object encoding key
"int"
# 短字符串
> set key "hello"
OK
> object encoding key
"embstr"
# ⼤于39个字节的字符串
> set key "one string greater than 39 bytes ........"
OK
> object encoding key
"raw思考:
上述效果具体怎么实现?
为啥很多大厂,往往是自己造轮子,而不是直接使用业界成熟的呢?
🌤 Redis默认并未直接使用C字符串(C字符串仅仅作为字符串字面量,用在一些无需对字符串进行修改的地方,如打印日志)。而是以Struct的形式构造了一个SDS的抽象类型。当Redis需要一个可以被修改的字符串时,就会使用SDS来表示。在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的 即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)

Copystruct sdshdr{
//int 记录buf数组中未使用字节的数量 如上图free为0代表未使用字节的数量为0
int free;
//int 记录buf数组中已使用字节的数量即sds的长度 如上图len为5代表未使用字节的数量为5
int len;
//字节数组用于保存字符串 sds遵循了c字符串以空字符结尾的惯例目的是为了重用c字符串函数库里的函数
char buf[];
}
上图表示了SDS与C字符串的区别,关于为什么Redis要使用SDS而不是C字符串,我们可以从以下几个方面来分析。

⛽️ C字符串,如果程序员在字符串修改的时候如果忘记给字符串重新分配足够的空间,那么就会发生内存溢出,如上图所示,忘记给s1分配足够的内存空间,s1的数据就会溢出到s2的空间, 导致s2的内容被修改。而Redis提供的SDS其内置的空间分配策略则可以完全杜绝这种事情的发生。当API需要对SDS进行修改时,API会首先会检查SDS的空间是否满足条件,如果不满足, API会自动对它动态扩展, 然后再进行修改。

C字符串内存重分配
在C字符串中,如果对字符串进行修改,那么我们就不得不面临内存重分配。因为C字符串是由一个N+1长度的数组组成,如果字符串的长度变长,我们就必须对数组进行扩容,否则会产生内存溢出。而如果字符串长度变短,我们就必须释放掉不再使用的空间,否则会发生内存泄漏。
SDS空间分配策略
对于Redis这种具有高性能要求的内存数据库,如果每次修改字符串都要进行内存重分配,无疑是巨大的性能损失。而Redis的SDS提供了两种空间分配策略来解决这个问题。


Redis通过空间预分配和惰性空间释放策略在字符串操作中一定程度上减少了内存重分配的次数。但这种策略同样会造成一定的内存浪费,因此Redis SDS API提供相应的API让我们在有需要的时候真正的释放SDS的未使用空间。
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。如果有一种使用空字符来分割多个单词的特殊数据格式,就不能用C字符串来表示,如"Redis\0String",C字符串的函数会把’\0’当做结束符来处理,而忽略到后面的"String"。而SDS的buf字节数组不是在保存字符,而是一系列二进制数组,SDS API都会以二进制的方式来处理buf数组里的数据,使用len属性的值而不是空字符来判断字符串是否结束。
我们来看几个Redis常见操作的时间复杂度。
Redis在获取字符串长度上的时间复杂度为常数级O(1)
通过以上分析,我们可以得到,SDS这种数据结构相对于C字符串有以下优点:
Redis定位于一个高性能的内存数据库,其面向的就是大数据量,大并发,频繁读写,高响应速度的业务。因此在保证安全稳定的情况下,性能的提升非常重要。而SDS这种数据结构屏蔽了C字符串的一些缺点,可以提供安全高性能的字符串操作。
String 类型的具体应用场景
下面是一个比较典型的缓存使用场景,其中 Redis 作为缓冲层,MySQL 作为存储层,大多数请求的数据从 Redis 中获取。由于 Redis 支持 高并发 的特性,所以缓存通常能 加速读写 和 降低后端压力 的作用
Redis+MySQL 组成的缓存存储架构

模拟业务数据访问过程
// 1. 根据用户 Uid 获取用户信息
UserInfo GetUserInfo(long uid) {
// 2. 从 Redis 获取用户信息, 假设 信息保存在 user:info 对应键中
// 根据 uid 得到 Redis 的键
String key = "user:info:" + uid;
// 尝试从 Redis 中获取对应的值
String value = Redis 执行命令:get key;
// 3. 如果没有从Redis中得到⽤⼾信息,及缓存miss,则进⼀步从MySQL中获取对应的信息,随后写⼊缓存并返回
// 如果缓存命中 (hit)
if (value != null) {
// 假设用户信息按照 JSON 格式存储
UserInfo userInfo = JSON 反序列化 (value);
return userInfo;
}
// 如果缓存未命中 (miss)
if (value == null) {
// 从数据库中,根据 uid 获取用户信息
UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>;
// 如果表中没有 uid 对应的用户信息
if (userInfo == null) {
响应 404;
return null;
}
// 将用户信息序列化成 JSON 格式
String value = JSON 序列化 (userInfo);
// 写入缓存,为了防止数据腐烂 (rot),设置过期时间为 1 小时 (3600 秒)
Redis 执行命令:set key value ex 3600;
// 返回用户信息
return userInfo;
}
}通过增加缓存功能,在理想情况下,每个用户信息,⼀个小时期间只会有⼀次MySQL查询,极大地提升了查询效率,也降低了MySQL的访问数
注意:Redis 没有表、字段等命名空间,键名没有强制要求(除了一些特殊字符)
设计合理的键名,有利于防止键冲突和项目的可维护性。推荐使用 “业务名:对象名:唯一标识:属性” 作为键名。
思考:Redis 缓存策略
存在一个明显的问题:随着时间的推移,肯定会有越来越多的 key 在 redis 上访问不到,从而从 mysql 读取并写入 redis 了。此时 redis 中的数据是不是就越来越多嘛??
计数器\限速器\分布式ID等主要是利用Redis字符串自增自减的特性。
如下:视频网站的视频播放次数可以使用 Redis 来完成:用户每播放⼀次视频,相应的视频播放数就会自增 1

示例:统计视频播放次数
long IncrVideoCounter(long vid) {
String key = "video:" + vid;
long count = Redis 执行命令:incr key;
return count;
}会话的概念:客户端和服务端在交互过程中产生的专属于该客户端的中间状态数据。
Cookie 和 Session
Session ID 一般保存在 Cookie 中,客户端每次请求都会携带这个 Session ID(通过 Cookie),服务器根据 Session ID 查找对应的服务端 Session 数据
⌚️ 通常在单体系统中,Web服务将会用户的Session信息(例如用户登录信息)保存在自己的服务器中。但是在分布式系统中,这样做会有问题。因为分布式系统通常有很多个服务,每个服务又会同时部署在多台机器上,通过负载均衡机制将将用户的访问均衡到不同服务器上。这个时候用户的请求可能分发到不同的服务器上,从而导致用户登录保存Session是在一台服务器上,而读取Session是在另一台服务器上因此会读不到Session。
实际案例理解:医院就诊
问题:同一个客户端多次访问可能遇到不同的服务器。解决方案如下:
Session 分散存储 图如下:

会话管理的重要性
解决方案:这种问题通常的做法是把Session存到一个公共的地方,让每个Web服务,都去这个公共的地方存取Session。而Redis就可以是这个公共的地方。(数据库、memecache等都可以各有优缺点)。
Redis 集中管理 Session


此功能可以用以下伪代码说明基本实现思路:
String SendCapcha(String phoneNumber) {
String key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟
// 使用 NX,只在不存在 key 时才能设置成功
bool r = Redis 执行命令:set key ex 60 nx;
if (r == false) {
// 说明之前设置过该手机的验证码了
long c = Redis 执行命令:incr key;
if (c > 5) {
// 说明超过一分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
String validationCode = 生成随机的 6 位数的验证码();
String validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟内有效
Redis 执行命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过手机短信发送给用户
return validationCode;
}
// 验证用户输入的验证码是否正确
bool VerifyCode(String phoneNumber, String validationCode) {
String validationKey = "validation:" + phoneNumber;
String value = Redis 执行命令:get validationKey;
if (value == null) {
// 说明没有这个手机的验证码记录,验证失败
return false;
}
if (value.equals(validationCode)) {
return true;
} else {
return false;
}
}🏑 小结:Redis 的 String 是一种二进制安全的字符串类型,支持任意格式的数据存储(文本、数字、二进制),不处理字符集编码,适用于缓存、计数器、Session 存储等多种场景,但在使用时应注意控制数据大小和字符集一致性,以保证性能和正确性。