引言
String 是 Redis 里最基础、也是用得最频繁的数据类型。几乎每个系统的第一个 Redis 用法,都是拿它来"缓存一个值"。但如果你对 String 的理解只停留在 SET / GET,那就太可惜了------它不仅能存文本,还能做计数器、做分布式锁、做限流、做验证码,背后还藏着精心设计的底层结构。
这篇文章我会把 Redis 的 String 类型彻底拆开:先看它"是什么"和底层结构,再逐个讲常用命令,然后讲它的三种内部编码,最后用五个真实场景演示它到底能干什么。每个知识点都配命令示例和 Java 代码。
一、String 是什么
Redis 的 String 类型是"一个 Key 对应一个 Value"的简单映射,Value 是一个字符串。但它"字符串"的概念比你想的更宽:
- 可以是普通文本(
"hello") - 可以是数字(
100,还能自增) - 可以是序列化后的 JSON(存对象)
- 可以是二进制数据(如图片,最大 512MB)
一句话:**Redis 的 String 是"二进制安全"的,它不关心内容是什么,原样存取。** 这也是它和 Memcached 等普通 KV 缓存相比更灵活的地方。
二、底层结构:SDS(简单动态字符串)
Redis 自己实现了一种字符串结构 SDS(Simple Dynamic String,简单动态字符串) ,而不是直接使用 C 语言的 char[]。这是 Redis 高性能、高安全性的重要基础。
SDS 相比 C 字符串的几个关键改进:
- O(1) 获取长度 :C 字符串要遍历到
\0才知道长度(O(n)),SDS 用len字段直接记录长度,STRLEN命令秒回。 - 杜绝缓冲区溢出:C 字符串拼接时如果不检查空间会溢出,SDS 在修改前会先检查并自动扩容。
- 减少内存重分配:SDS 采用"空间预分配"和"惰性释放"策略------扩容时多分配一些空间,缩短时先不释放,避免频繁的内存分配/回收。
- 二进制安全 :C 字符串遇到
\0就认为结束,无法存二进制;SDS 以len为准,可以安全存任意二进制内容。
这些细节解释了为什么 Redis 的 String 操作既快又稳。
三、常用命令详解
3.1 基础读写
plaintext
127.0.0.1:6379> SET name zhangsan
OK
127.0.0.1:6379> GET name
"zhangsan"
127.0.0.1:6379> STRLEN name # 字符串长度
(integer) 8
127.0.0.1:6379> DEL name # 删除
(integer) 1
3.2 批量与追加
plaintext
127.0.0.1:6379> MSET a 1 b 2 c 3 # 批量设置
OK
127.0.0.1:6379> MGET a b c # 批量获取
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> APPEND name "hello" # 追加
(integer) 13
3.3 数字运算(计数器核心)
plaintext
127.0.0.1:6379> SET count 10
OK
127.0.0.1:6379> INCR count # 原子自增 1
(integer) 11
127.0.0.1:6379> INCRBY count 5 # 自增指定值
(integer) 16
127.0.0.1:6379> DECR count # 原子自减 1
(integer) 15
127.0.0.1:6379> DECRBY count 5 # 自减指定值
(integer) 10
INCR 系列是原子操作,即使多个客户端同时执行也不会出现"丢计数"的问题,这是做计数器的关键。
3.4 带过期时间的设置
plaintext
127.0.0.1:6379> SETEX code 60 123456 # 设置 60 秒后过期
OK
127.0.0.1:6379> TTL code # 查看剩余过期时间(秒)
(integer) 55
127.0.0.1:6379> SET name zhangsan EX 60 NX # 组合参数:60秒过期 + 不存在才设置
3.5 条件设置(分布式锁基础)
plaintext
127.0.0.1:6379> SETNX lock 1 # 不存在才设置,返回 1;存在返回 0
(integer) 1
127.0.0.1:6379> SETNX lock 2
(integer) 0 # 已存在,设置失败
SETNX(SET if Not eXists)是分布式锁的基石,配合过期时间就能防止死锁。
3.6 子串与覆盖
plaintext
127.0.0.1:6379> SET hello world
OK
127.0.0.1:6379> GETRANGE hello 0 2 # 取子串
"wor"
127.0.0.1:6379> SETRANGE hello 0 W # 覆盖指定位置
(integer) 5
127.0.0.1:6379> GET hello
"World"
四、String 的三种内部编码
Redis 会根据字符串的长度和内容,自动选择最省内存的内部编码:
| 编码 | 触发条件 | 特点 |
|---|---|---|
| int | 值是整数,且能用 long 表示 | 直接存整数,最省内存 |
| embstr | 长度 ≤ 44 字节 | 对象和内容连续分配,一次内存分配 |
| raw | 长度 > 44 字节 | 对象和内容分开分配 |
可以用 OBJECT ENCODING 命令查看:
plaintext
127.0.0.1:6379> SET num 100
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"
127.0.0.1:6379> SET short hello
OK
127.0.0.1:6379> OBJECT ENCODING short
"embstr"
了解编码有助于理解 Redis 的内存优化思路,面试也常被问到。
五、五大实战场景
5.1 缓存对象(JSON)
把对象序列化成 JSON 存进 String,是最常见的用法:
java
public void cacheUser(StringRedisTemplate redis, User user) {
// 存:对象转 JSON
redis.opsForValue().set("user:" + user.getId(),
JSON.toJSONString(user), 30, TimeUnit.MINUTES);
// 取:JSON 转对象
String json = redis.opsForValue().get("user:" + user.getId());
User cached = JSON.parseObject(json, User.class);
}
5.2 计数器(浏览量、点赞数)
java
// 原子自增,天然线程安全
Long views = redis.opsForValue().increment("article:100:views");
相比"先读出来 +1 再写回去"的写法,INCR 一步到位且不会并发丢失。
5.3 分布式锁
java
public boolean tryLock(StringRedisTemplate redis, String key, String value, long expireSeconds) {
// SET key value NX EX expireSeconds:不存在才设置 + 设置过期时间
Boolean success = redis.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void unlock(StringRedisTemplate redis, String key, String value) {
// 释放锁:校验是自己的锁再删(防止误删别人的锁,正式场景建议用 Lua 保证原子性)
if (value.equals(redis.opsForValue().get(key))) {
redis.delete(key);
}
}
5.4 简单限流
利用 INCR + 过期时间,实现"某个 Key 在一段时间内最多访问 N 次":
java
public boolean isAllowed(StringRedisTemplate redis, String userId, int maxCount, long windowSeconds) {
String key = "rate:" + userId;
Long count = redis.opsForValue().increment(key);
// 第一次访问时设置过期窗口
if (count != null && count == 1) {
redis.expire(key, windowSeconds, TimeUnit.SECONDS);
}
return count != null && count <= maxCount;
}
5.5 验证码 / Session 共享
java
// 存验证码,5 分钟过期
redis.opsForValue().set("sms:" + phone, code, 5, TimeUnit.MINUTES);
// 校验
String saved = redis.opsForValue().get("sms:" + phone);
boolean ok = code.equals(saved);
六、命令速查表
| 命令 | 作用 |
|---|---|
| SET / GET | 设置 / 获取 |
| SETNX | 不存在才设置 |
| SETEX | 设置并指定过期时间 |
| MSET / MGET | 批量设置 / 获取 |
| APPEND | 追加 |
| INCR / DECR / INCRBY / DECRBY | 原子自增 / 自减 |
| STRLEN | 获取长度 |
| GETRANGE / SETRANGE | 取子串 / 覆盖 |
| GETSET | 设置新值并返回旧值 |
| TTL | 查看剩余过期时间 |
总结
Redis 的 String 类型看似简单,实则内涵丰富:
- 底层是 SDS ,用
len记录长度实现 O(1) 取长、自动扩容防溢出、预分配减少重分配、二进制安全。 - 命令不止 SET/GET :
INCR系列做计数器、SETNX+SETEX做分布式锁、MSET/MGET做批量、APPEND/GETRANGE做字符串操作。 - 三种内部编码:int、embstr、raw,Redis 会自动按长度选最省内存的编码。
- 五大场景:缓存对象、计数器、分布式锁、限流、验证码/Session,都是生产环境的高频用法。
一句话总结:**String 是 Redis 的"万金油"------一个 Key 一个值,配上原子自增、过期时间和条件设置,就能解决缓存、计数、锁、限流等一大堆问题。** 把它吃透,Redis 就算入门了一半。