redis
Redis基础篇
redis基础篇
正在加载知识笔记
redis
redis基础篇
Redis是一种键值型的NoSQL数据库,这里有两个关键字
其中**键值型**是指Redis中存储的数据都是以Key-Value键值对的形式存储,而Value的形式多种多样,可以使字符串、数值甚至Json
而NoSQL则是相对于传统关系型数据库而言,有很大差异的一种数据库
NoSql可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库。
传统关系型数据库是结构化数据,每张表在创建的时候都有严格的约束信息,如字段名、字段数据类型、字段约束等,插入的数据必须遵循这些约束
而NoSQL则对数据库格式没有约束,可以是键值型,也可以是文档型,甚至是图格式
传统数据库的表与表之间往往存在关联,例如外键约束
而非关系型数据库不存在关联关系,要维护关系要么靠代码中的业务逻辑,要么靠数据之间的耦合
{
id: 1,
name: "张三",
orders: [
{
id: 1,
item: {
id: 10, title: "荣耀6", price: 4999
}
},
{
id: 2,
item: {
id: 20, title: "小米11", price: 3999
}
}
]
}
例如此处要维护张三与两个手机订单的关系,不得不冗余的将这两个商品保存在张三的订单文档中,不够优雅,所以建议使用业务逻辑来维护关联关系
传统关系型数据库会基于Sql语句做查询,语法有统一的标准
sql SELECT id, age FROM tb_user WHERE id = 1 |
|---|
而不同的非关系型数据库查询语法差异极大
bash Redis: get user:1 MongoDB: db.user.find({_id: 1}) elasticsearch: GET http://localhost:9200/users/1 |
|---|
传统关系型数据库能满足事务的ACID原则(原子性、一致性、独立性及持久性)
而非关系型数据库汪汪不支持事务,或者不能要个保证ACID的特性,只能实现计本的一致性
| SQL | NoSQL | |
|---|---|---|
| 数据结构 | 结构化(Structured) | 非结构化 |
| 数据关联 | 关联的(Relational) | 无关联的 |
| 查询方式 | SQL查询 | 非SQL |
| 事务特性 | ACID | BASE |
| 存储方式 | 磁盘 | 内存 |
| 扩展性 | 垂直 | 水平 |
| 使用场景 | 1)数据结构固定 2)对一致性、安全性要求不高 | 1)数据结构不固定 2)相关业务对数据安全性、一致性要求较高 3)对性能要求 |
Redis诞生于2009年,全称是Remote Dictionary Server远程词典服务器,是一个基于内存的键值型NoSQL数据库。
特征:
作者:Antirez
Redis官网:
Redis - The Real-time Data Platform
第一步 下载redis及版本选择
第二步 下载安装最新版本的gcc编辑器
yum -y install gcc
gcc --version
第三步 上传redis-7.0.10tar.gz放到/opt目录下
第四步 解压命令:
tar -zxvf redis-7.0.10.tar.gz
创建了javaRedis文件夹,解压至这个文件夹里面
tar -zxvf redis-7.0.10.tar.gz -C javaRedis
第五步 进入目录 :cd redis-7.0.10
执行make指令(没有c语音环境会报错)
解决办法:运行make disclean
没有错误则再执行make install
make
make install
第五步
安装完成
![]()
这三个指令在bin目录下
ls /usr/local/bin
运行 redis-server
redis-server
前台启动成功!
地址:usr/local/bin->PATH
创建一个redis缓存文件夹(/root/myredis)
mkdir myredis
redis给我们提供了一个配置文件的模版
1.复制一份配置文件到myredis中
cp /opt/javaRedis/redis-7.0.10/redis.conf .
mv redis.conf myredis/
2.修改配置文件(后台启动,访问限制127.0.0.1 访问密码关闭)
找行号的时候在一般模式下输入 :行号——》回车
![]()
![]()
![]()
- 指定配置启动
#指定配置文件启动
redis-server /root/myredis/redis.conf
查看redis进程运行状态(PID:19304 ?作为守护进程不依赖窗口 监听窗口:6379)
[root@master myredis]# ps -aux | grep redis
root 19304 0.0 0.3 163156 3124 ? Ssl 10:37 0:00 redis-server *:6379
root 19318 0.0 0.0 112824 980 pts/1 R+ 10:37 0:00 grep --color=auto redis
#客户端连接
redis-cli
ping
1. 默认连接 (最常用)
<font style="color:rgb(68, 71, 70);">redis-cli</font><font style="color:rgb(68, 71, 70);">127.0.0.1</font> (本机)<font style="color:rgb(68, 71, 70);">6379</font><font style="color:rgb(68, 71, 70);">ping</font>,如果返回 <font style="color:rgb(68, 71, 70);">PONG</font>,说明连接畅通。2. 指定参数连接 (进阶)
当 Redis 不在本机,或者端口改了,或者有密码时,就需要用参数:
<font style="color:rgb(68, 71, 70);">redis-cli -</font><font style="color:rgb(68, 71, 70);">h</font><font style="color:rgb(68, 71, 70);"> [IP地址] -p [端口号] -a [密码]</font><font style="color:rgb(68, 71, 70);">-</font><font style="color:rgb(68, 71, 70);">h</font> (host): 指定服务器 IP。<font style="color:rgb(68, 71, 70);">-p</font> (port): 指定端口。<font style="color:rgb(68, 71, 70);">-a</font> (auth): 指定密码(通常建议出于安全考虑,连上后再输入 <font style="color:rgb(68, 71, 70);">auth [密码]</font>,而不是直接写在命令行里,防止被历史记录看到)。**<font style="color:rgb(68, 71, 70);">--raw</font>** 参数 (重点):
<font style="color:rgb(68, 71, 70);">--raw</font>。<font style="color:rgb(68, 71, 70);">\xe4\xb8...</font>)。加上 <font style="color:rgb(68, 71, 70);">redis-cli --raw</font>,就能直接显示正常的汉字。1. 优雅关闭 (推荐做法)
<font style="color:rgb(68, 71, 70);">redis-cli shutdown</font><font style="color:rgb(68, 71, 70);">redis-cli</font> 连进去,然后再输入 <font style="color:rgb(68, 71, 70);">shutdown</font>,效果是一样的。2. 远程关闭
<font style="color:rgb(68, 71, 70);">redis-cli -</font><font style="color:rgb(68, 71, 70);">h</font><font style="color:rgb(68, 71, 70);"> [IP] -p [端口] shutdown</font><font style="color:rgb(68, 71, 70);">-</font><font style="color:rgb(68, 71, 70);">h</font> 和 <font style="color:rgb(68, 71, 70);">-p</font> 告诉客户端去关闭哪一个服务。3. 暴力关闭 (危险做法)
<font style="color:rgb(68, 71, 70);">kill -9 [进程号]</font><font style="color:rgb(68, 71, 70);">kill -9</font> 是系统层面的强制杀死进程。Redis 来不及把内存里的数据写回硬盘就被杀掉了,上次保存之后的所有新数据都会丢失。除非进程卡死完全无响应,否则千万不要用这个命令。疑惑是:“现在的电脑都是多核 CPU,单线程不应该慢吗?为什么 Redis 单线程反而快?”
根据图片中的描述,Redis 的单线程模型主要有以下优势:
既然是单线程,那成千上万个客户端同时连接 Redis,它怎么处理得过来?这里就用到了 I/O 多路复用 (I/O Multiplexing)。
图片中对它的定义是:在一个线程中同时监听多个 I/O 通道(Socket)的状态。
为了理解这个概念,我们可以把 Redis 比作一家餐厅,客户端比作客人,线程比作服务员。
select、poll 或 epoll 等机制。结合图片内容,Redis 之所以快,是因为它由这三驾马车拉动:
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5中常用的数据类型
2中特殊类型
| 命令 | 描述 |
|---|---|
| set key value | 设置指定key的值 NX:当数据库中key不存在时,可以将key-value添加数据库 XX: 当数据库key存在时,可以将key-value添加数据库 EX: key的超时秒数 PX:key的超时毫秒数 |
| get key | 获取指定key的值 |
| setex key seconds value | 设置指定key的值,并将key的过期时间设为seconds秒 |
| setnx key value | 只有在key不存在时设置key的值 |
| append key value | 将给定的value追加到原值的末尾 |
| strlen key | 获取值的长度 |
| incr key | 将key中存储的数字值增1,只能对数字值操作,如果为空,新增值1 |
| decr key | 将key中存储的数字值减1,只能对数字值操作,如果为空,新增值-1 |
| incrby/decrby <步长> | 将key中储存的数字值增减,自定义步长 |
| mset .... | 同时设置一个或者多个key-value对 |
| mget.... | 同时获取一个或者多个value |
| ........ | ....... |
实际上 Redis 为了追求极致的性能和灵活性,自己构建了一种名为 SDS (Simple Dynamic String,简单动态字符串) 的结构。
Redis 没有直接使用 C 语言传统的字符串表示(以空字符 \0 结尾的字符数组),而是使用 SDS 来作为默认的字符串表示。
SDS 长什么样?
你可以把 SDS 想象成一个结构体(Struct)。虽然不同版本的 Redis 实现细节微调过,但核心思想是不变的。
一个典型的 SDS 结构大致包含这三个部分:
len** (Length)**: 记录当前字符串实际使用的长度。alloc** (Allocation)**: 记录给这个字符串分配的总空间(有些版本叫 free,记录剩余空间)。buf** (Buffer)**: 实际存储字节的数组。为什么要专门设计 SDS?(对比 C 语言字符串)
Redis 主要是为了解决 C 语言字符串的几个痛点:
A. 获取字符串长度的速度 (O(1) vs O(N))
\0 为止。如果字符串很长,这非常耗时(O(N))。len 属性。无论字符串多长,获取长度只需要读取这个属性,瞬间完成(O(1))。这也是为什么 Redis 执行 STRLEN 命令极快的原因。B. 二进制安全 (Binary Safe)
\0 (空字符) 作为字符串结束的标志。如果你想在 Redis 里存一张图片(二进制数据),图片数据中间可能恰好包含 \0,C 语言会误以为字符串结束了,导致数据截断。\0 来判断结束,而是根据 len 属性来判断读多少个字节。所以 Redis 的 String 可以存储任何数据:文本、图片、视频片段、序列化对象等。C. 减少内存重新分配 (预分配机制)
这是 Redis 高性能的关键之一。
APPEND)时,Redis 不仅会分配你需要的空间,还会多分配一部分空间备用。下次再追加内容时,如果备用空间够用,就不需要找操作系统申请内存了。Redis String 的三种内部编码
虽然对外都叫 String,但在内存中,Redis 会根据原本内容的类型和长度,自动选择三种存储方式之一:
| 编码方式 | 场景 | 特点 |
|---|---|---|
| int | 存储的是纯数字 (如 set age 20) | 如果是 64 位整数,Redis 会直接把指针当成整数值存起来,极度节省空间。 |
| embstr | 存储短字符串 (通常 < 44 字节) | 嵌入式 SDS。它把 Redis 的对象头 (RedisObject) 和 SDS 结构连续存在一起。只分配一次内存,效率极高,且对 CPU 缓存友好。 |
| raw | 存储长字符串 | 标准 SDS。对象头和 SDS 数据分开存储,需要分配两次内存。 |
总结
Redis 的 String 其实是一个智能的容器 (SDS):
<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Redis Hash</font>是一个<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">String</font>类型的<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Field</font>和<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Value</font>的映射表,<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Hash</font>特别适合用于存储对象
| 命令 | 描述 |
|---|---|
| hset key field value | 将哈希表key 中的字段field的值设为value |
| hget key field | 获取存储在哈希表中指定字段的值 |
| hdel key field | 删除存储在哈希表中的指定字段 |
| hkeys key | 获取哈希表中所有字段 |
| hvals key | 获取哈希表中所有值 |
| hgetall key | 获取在哈希表中指定key的所有字段和值 |
Redis 的 Hash (哈希) 结构也是一个“变形金刚”。它主要用于存储对象(比如用户信息:姓名、年龄、ID),为了在内存占用和查询速度之间找到最优解,它会在底层使用两种不同的数据结构:
1. 第一阶段:省内存模式 (ZipList / ListPack)
当你创建一个 Hash,且里面的字段(Field)和值(Value)都很少、很短时,Redis 会优先使用 ZipList(Redis 7.0 后逐步替换为 ListPack)。
key 和 value 挨在一起,一个接一个地塞进这段连续内存里。[ ... | Field1 | Value1 | Field2 | Value2 | Field3 | Value3 | ... ]
Field2,Redis 需要从头开始遍历。2. 第二阶段:高性能模式 (HashTable)
当满足以下任一条件时,Redis 会将底层结构升级为真正的 HashTable:
redis.conf 中修改)HashMap 或 Python 的 dict)。3. Redis Hash 的黑科技:渐进式 Rehash (Progressive Rehash)
这是面试中的超高频考点。
当 Hash 表里的数据越来越多,必须扩容(比如从 1000 个桶扩容到 2000 个),否则冲突会很严重,查询变慢。
但是,Redis 是单线程的。如果一次性把 100 万个数据从“旧表”搬运到“新表”,整个 Redis 就会卡死几秒钟,这在线上是灾难。
Redis 的解决方案:
它采用了“分期付款”的策略,不一次性搬完。
ht[0] 旧表, ht[1] 新表)。总结
Redis Hash 的设计哲学是:
<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Redis List</font>是简单的字符串列表,按照插入顺序排序
| 命令 | 描述 |
|---|---|
**<font style="color:#ED740C;background-color:rgba(27, 31, 35, 0.05);">lpush/rpush</font>**key value1 [value2] | 将一个或多个值插入到列表头部或尾部 |
| lpop/rpop | 从左边/右边吐出一个值。值在键在,值光键亡 |
| lrange key start stop | 获取列表指定范围内的元素 |
**<font style="color:#ED740C;background-color:rgba(27, 31, 35, 0.05);">RPOP</font>**key | 移除并获取列表最后一个元素 |
| rpoplpush | 将key1列表右边吐出一个值,插到key2列表左边 |
**<font style="color:#ED740C;background-color:rgba(27, 31, 35, 0.05);">llen</font>**key | 获取列表长度 |
**<font style="color:#ED740C;background-color:rgba(27, 31, 35, 0.05);">brpop</font>**key1 [key2] timeout | 移出并获取列表的最后一个元素 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
在 Redis 的早期版本中,List 的底层实现会在 ZipList (压缩列表) 和 LinkedList (双向链表) 之间切换。但现在的 Redis(3.2 版本以后)为了平衡内存和性能,引入了一种混合结构:QuickList (快速列表)。
我们可以把 Redis 的 List 想象成一列“火车”。
1. 宏观结构:QuickList (火车)
如果你学过数据结构,你知道:
Redis 的 QuickList 结合了两者的优点。
通俗类比:
QuickList 就像一列火车。
2. 微观核心:ZipList (紧凑的车厢)
为了理解 QuickList,必须先理解它里面的“车厢”——ZipList。
ZipList 是一块连续的内存空间。它不使用指针来连接数据,而是把数据一个挨一个地挤在一起。
它的结构大概是这样的:
[ 总字节数 | 尾部偏移量 | 元素数量 | 元素1 | 元素2 | ... | 结束标记 ]
3. 为什么要搞出 QuickList 这种混合体?
Redis 的设计哲学是:在极端性能和内存效率之间找平衡。
1,还得附送两个指针,数据本身比元数据还小,太亏了。QuickList 的完美平衡:
它限制了每个“车厢”(ZipList)的大小(比如每个车厢只存 8KB 数据)。
4. 总结与 Redis 7.0 的进化
简单来说,Redis List 的底层是 “链表 + 数组”的结合体。
注:Redis 7.0 的更新
在最新的 Redis 7.0 中,ZipList 已经被 ListPack 取代了。
- 原因: ZipList 有一个致命缺陷叫“连锁更新”(Cascading Update)。如果一个数据长度变了,可能导致后面所有数据的长度记录都得变,引发连续的内存重分配。
- ListPack: 结构更简单,去掉了前一个元素的长度记录,彻底解决了连锁更新问题,但整体思路依然是“紧凑排列的内存块”。
<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Redis set</font>是<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">String</font>类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据
Redis set 对外提供功能和list类似,特殊点在于自动重排和不能出现重复数据
Redis的set是string类型的无序集合,它底层其实是一个value为null的hash表,所以添加、删除、查找的复杂都是O(1)。一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变
| 命令 | 描述 |
|---|---|
| sadd key member1 [member2] | 向集合添加一个或多个成员 |
| smembers key | 返回集合中的所有成员 |
| sismember | 判断集合中是否包含该值 (1有0没有) |
| scard key | 获取集合的成员数 |
| sinter key1 [key2] | 返回给定所有集合的交集 |
| sunion key1 [key2] | 返回所有给定集合的并集 |
| sdiff key1 [key2] | 返回给定所有集合的差集 |
| srem key member1 [member2] | 移除集合中一个或多个成员 |
| spop | 随机从该集合中吐出一个值 |
| spop | 随机从该集合中吐出N个值 |
| srandmember | 随机从该集合中取出N个值,但是不会删除 |
Redis 的 Set (集合) 也是一个典型的“两面派”。它的设计目标是:保证元素唯一(无重复)且无序。
为了达到这个目标,同时兼顾省内存和快速度,Redis 会在底层根据情况在这两种结构中切换:
1. 极简模式:IntSet (整数集合)
当你创建的集合满足以下两个条件时,Redis 会使用 IntSet:
set-max-intset-entries 配置)。它的结构长什么样?
IntSet 其实就是一个有序的、不重复的整数数组。
它是一块连续的内存,里面存着 [1, 5, 9, 12, ...]。
2. 标准模式:HashTable (哈希表)
一旦不满足 IntSet 的条件(比如你存了一个字符串 "hello",或者整数数量超过了 512 个),Redis 就会立马把底层结构转化为 HashTable。
它的结构长什么样?
这和你刚才学的 Hash (哈希) 的底层结构(字典 dict)是完全一样的。
Key -> Value 键值对。NULL(空)。只利用了 Key 的唯一性来保证集合元素不重复。这就像 Java 中的 HashSet 其实底层就是 HashMap,只是 Value 放了个占位符一样。
3. 总结:Set 的“变身”逻辑
Redis Set 的哲学是:能省则省,该快则快。
SADD myset 1 2 3。Redis 发现都是小整数,于是用 IntSet,内存占用极小。SADD myset "apple"(加入了非整数),IntSet 存不了字符串,瞬间转化为 HashTable。<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Redis ZSet</font>有序集合是<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">String</font>类型元素的集合,且不允许重复的成员。每个元素都会关联一个<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">double</font>类型的分数(<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">score</font>) 。<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Redis</font>正是通过分数来为集合中的成员进行从小到大排序。有序集合的成员是唯一的,但分数却可以重复。
| 命令 | 描述 |
|---|---|
| zadd key score1 member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
| zrange key start stop [WITHSCORES] | 通过索引区间返回有序集合中指定区间内的成员 |
| zincrby key increment member | 有序集合中对指定成员的分数加上增量increment |
| zrem key member [member …] | 移除有序集合中的一个或多个成员 |
这也是 Redis 数据结构中的“终极 BOSS”,也是面试中区分度最高的一个知识点。
ZSet (Sorted Set, 有序集合) 的独特之处在于:它不仅要求元素唯一(像 Set),还给每个元素关联了一个分数(Score),并根据这个分数自动排序。
为了实现“既能快速查分,又能快速排序,还能支持范围查找”,Redis ZSet 的底层也是一套“混合双打”的组合拳:
1. 轻量级模式:ZipList (压缩列表) / ListPack
和 Hash、Set 一样,当数据量很少时,ZSet 会使用紧凑的 ZipList(Redis 7.0+ 为 ListPack)。
[ Member1 | Score1 | Member2 | Score2 | ... ]2. 重量级模式:SkipList (跳表) + HashTable (哈希表)
一旦数据变多,ZSet 就会升级为由 HashTable 和 SkipList 共同组成的复合结构。这是 ZSet 的精髓所在。
你需要理解,为什么它需要两个结构配合?
A. 负责“快查”的:HashTable (哈希表)
Member -> Score 的映射。ZSCORE user:ranking zhangsan)。如果没有哈希表,你得在有序列表里慢慢找,太慢了。有了哈希表,O(1) 就能把分数取出来。B. 负责“排序”和“范围查”的:SkipList (跳表)
这是 ZSet 的灵魂。传统的有序链表查找太慢(O(N)),而平衡树(如红黑树)实现太复杂且范围查询(Range Query)不如链表方便。
SkipList (跳表) 是一种“空间换时间”的黑科技。
3. 为什么不用红黑树(Red-Black Tree)?
这是面试官最喜欢追问的问题。
虽然 Java 的 TreeMap 用的是红黑树,但 Redis 作者 Antirez 选择跳表是因为:
ZRANGE(查排名范围),链表结构的跳表有着天然优势。总结
Redis ZSet 的底层架构是:
ZSCORE 查分只需要 O(1)。ZRANGE 查排名、排序只需要 O(log N)。这也解释了为什么 ZSet 是 Redis 中功能最强大、但也最占内存的数据结构。
你好!很高兴能带你深入了解 Redis。我是你的 Redis 老师。
今天我们要聊的是 BitMap(位图)。在 Redis 的世界里,BitMap 并不是一种独立的数据类型,而是一种极其巧妙的逻辑设计。它被称为“省空间大师”,特别适合处理那种“只有两种状态”的海量数据。
首先我们要揭开它的真面目:BitMap 的底层就是 String(字符串)。
在 Redis 中,String 是二进制安全的,最大可以存储 512MB 的内容。既然是二进制,那底层其实就是一个个 0 和 1 组成的比特序列。BitMap 就是利用了这一点,通过偏移量(offset)直接操作这些比特位。
操作 BitMap 的命令非常直观,你可以把它们看作是在操作一排无限长的“开关”。
SETBIT key offset value
SETBIT user:sign:20231001 10 1(表示用户 ID 为 10 的人在 2023-10-01 签到了)。GETBIT key offset
GETBIT user:sign:20231001 10(返回 1 说明签到了,0 说明没签到)。BITCOUNT key [start end]
BITCOUNT user:sign:20231001(今天一共有多少人签到)。BITOP operation destkey key [key ...]
BITPOS key bit [start] [end]
SETBIT mykey 1000000000 1,虽然只占 1 个位,但 Redis 会立刻分配足以容纳 10 亿位的内存(约 125MB)。如果偏移量太大,会导致 Redis 阻塞。总结一下:BitMap 就是用操作字符串的方式,去精准打击每一个比特位。它是解决“海量数据、二值状态、高频统计”问题的银弹。
| 命令 | 描述 |
|---|---|
| keys * | 查看当前库所有key |
| unlink key | 非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作 |
| expire key 10 | 为给定的key设置10秒过期时间 |
| ttl key | 查看还有多少秒过期,-1表示永不过期,-2表示已经过期 |
| KEYs pattern | 查找所有符合给定模式(pattern)的key |
| EXISTs key | 检查给定key是否存在 |
| TYPE key | 返回key所储存的值的类型 |
| TTL key | 返回给定key的剩余生存时间(TTL, time to live),以秒为单位 |
| DEL key | 该命令用于在key存在是删除key |
| select | 命令切换数据库 |
| dbsize | 查看当前数据库的key数量 |
| flushdb | 清空当前库 |
| flushall | 清空全部库 |
更多详细的命令可以查看官方文档:https://www.redis.net.cn/order/
方案1:长key处理 (String 类型)利用冒号分隔层级,将每个属性存为单独的 Key
代码段
SET user:1:name Alice
SET user:1:email alice@example.com
SET user:1:regtime "2023-09-12 10:00:00"
方案2:hash处理 (Hash 类型 - 推荐)最适合存储对象,将所有属性存入一个 Key 的不同 Field 中
代码段
HSET user:1 name Alice email alice@example.com regtime "2023-09-12 10:00:00"
方案3:json string (String 类型)将整个对象序列化为 JSON 字符串存入一个 Key
代码段
SET user:1 '{"id":1, "name":"Alice", "email":"alice@example.com", "regtime":"
方案1:长key处理
代码段
SET product:1:name "iPhone 12"
SET product:1:desc "一款先进的智能手机"
SET product:1:price 999
SET product:1:stock 100
方案2:hash处理 (推荐)方便单独修改库存或价格
代码段
HSET product:1 name "iPhone 12" desc "一款先进的智能手机" price 999 stock 100
# 修改库存非常方便: HINCRBY product:1 stock -1
方案3:json string
代码段
SET product:1 '{"id":1, "name":"iPhone 12", "desc":"一款先进的智能手机", "pri
记录消息 (LPUSH)将新消息从左侧推入列表
代码段
LPUSH chat:user:1 "你好,在吗?"
LPUSH chat:user:1 "今晚有空吃饭吗?"
消息切割 (LTRIM)只保留索引 0 到 9 的元素(即最新的 10 条),其余删除
代码段
LTRIM chat:user:1 0 9
获取聊天 (LRANGE)查看当前列表中的所有消息
代码段
LRANGE chat:user:1 0 -1
预设数据:
SADD user:1:friends 2 3 4SADD user:2:friends 3 4 5添加好友
代码段
SADD user:1:friends 6 # 给用户1添加好友6
判断好友
代码段
SISMEMBER user:1:friends 2 # 判断2是否是1的好友 (返回1是,0否)
删除好友
代码段
SREM user:1:friends 2 # 从用户1的好友列表中删除2
共同好友 (Inter)
代码段
SINTER user:1:friends user:2:friends
# 结果应为 3 和 4,因为他们都在两个集合中
可能好友 (Diff - 推荐机制)逻辑:用户2的朋友中,哪些还不是用户1的朋友?(差集)
代码段
SDIFF user:2:friends user:1:friends
# 结果为 5 (因为2认识5,但1不认识,所以可以推荐5给1)
好友数量
代码段
SCARD user:1:friends
好的,紧接上一题,这里是第 5 题和第 6 题的完整解答(对应图片 image_b173dc.png)。
预设数据:
exam:rank 的排行榜。添加学生 (ZADD)
*语法:ZADD key score member*
ZADD exam:rank 90 "student:1"
ZADD exam:rank 80 "student:2"
ZADD exam:rank 50 "student:3"
ZADD exam:rank 100 "ChaiJieJie"
查看高分 (ZREVRANGE)
从高到低排序(第1名在最前),-1_ 代表最后一名_
ZREVRANGE exam:rank 0 -1 WITHSCORES
# 结果: ChaiJieJie(100), student:1(90), student:2(80), student:3(50)
查看低分 (ZRANGE)
从低到高排序(倒数第1名在最前)
ZRANGE exam:rank 0 -1 WITHSCORES
# 结果: student:3(50), student:2(80), student:1(90), ChaiJieJie(100)
查看及格 (高) (ZREVRANGEBYSCORE)
查看 60 分到正无穷(+inf) 的人,从高分开始展示
ZREVRANGEBYSCORE exam:rank +inf 60 WITHSCORES
查看不及格 (低) (ZRANGEBYSCORE)
查看负无穷(-inf) 到 60 分(不包含 60 则用(60)的人
ZRANGEBYSCORE exam:rank -inf (60 WITHSCORES
查看前三名 (Top 3)
范围取索引 0 到 2
ZREVRANGE exam:rank 0 2 WITHSCORES
查看柴姐姐第几 (ZREVRANK)
查看特定人的排名(索引从 0 开始,所以 0 代表第 1 名)
ZREVRANK exam:rank "ChaiJieJie"
# 返回 0,表示她是第一名
查看及格数量 (ZCOUNT)
统计分数在 60 到正无穷之间的人数
ZCOUNT exam:rank 60 +inf
预设数据:
方案1:长key (String 类型)
将属性拼接到 Key 后面,适合属性很少且不需要一次性取出的场景
SET user:1:online_time 3600
SET user:1:login_count 10
增加一次登录: INCR user:1:login_count
方案2:hash处理 (Hash 类型 - 推荐)
最适合对象存储,聚合性好,key 看起来整洁
HSET user:1 online_time 3600 login_count 10
增加一次登录: HINCRBY user:1 login_count 1
获取所有信息: HGETALL user:1
<font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Jedis</font><font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Lettuce</font><font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">Redisson</font><font style="color:rgb(244, 116, 102);background-color:rgba(27, 31, 35, 0.05);">spring-boot-starter-data-redis</font><dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
public class JedisTest {
//测试连接ping
@Test
public void test_ping() {
//1.链接redis
/**
* 参数1: string host redis服务器的ip地址或者域名
* 参数2: int port redis的端口号
* 注意:
* 1. redis启动的时候,指定的配置文件必须注释 # bind 127.0.0.1 ::0
* 2. 云服务器玩家,一定要开放端口号 6379
*/
Jedis jedis = new Jedis("47.94.86.115",6379);
//2.使用方法
String ping = jedis.ping();
System.out.println("ping = " + ping);
//3.关闭连接
jedis.close();
}
//测试连接string
@Test
public void test_string() {
//1.链接redis
/**
* 参数1: string host redis服务器的ip地址或者域名
* 参数2: int port redis的端口号
* 注意:
* 1. redis启动的时候,指定的配置文件必须注释 # bind 127.0.0.1 ::0
* 2. 云服务器玩家,一定要开放端口号 6379
*/
Jedis jedis = new Jedis("47.94.86.115",6379);
//2.使用方法
String ping = jedis.ping();
System.out.println("ping = " + ping);
jedis.set("name","赵伟风");
String name = jedis.get("name");
System.out.println("name = " + name);
//3.关闭连接
jedis.close();
}
}
输出:
ping=PONG
name=hjs
| API | 返回值类型 | 说明 |
|---|---|---|
| redisTemplate.opsForValue() | ValueOperations | 操作String类型数据 |
| redisTemplate.opsForHash() | HashOperations | 操作Hash类型数据 |
| redisTemplate.opsForList() | ListOperations | 操作List类型数据 |
| redisTemplate.opsForSet() | SetOperations | 操作Set类型数据 |
| redisTemplate.opsForzSet() | ZSetOperations | 操作SortedSet类型数据 |
| redisTemplate | 通用的命令 |
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- Maven 模型版本,固定 4.0.0 -->
<modelVersion>4.0.0</modelVersion>
<!-- 继承 Spring Boot 官方 parent,统一管控版本号与插件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
<!-- 如果本地仓库没有 3.0.5,Maven 会自动从中央仓库下载 -->
</parent>
<!-- 本项目坐标 -->
<groupId>org.example</groupId>
<artifactId>redis_02_springboot_data_redis</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 依赖列表 -->
<dependencies>
<!-- 1. Spring Boot 核心启动器:提供自动配置、日志、yaml 支持等基础能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 2. 单元测试启动器:集成 JUnit5、AssertJ、Mockito 等常用测试框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- scope=test 表示只在测试 classpath 生效,打正式包时不会带入 -->
<scope>test</scope>
</dependency>
<!-- 3. Lombok:通过注解简化 POJO,省去 getter/setter/构造器等样板代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- 版本号由 spring-boot-starter-parent 统一管理 -->
</dependency>
-----------------------这个就是他的坐标----------------------------------------------
<!-- 4. Spring Data Redis 启动器:整合 RedisTemplate、连接池、序列化等,
默认使用 Lettuce 作为客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
spring:
data:
redis:
host: 192.168.136.100 # Redis 节点 IP
port: 6379 # Redis 节点端口
-----没设密码,以下省略
# 如果 Redis 没设密码,下面三行可省略
# password: 123456
# timeout: 2000ms # 命令超时(Spring Boot 3.x 推荐带单位)
# connect-timeout: 2000ms # 建立连接超时
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class,args);
}
}
// 表示这是一个 Spring Boot 的集成测试类,会启动完整的 Spring Boot 应用上下文
@SpringBootTest
public class RedisTest {
// 自动注入 Spring 容器中配置好的 RedisTemplate 实例
// RedisTemplate 是 Spring Data Redis 提供的高级封装,用于操作 Redis
@Autowired
private RedisTemplate redisTemplate;
// 测试方法,验证向 Redis 中存取字符串值并设置过期时间的功能
@Test
public void test() {
// 使用 RedisTemplate 的 ValueOperations(字符串操作)接口
// 将 key 为 "name" 的值设置为 "hjs",并设置过期时间为 1 天
// 参数说明:
// key: "name"
// value: "hjs"
// timeout: 1
// unit: TimeUnit.DAYS(天)
redisTemplate.opsForValue().set("name", "hjs", 1, TimeUnit.DAYS);
// 从 Redis 中根据 key "name" 获取对应的值
// 返回值类型为 Object,需要根据实际情况强转(这里实际是 String)
Object name = redisTemplate.opsForValue().get("name");
// 打印获取到的值,预期输出:name=hjs
System.out.println("name=" + name);
}
}
/**
* RedisTemplate 自定义配置类
*
* Spring Boot 提供了 RedisAutoConfiguration 会自动配置一个 RedisTemplate<String, Object>,
* 但默认使用 JdkSerializationRedisSerializer,导致存入 Redis 的 key 和 value 都是序列化后的二进制数据,
* 在 Redis 客户端(如 redis-cli)中查看时会出现乱码,不便于调试和运维。
*
* 通过自定义配置类,可以将 key 使用字符串序列化,value 使用 JSON 序列化,使数据更易读。
*/
@Configuration
public class RedisConfig {
/**
* 配置自定义的 RedisTemplate
*
* @param redisConnectionFactory Spring Boot 自动注入的 Redis 连接工厂(基于 Lettuce 或 Jedis)
* @return 配置好的 RedisTemplate<String, Object>
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建 RedisTemplate 实例,指定 key 为 String 类型,value 为 Object 类型
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂(必须),用于获取 Redis 连接
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 创建序列化器
// String 类型的 key 和 hashKey 使用 UTF-8 编码的字符串序列化器,保证 key 在 Redis 中可读
StringRedisSerializer stringRedisSerializer = StringRedisSerializer.UTF_8;
// value 和 hashValue 使用 Jackson 的 JSON 序列化器,支持复杂对象(如 POJO、List、Map 等)的存储
// 存储后在 Redis 中显示为可读的 JSON 字符串
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// ==================== Key 序列化方式 ====================
redisTemplate.setKeySerializer(stringRedisSerializer); // 普通 key(如 opsForValue().set 的 key)
redisTemplate.setHashKeySerializer(stringRedisSerializer); // Hash 结构的 field(即 hashKey)
// ==================== Value 序列化方式 ====================
redisTemplate.setValueSerializer(jsonRedisSerializer); // 普通 value(如 String、Object、List 等)
redisTemplate.setHashValueSerializer(jsonRedisSerializer); // Hash 结构的 value
// 可选:如果使用 StringRedisTemplate 的场景较多,也可设置默认序列化器
// redisTemplate.setDefaultSerializer(jsonRedisSerializer);
// 初始化 RedisTemplate 的各项配置(在 setConnectionFactory 后调用 afterPropertiesSet 可确保配置生效)
// 通常不手动调用,Spring 会自动处理,但显式调用更保险
redisTemplate.afterPropertiesSet();
// 返回配置好的 RedisTemplate,供 @Autowired 注入使用
return redisTemplate;
}
}
@Test
public void StringTest(){
//设置name1为李白
redisTemplate.opsForValue().set("name1","李白");
//获取name1
Object name1 = redisTemplate.opsForValue().get("name1");
System.out.println("name1="+name1);
//设置age时间为9527,有效时间10秒
redisTemplate.opsForValue().set("age",9527,10,TimeUnit.SECONDS);
//获取age
Object age = redisTemplate.opsForValue().get("age");
System.out.println("age="+age);
//如果name1不存在则设置name1为王五
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("name1", "王五");
System.out.println("aBoolean="+aBoolean);
}
:::tips name1=李白
age=9527
aBoolean=false
:::
@Test
public void HashTest(){
HashOperations<String, Object, Object> stringObjectObjectHashOperations = redisTemplate.opsForHash();
stringObjectObjectHashOperations.put("12138","name","王五");
stringObjectObjectHashOperations.put("12138","age",9527);
stringObjectObjectHashOperations.put("12138","sex","男");
stringObjectObjectHashOperations.put("12138","address","上海");
//获取map集合
Map<Object, Object> map = stringObjectObjectHashOperations.entries("12138");
Set<Object> objects = map.keySet();
for (Object object : objects){
System.out.println(object+"="+map.get(object));
}
System.out.println("------------------------");
//只获得keys
Set<Object> keys = stringObjectObjectHashOperations.keys("12138");
for(Object key:keys){
System.out.println(key);
}
System.out.println("------------------------");
//只获得values
List<Object> values = stringObjectObjectHashOperations.values("12138");
for(Object value:values){
System.out.println(value);
}
}
:::tips name=王五
age=9527
sex=男
address=上海
name
age
sex
address
王五
9527
男
上海
:::
@Test
public void ListTest(){
ListOperations<String, Object> listOperations = redisTemplate.opsForList();
//添加数据
listOperations.leftPush("list1","A");
listOperations.leftPush("list1","B");
listOperations.leftPushAll("list1","C","g","h","add");
listOperations.leftPush("list1","d");
//获取数据
List<Object> list1 = listOperations.range("list1", 0, -1);
for (Object object : list1){
System.out.println(object);
}
Long size = listOperations.size("list1");
System.out.println("size="+size);
//最后输出并删除
for (int i = 0; i < size; i++){
System.out.println(listOperations.rightPop("list1")+" ");
}
//最后输出长度
System.out.println(listOperations.size("list1"));
}
:::tips d
add
h
g
C
B
A
size=7
A
B
C
g
h
add
d
0
:::
@Test
public void SetTest(){
SetOperations<String, Object> setOperations = redisTemplate.opsForSet();
//添加数据,这里添加了两个 A,因为set集合不允许重复,所以只添加了一个A
setOperations.add("set1","A","B","C","A","D");
Set<Object> set1 = setOperations.members("set1");
for (Object object : set1){
System.out.println(object);
}
System.out.println("------------------------");
//删除bc
setOperations.remove("set1","B","C");
Set<Object> set2 = setOperations.members("set1");
for (Object object : set2){
System.out.println(object);
}
}
:::tips D
B
A
C
D
A
:::
@Test
public void ZSetTest(){
ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1","A",1.0);
zSetOperations.add("zset1","B",2);
zSetOperations.add("zset1","C",3.0);
zSetOperations.add("zset1","D",2.5);
Set<Object> zset1 = zSetOperations.range("zset1", 0, -1);
for (Object object : zset1){
System.out.println(object);
}
//修改分数
zSetOperations.incrementScore("zset1","B",5.9);
//取值
System.out.println("-------------------------");
Set<Object> zset12 = zSetOperations.range("zset1", 0, -1);
for (Object object : zset12){
System.out.println(object);
}
//删除成员
zSetOperations.remove("zset1","B");
System.out.println("-------------------------");
Set<Object> zset11 = zSetOperations.range("zset1", 0, -1);
for (Object object : zset11){
System.out.println(object);
}
}
:::tips A
B
D
C
A
D
C
B
A
D
C
:::
你好!很高兴看到你正在深入学习 Java 后端技术。Redis 是后端开发中非常重要的一环,而 Spring Data Redis 提供的 StringRedisTemplate 和 RedisTemplate 是我们操作 Redis 最常用的两个工具。
作为一个在生产环境摸爬滚打多年的“老鸟”,我来带你彻底拆解一下 StringRedisTemplate。我们不仅要懂怎么用,更要懂它“为什么”存在。
StringRedisTemplate?要理解它,我们先得看它的“老大哥” —— RedisTemplate。
假设你直接使用默认配置的 RedisTemplate<Object, Object> 向 Redis 存入一个键值对:key="name", value="zhangsan"。
当你兴高采烈地打开终端(redis-cli),输入 get name 想查看数据时,你会发现结果是这样的:
"\xac\xed\x00\x05t\x00\x04name"
或者类似的乱码。
因为 RedisTemplate 的默认序列化策略是 JDK 序列化 (JdkSerializationRedisSerializer)。它把 Java 对象变成了二进制流存进了 Redis。虽然 Java 能读懂,但是:
为了解决这个问题,Spring 官方专门封装了一个特殊的类 —— StringRedisTemplate。
它的核心逻辑非常简单:它默认假设 Redis 里的 Key 和 Value 都是字符串(String)。
当你用 StringRedisTemplate 存 key="name", value="zhangsan" 时,redis-cli 里看到的就是清清爽爽的:
"zhangsan"
StringRedisTemplate 本质上是一个继承自 RedisTemplate<String, String> 的子类。它和标准 RedisTemplate 最大的区别在于序列化器(Serializer)的配置。
看一下它的源码定义(简化版):
public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
// 设置 Key 的序列化器为 StringRedisSerializer
setKeySerializer(RedisSerializer.string());
// 设置 Value 的序列化器为 StringRedisSerializer
setValueSerializer(RedisSerializer.string());
// 设置 HashKey 的序列化器为 StringRedisSerializer
setHashKeySerializer(RedisSerializer.string());
// 设置 HashValue 的序列化器为 StringRedisSerializer
setHashValueSerializer(RedisSerializer.string());
}
}
划重点:
String 类型。StringRedisSerializer。这个序列化器非常简单,就是直接把 Java 的 String 转成 UTF-8 的 byte[] 存入 Redis,不做任何额外的二进制包装。虽然它叫 StringRedisTemplate,但别误会,它不仅能操作 Redis 的 String 类型,它能操作 Redis 的所有数据结构(List, Set, Hash, ZSet),前提是这些结构里的内容(元素)也是字符串。
通常我们会通过它获取 Operation 接口来操作:
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void demo() {
// 1. 操作 String (最常用)
// SET name "zhangsan"
stringRedisTemplate.opsForValue().set("name", "zhangsan");
// GET name
String value = stringRedisTemplate.opsForValue().get("name");
// 2. 操作 List
// LPUSH myList "a" "b" "c"
stringRedisTemplate.opsForList().leftPushAll("myList", "a", "b", "c");
// 3. 操作 Hash
// HSET myHash age "18"
stringRedisTemplate.opsForHash().put("myHash", "age", "18");
// 4. 操作 Set
// SADD mySet "java" "python"
stringRedisTemplate.opsForSet().add("mySet", "java", "python");
// 5. 常用通用操作
// EXPIRE name 60 (设置过期时间 60秒)
stringRedisTemplate.expire("name", 60, TimeUnit.SECONDS);
// DEL name
stringRedisTemplate.delete("name");
}
你可能会问:“老师,实际开发中我存的都是 User 对象、Order 对象,不仅仅是字符串啊,那是不是只能用 RedisTemplate 了?”
答案是:不,在互联网大厂的生产实践中,我们依然大量使用 StringRedisTemplate。
为什么?因为我们通常希望 Redis 里的数据是 JSON 格式。JSON 是通用的,谁都读得懂。
最佳实践流程:
User)转换成 JSON 字符串(使用 Jackson, Gson, Fastjson 等工具)。代码示例:
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 假设这是 Jackson 的工具类
@Autowired
private ObjectMapper objectMapper;
public void saveUser(User user) {
// 1. 对象 -> JSON String
String jsonUser = objectMapper.writeValueAsString(user);
// 2. 存入 Redis (作为字符串存储)
stringRedisTemplate.opsForValue().set("user:1001", jsonUser);
}
public User getUser(String userId) {
// 1. 从 Redis 取出 JSON String
String jsonUser = stringRedisTemplate.opsForValue().get("user:" + userId);
// 2. JSON String -> 对象
if (jsonUser != null) {
return objectMapper.readValue(jsonUser, User.class);
}
return null;
}
这样做的好处是完全可控。你清楚地知道存进去的是什么 JSON,不管是用 Java 读,还是用 Python 脚本修数据,或者用可视化工具查看,都非常清晰。
StringRedisTemplate vs RedisTemplate 对比总结| 特性 | StringRedisTemplate | RedisTemplate (默认配置) |
|---|---|---|
| 泛型 | <String, String> | <Object, Object> |
| 序列化策略 | StringRedisSerializer (存文本) | JdkSerializationRedisSerializer (存二进制) |
| Redis中数据格式 | 可读的字符串/JSON | 不可读的乱码/二进制流 |
| 跨语言兼容性 | 极好 (通用标准) | 差 (仅限 Java) |
| 内存占用 | 低 (数据紧凑) | 高 (包含类元数据头信息) |
| 适用场景 | 绝大多数业务场景,特别是需要存 JSON 或普通字符串时 | 需要存储复杂的 Java 对象且不需要跨语言时(现在用得较少) |
StringRedisTemplate 配合 JSON 转换库(如 Jackson)是最佳选择。这能避免很多坑。@Autowired RedisTemplate<Object, Object> redisTemplate 拿来就用。如果你非要用它来存对象并希望自动转 JSON,你需要手动编写 @Configuration 配置类,将它的序列化器修改为 Jackson2JsonRedisSerializer 或 GenericJackson2JsonRedisSerializer。StringRedisTemplate 很自由,但 Key 一定要规范。通常使用冒号分隔,例如 app:module:id(如 user:login:token:12345),这样在 Redis 可视化工具(如 RDM)里会自动分文件夹显示,非常整洁。opsForXxx()首先你要记住,StringRedisTemplate 本身的方法并不多,它通过工厂模式返回不同的“操作对象”来处理不同类型的数据:
opsForValue():操作 String(字符串/值)opsForHash():操作 Hash(哈希/字典)opsForList():操作 List(列表)opsForSet():操作 Set(集合)opsForZSet():操作 ZSet(有序集合)opsForValue)这是最基础、使用频率最高的操作,通常用于缓存 JSON 数据、计数器、分布式锁等。
// 获取操作对象
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 1. 设置值 (SET key value)
ops.set("user:name", "张三");
// 2. 设置值并带过期时间 (SET key value EX 60) -> 非常常用!
// 缓存场景必备,防止缓存雪崩
ops.set("verify:code:13800138000", "1234", 60, TimeUnit.SECONDS);
// 3. 获取值 (GET key)
String name = ops.get("user:name");
// 4. 只有 Key 不存在时才设置 (SETNX key value)
// 分布式锁的雏形。如果 key 存在则不操作返回 false,不存在则设置返回 true
Boolean isLocked = ops.setIfAbsent("lock:order:1001", "locked");
// 5. 只有 Key 存在时才设置 (SETXX)
// 更新缓存时常用
ops.setIfPresent("user:name", "李四");
// 6. 计数器自增/自减 (INCR key)
// 比如统计文章阅读量、点赞数
ops.increment("article:read:101", 1); // +1
ops.decrement("article:read:101", 1); // -1
// 7. 批量获取 (MGET)
List<String> values = ops.multiGet(Arrays.asList("key1", "key2", "key3"));
opsForHash)适合存储对象,且需要频繁修改对象中某个字段的场景(比如购物车)。它就像一个“迷你版”的 Java Map。
// 获取操作对象
HashOperations<String, Object, Object> ops = stringRedisTemplate.opsForHash();
// 1. 存入单个字段 (HSET key field value)
ops.put("cart:user:1001", "product:505", "1");
// 2. 存入整个 Map (HMSET key field1 value1 ...)
Map<String, String> data = new HashMap<>();
data.put("name", "iPhone 15");
data.put("price", "8999");
ops.putAll("product:1001", data);
// 3. 获取单个字段 (HGET key field)
String price = (String) ops.get("product:1001", "price");
// 4. 获取所有字段和值 (HGETALL key)
// 返回一个 Map
Map<Object, Object> allEntries = ops.entries("product:1001");
// 5. 判断字段是否存在 (HEXISTS key field)
Boolean hasKey = ops.hasKey("cart:user:1001", "product:505");
// 6. 删除字段 (HDEL key field)
ops.delete("cart:user:1001", "product:505");
// 7. 给 Hash 中的某个字段自增 (HINCRBY)
// 购物车商品数量 +1
ops.increment("cart:user:1001", "product:505", 1);
opsForList)适合做简单的消息队列、最新 N 条记录等。
// 获取操作对象
ListOperations<String, String> ops = stringRedisTemplate.opsForList();
// 1. 从左边压入 (LPUSH key value)
// 比如记录用户的浏览历史,最新的在最前面
ops.leftPush("history:user:1001", "http://xxx.com/a");
// 2. 从右边压入 (RPUSH key value)
ops.rightPush("task:queue", "task_1");
// 3. 从左边弹出 (LPOP key)
// 拿出并删除第一个元素
String popValue = ops.leftPop("task:queue");
// 4. 获取范围内的元素 (LRANGE key start end)
// 获取前 10 条记录,0 是开始,-1 代表所有,9 代表第 10 条
List<String> range = ops.range("history:user:1001", 0, 9);
// 5. 获取列表长度 (LLEN key)
Long size = ops.size("task:queue");
opsForSet)适合做点赞用户列表(由不可重复性保证)、共同好友(交集)、抽奖(随机弹出)。
// 获取操作对象
SetOperations<String, String> ops = stringRedisTemplate.opsForSet();
// 1. 添加元素 (SADD key member)
// 自动去重,重复的加不进去
ops.add("likes:article:1001", "user_a", "user_b", "user_c");
// 2. 判断是否是成员 (SISMEMBER key member)
// 判断有没有点赞过
Boolean isMember = ops.isMember("likes:article:1001", "user_a");
// 3. 获取所有成员 (SMEMBERS key)
// 注意:如果 Set 很大,慎用这个方法,会阻塞 Redis
Set<String> members = ops.members("likes:article:1001");
// 4. 获取集合大小 (SCARD key)
Long size = ops.size("likes:article:1001");
// 5. 随机获取一个元素 (SRANDMEMBER key)
// 抽奖场景
String luckyUser = ops.randomMember("likes:article:1001");
// 6. 移除元素 (SREM key member)
ops.remove("likes:article:1001", "user_a");
opsForZSet)ZSet 是 Redis 最有特色的数据结构,每个元素带一个分数(Score),自动排序。
// 获取操作对象
ZSetOperations<String, String> ops = stringRedisTemplate.opsForZSet();
// 1. 添加元素并设置分数 (ZADD key score member)
// 比如游戏排行榜,分数是 100
ops.add("rank:game", "player_a", 100);
// 2. 增加分数 (ZINCRBY key increment member)
// 玩家又得了 10 分
ops.incrementScore("rank:game", "player_a", 10);
// 3. 获取排名(从低到高) (ZRANK key member)
Long rank = ops.rank("rank:game", "player_a");
// 4. 获取倒序排名(从高到低) (ZREVRANK key member)
// 通常排行榜都是看这个,返回值 0 就是第一名
Long reverseRank = ops.reverseRank("rank:game", "player_a");
// 5. 获取指定范围的元素(带分数) (ZREVRANGE key start end)
// 获取前 10 名
Set<TypedTuple<String>> top10 = ops.reverseRangeWithScores("rank:game", 0, 9);
for (TypedTuple<String> tuple : top10) {
System.out.println("用户:" + tuple.getValue() + " 分数:" + tuple.getScore());
}
这些方法不属于 opsForXxx,而是直接由 stringRedisTemplate 调用的,针对 Key 本身的操作。
// 1. 判断 Key 是否存在 (EXISTS)
Boolean hasKey = stringRedisTemplate.hasKey("user:name");
// 2. 删除 Key (DEL)
// 既可以传单个,也可以传 Collection 删除多个
stringRedisTemplate.delete("user:name");
// 3. 设置过期时间 (EXPIRE)
// 非常重要!给已经存在的 Key 设置过期时间
stringRedisTemplate.expire("user:name", 10, TimeUnit.MINUTES);
// 4. 获取过期时间 (TTL)
// 返回剩余秒数,-1 表示永不过期,-2 表示 Key 不存在
Long expire = stringRedisTemplate.getExpire("user:name");
// 5. 模糊搜索 Key (KEYS)
// 生产环境严禁使用 keys *,会卡死 Redis!
// 开发环境可以用用
Set<String> keys = stringRedisTemplate.keys("user:*");
set 的时候尽量一步到位设置好过期时间(使用 set(key, value, time, unit))。不要先 set 再 expire,因为如果 set 完程序崩了,这个 Key 就变成永不过期的“僵尸数据”了。ops.set。可以使用 opsForValue().multiSet(map) 或者使用 Pipeline(管道)技术,性能提升巨大。业务:模块:ID,这样在查看数据时非常清晰。掌握了上面这些,你基本上能应对 95% 的 Redis 开发需求了!如果有具体场景不知道用哪个方法,可以随时问我。
StringRedisTemplate 里的 Redis Stream,可以把它理解成:
Redis 提供的一种消息流数据结构,而 StringRedisTemplate 是 Spring 用来操作它的工具。
如果你正在看这句代码:
stringRedisTemplate.opsForStream().add(SHORT_LINK_STATS_STREAM_TOPIC_KEY, producerMap);
它的意思就是:
往 Redis 的某个 Stream 里追加一条消息。
Redis Stream 是 Redis 5.0 引入的数据结构,专门适合做:
它和 Redis 里常见的 String / List / Set / Hash / ZSet 不一样,特点是:
你可以不断往里面写消息,每条消息都会有一个唯一 ID。
例如:
short-link:stats-stream
├── 1710000000000-0 {statsRecord: "..."}
├── 1710000000100-0 {statsRecord: "..."}
└── 1710000000200-0 {statsRecord: "..."}
这里每条消息都有一个像 1710000000000-0 这样的 ID。
因为 Stream 支持:
而 List 更像简单队列,能力没这么强。
Pub/Sub 是实时广播:
而 Stream 可以:
所以它更像真正的消息队列。
StringRedisTemplate 和 Redis Stream 的关系StringRedisTemplate 是 Spring Data Redis 提供的模板类。
它本来是操作 Redis 的通用工具,专门处理:
StringString当你调用:
stringRedisTemplate.opsForStream()
拿到的是一个专门操作 Redis Stream 的接口。
也就是说:
StringRedisTemplate
-> opsForStream()
-> 对 Redis Stream 做增删读写
opsForStream().add(...)你看到的代码:
stringRedisTemplate.opsForStream().add(SHORT_LINK_STATS_STREAM_TOPIC_KEY, producerMap);
可以拆成三部分看。
opsForStream()表示我要操作 Redis 的 Stream 类型。
类似于:
opsForValue():操作 StringopsForHash():操作 HashopsForList():操作 ListopsForSet():操作 SetopsForZSet():操作 ZSetopsForStream():操作 StreamSHORT_LINK_STATS_STREAM_TOPIC_KEY这是 Stream 的 key,也就是消息流名字。
比如可能是:
short-link:stats:stream
你可以把它理解成“主题”或者“队列名”。
producerMap这是消息体。
例如:
Map<String, String> producerMap = new HashMap<>();
producerMap.put("statsRecord", "{\"fullShortUrl\":\"xxx\",\"uv\":\"yyy\"}");
这表示往 Stream 里放一条字段结构的消息。
Redis Stream 的一条消息本质就是:
field1=value1, field2=value2, ...
假设执行:
Map<String, String> map = new HashMap<>();
map.put("statsRecord", "{\"fullShortUrl\":\"a.com/abc\",\"uv\":\"u123\"}");
stringRedisTemplate.opsForStream().add("short-link:stats-stream", map);
Redis 里大概就会有这样一条记录:
Stream key: short-link:stats-stream
ID: 1712345678901-0
fields:
statsRecord = {"fullShortUrl":"a.com/abc","uv":"u123"}
也就是说:
short-link:stats-streamstatsRecord -> JSON字符串Map<String, String>因为 Stream 的消息结构不是一个纯字符串,而是一个“字段集合”。
比如一条消息可以长这样:
name=tom
age=20
city=beijing
所以 Spring 这里通常让你传:
Map<String, String>
很自然。
在你这个项目里,消息体只有一个字段:
producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
意思是把整个统计对象转成 JSON,再塞到一个字段里。
这也是很常见的做法。
就是你现在这句:
stringRedisTemplate.opsForStream().add("streamKey", map);
作用:
往 Stream 追加一条消息。
Spring 里可以读 Stream 消息,例如:
List<MapRecord<String, Object, Object>> records =
stringRedisTemplate.opsForStream().read(
StreamReadOptions.empty().count(1),
StreamOffset.fromStart("streamKey")
);
表示从头读取消息。
Redis Stream 支持消费组,适合多个消费者共同处理消息。
例如 Redis 原生命令:
XGROUP CREATE streamKey group1 0
作用:
streamKey 创建一个消费组 group1Spring 里也支持对应操作。
消费者组读取通常类似:
XREADGROUP GROUP group1 consumer1 COUNT 1 STREAMS streamKey >
含义:
group1 这个组来消费consumer1> 表示读新消息消费成功后要确认:
XACK streamKey group1 1712345678901-0
这样 Redis 才知道这条消息已经被成功处理。
每条消息都会有一个唯一 ID,一般格式:
时间戳-序号
例如:
1712345678901-0
1712345678901-1
1712345679000-0
含义是:
作用:
像你这个短链接项目里,访问统计包括:
这些统计有个特点:
短链跳转是高频行为,每次访问都可能要记录统计。
用户访问短链时,最重要的是:
如果同步写数据库统计,会拖慢跳转速度。
所以可以:
add 一条消息到 Stream这就是典型的:
主链路快响应,统计异步化。
Redis Stream 可以做消息队列,但它不是专门的大型 MQ 中间件。
所以一般:
代码:
@Component
@RequiredArgsConstructor
public class ShortLinkStatsSaveProducer {
private final StringRedisTemplate stringRedisTemplate;
public void send(Map<String, String> producerMap) {
stringRedisTemplate.opsForStream().add(SHORT_LINK_STATS_STREAM_TOPIC_KEY, producerMap);
}
}
可以直接理解成:
这是一个“短链接统计消息生产者”。
stringRedisTemplate 用来操作 Redis。
把统计数据写入 Redis Stream。
每来一次短链访问统计,就往 Redis Stream 里塞一条消息,后续消费者异步处理。
你可以把它类比成:
send(statsRecord)
相当于:
把一条“待处理统计任务”放进队列。
短链跳转时,不直接写统计库,而是说:
先把统计任务记到消息流里,稍后慢慢处理。
如果面试官问你:StringRedisTemplate** 操作 Redis Stream 是干什么的?**
你可以这样答:
StringRedisTemplate是 Spring Data Redis 提供的 Redis 操作模板。
调用opsForStream()可以操作 Redis 的 Stream 数据结构。
Stream 是 Redis 5.0 提供的消息流,适合做轻量级消息队列。
在我们的短链接项目里,访问统计不会同步写库,而是先通过opsForStream().add(...)把统计消息写入 Redis Stream,再由消费者异步处理,这样可以降低短链跳转主链路延迟。
stringRedisTemplate.opsForStream().add(SHORT_LINK_STATS_STREAM_TOPIC_KEY, producerMap);
就是:
向 Redis 的短链接统计消息流里追加一条消息。
这条消息里装的是:
本次短链接访问的统计数据。
如果你愿意,我下一条可以继续给你讲:
Redis Stream 的生产者、消费者、消费组、ack 机制整套流程图式解释。
知识笔记会随着实践和认知变化持续更新,不代表最终结论。