本文将详细介绍 Redis 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是字符串长度,通常视为0(1) |
getrange key start end | 获取指定key的从start到end的部分值 | O(n),n是字符串长度,通常视为O(1) |
1. 相关命令演示
# SET 命令
# 设置 key1 的值为 "Hello"
# 时间复杂度: O(1)
127.0.0.1:6379> SET key1 "Hello"
OK
# 设置 key2 的值为 "Redis"
# 时间复杂度: O(1)
127.0.0.1:6379> SET key2 "Redis"
OK
# GET 命令
# 获取 key1 的值
# 时间复杂度: O(1)
127.0.0.1:6379> GET key1
"Hello"
# DEL 命令
# 删除 key1
# 时间复杂度: O(1) 对单个键
127.0.0.1:6379> DEL key1
(integer) 1
# 尝试获取已经被删除的 key1 的值
# 时间复杂度: O(1)
127.0.0.1:6379> GET key1
(nil)
# MSET 命令
# 批量设置多个键值对
# 时间复杂度: O(k), k 是键的个数
127.0.0.1:6379> MSET key1 "Hello" key2 "Redis" key3 "!"
OK
# MGET 命令
# 批量获取多个键的值
# 时间复杂度: O(k), k 是键的个数
127.0.0.1:6379> MGET key1 key2 key3
1) "Hello"
2) "Redis"
3) "!"
# INCR 命令
# 将 counter 的值加 1
# 时间复杂度: O(1)
127.0.0.1:6379> SET counter 10
OK
127.0.0.1:6379> INCR counter
(integer) 11
# DECR 命令
# 将 counter 的值减 1
# 时间复杂度: O(1)
127.0.0.1:6379> DECR counter
(integer) 10
# INCRBY 命令
# 将 counter 的值加 5
# 时间复杂度: O(1)
127.0.0.1:6379> INCRBY counter 5
(integer) 15
# DECRBY 命令
# 将 counter 的值减 3
# 时间复杂度: O(1)
127.0.0.1:6379> DECRBY counter 3
(integer) 12
# INCRBYFLOAT 命令
# 将 counter 的值加 2.5(浮点数)
# 时间复杂度: O(1)
127.0.0.1:6379> INCRBYFLOAT counter 2.5
"14.5"
# APPEND 命令
# 将 " World" 追加到 key1 的值后
# 时间复杂度: O(1)
127.0.0.1:6379> APPEND key1 " World"
(integer) 11
127.0.0.1:6379> GET key1
"Hello World"
# STRLEN 命令
# 获取 key1 的值的长度
# 时间复杂度: O(1)
127.0.0.1:6379> STRLEN key1
(integer) 11
# SETRANGE 命令
# 从偏移量 6 开始,将 key1 的值替换为 "Redis"
# 时间复杂度: O(n),n 是字符串的长度,通常视为 O(1)
127.0.0.1:6379> SET key1 "Hello World"
OK
127.0.0.1:6379> SETRANGE key1 6 "Redis"
(integer) 11
127.0.0.1:6379> GET key1
"Hello Redis"
# GETRANGE 命令
# 获取 key1 从偏移量 0 到 4 的部分值
# 时间复杂度: O(n),n 是字符串的长度,通常视为 O(1)
127.0.0.1:6379> GETRANGE key1 0 4
"Hello"
2. 内部编码
Redis 中的字符串(String)类型是最基本的数据类型。为了高效地存储和操作字符串数据,Redis对字符串类型进行了多种内部编码优化。具体来说,字符串的内部编码主要有以下三种:int
、embstr
和 raw。
Redis 4.0 及之后版本
embstr
编码:用于长度小于等于 44 字节的字符串。raw
编码:用于长度大于 44 字节的字符串。
2.1 int
当一个字符串的值可以表示为 64 位(8字节)带符号整数时,Redis 会将该字符串编码为 int
类型。这种编码方式节省了内存,并且可以更高效地执行诸如 INCR
和 DECR
等操作。
127.0.0.1:6379> SET myint 100
OK
127.0.0.1:6379> OBJECT ENCODING myint
"int"
2.2. embstr
当字符串的长度小于等于 44 字节时,Redis 会使用 embstr
编码。这种编码将 Redis 对象和实际字符串数据存储在连续的内存块中,因此在内存分配和访问上更加高效。embstr
编码在创建时效率很高,但一旦需要修改字符串内容,它会转换为 raw
编码。
127.0.0.1:6379> SET shortstr "Hello, Redis!"
OK
127.0.0.1:6379> OBJECT ENCODING shortstr
"embstr"
2.3 raw
当字符串长度大于 44 字节或需要进行修改操作时,Redis 会使用 raw
编码。raw
编码将 Redis 对象和字符串数据分开存储。这种方式适用于较长字符串或频繁修改的字符串,因为在这种情况下,内存分配和管理会更加高效。
127.0.0.1:6379> SET longstr "This is a very long string that exceeds the embstr limit."
OK
127.0.0.1:6379> OBJECT ENCODING longstr
"raw"
来看一下我使用的版本
验证一下是39字节还是44字节发生内部编码类型的变化
测试长度为 39 字节的字符串
测试长度为 44 字节的字符串
测试长度为 45 字节的字符串
3 典型使用场景
3.1 缓存(Cache)功能
下图是比较典型的缓存使用场景,其中 Redis作为缓冲层,MySQL作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
下面的伪代码模拟了图 2-10 的业务数据访问过程
1. 假设业务是根据用户 uid 获取用户信息
UserInfo getUserInfo(long uid) {
...
}
2. 首先从 Redis 获取用户信息,我们假设用户信息保存在"user:info:<uid>" 对应的键中
1 // 根据 uid 得到 Redis 的键
2 String key = "user:info:" + uid;
3
4 // 尝试从 Redis 中获取对应的值
5 String value = Redis 执行命令:get key;
7 // 如果缓存命中(hit)
8 if(value != null){
9 //假设我们的用户信息按照 JSON 格式存储
10 UserInfo userInfo = JSON 反序列化(value);
11 return userInfo:
12 }
3. 如果没有从 Redis 中得到用户信息,及缓存 miss,则进一步从 MVSOL中获取对应的信息,随后写入缓存并返回:
// 如果缓存未命中(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;
}
3.2 计数器功能
许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。如下图所示,例如视频网站的视频播放次数可以使用Redis 来完成:用户每播放一次视频,相应的视频播放数就会自增 1。
3.3 共享会话
如图所示,一个分布式Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成一个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题是用户无法容忍的。
为了解决这个问题,可以使用 Redis 将用户的 Session 信息进行集中管理,如下图所示,在这种模式下,只要保证 Redis 是高可用和可扩展性的,无论用户被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息。
3.4 手机验证码
很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不会频繁访会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,如图所示。
此功能可以⽤以下伪代码说明基本实现思路
// 发送验证码并处理发送频率限制
String 发送验证码(String phoneNumber) {
// 构造用于限制短信发送频率的 Redis 键
String key = "shortMsg:limit:" + phoneNumber;
// 尝试在 Redis 中设置键值对,设置过期时间为 1 分钟(60 秒),只在键不存在时才能成功(NX 参数)
boolean r = Redis.执行命令:set key 1 ex 60 nx;
if (!r) {
// 如果设置失败,说明之前已经设置过该手机号的发送频率限制
// 增加计数器,表示又发送了一次验证码
long c = Redis.执行命令:incr key;
if (c > 5) {
// 如果超过了一分钟内的 5 次发送限制,限制发送,返回 null
return null;
}
}
// 生成随机的 6 位数验证码
String validationCode = 生成随机的 6 位数的验证码();
// 设置验证码的存储键,有效期设置为 5 分钟(300 秒)
String validationKey = "validation:" + phoneNumber;
Redis.执行命令:set validationKey validationCode ex 300;
// 返回生成的验证码,后续通过手机短信发送给用户
return validationCode;
}
// 验证用户输入的验证码是否正确
boolean 验证验证码(String phoneNumber, String validationCode) {
// 构造存储验证码的 Redis 键
String validationKey = "validation:" + phoneNumber;
// 从 Redis 中获取存储的验证码值
String value = Redis.执行命令:get validationKey;
if (value == null) {
// 如果没有找到该手机号的验证码记录,验证失败
return false;
}
// 比较用户输入的验证码是否和存储的验证码一致,返回相应的验证结果
return value.equals(validationCode);
}
3.5 分布式锁
在 Redis 中,String 类型可以用于实现简单的分布式锁:
-
原子性操作: Redis 的命令是原子性的,即 Redis 单个命令的执行是不可中断的,要么全部执行成功,要么全部不执行,不存在部分执行的情况。例如,
SETNX
命令(设置值并仅在键不存在时设置成功)和DEL
命令(删除键值对)都是原子操作。这种特性确保了在高并发环境下,对分布式锁的获取和释放操作是可靠的。 -
并发控制: Redis 是单线程的,通过事件循环和非阻塞 I/O 实现高并发处理。虽然 Redis 本身是单线程的,但其内部使用了时间片轮转机制来实现多个客户端的请求处理。这使得 Redis 能够高效处理大量的并发请求,适合作为分布式锁的存储和管理工具。
-
过期时间: 可以为 Redis 的 String 类型设置过期时间(Expiration),即在设置键值对时可以指定键的生存时间。这一特性对于分布式锁尤为重要,可以避免因为客户端异常退出而造成的死锁情况。设置合适的过期时间可以确保即使锁未显式释放,也能在一定时间后自动释放,从而避免资源长时间被锁定。
此功能可以⽤以下伪代码说明基本实现思路
// 示例中的参数
String lockKey = "resource_lock";
String clientId = UUID.randomUUID().toString();
// 执行 SETNX 命令
boolean lockAcquired = Redis.执行命令:setnx(lockKey, clientId);
if (lockAcquired) {
// 如果成功获得锁
try {
// TODO: 执行业务逻辑
} finally {
// 释放锁
Redis.执行命令:del(lockKey);
}
} else {
// 获取锁失败的处理逻辑
// TODO: 处理获取锁失败的情况
}
命令说明
- 当执行
SETNX lockKey clientId
命令时,Redis 将尝试设置键lockKey
的值为clientId
。 - 如果
lockKey
已经存在(即已被其他客户端设置),SETNX
命令将会设置失败,返回0
。 - 如果
lockKey
不存在,SETNX
命令将设置成功,返回1
表示获取了锁。
注意事项
- 获取锁后,一定要确保最终释放锁,以避免锁被长时间持有而造成资源无法访问。
- 可以结合设置过期时间的方式来实现自动释放锁,避免因客户端异常退出而导致的死锁问题。
以上介绍了使用Redis的字符串数据类型可以使用的几个场景,但其适用场景远不止于此,开发人员可以结合字符串类型的特点以及提供的命令,充分发挥自己的想象力,在自己的业务中去找到合适的场景去使用Redis的字符串类型。
码字不易,如果有用还请三连支持哦。