前言
什么是Redis?
Redis(Remote Dictionary Service,远程字典服务)是一个开源的内存数据库,也是目前最流行的KV(Key-Value)数据库之一。
它与传统数据库最大的区别在于:数据存储在内存中 ,而不是磁盘上。这使得Redis的读写速度极快,官方宣传比磁盘快约10万倍。
举一个直观的例子:
- 从内存读取数据:微秒级(μs)
- 从磁盘读取数据:毫秒级(ms)
差了整整1000倍,实际场景中差距可能更大。
Redis和数据结构数据库
很多人把Redis叫做"KV数据库",但这个说法不够准确。Redis的全称是Remote Dictionary Service(远程字典服务),它的value支持丰富的数据结构,因此更准确地说,Redis是一个数据结构数据库。
KV数据库和数据结构数据库的区别:
| 类型 | 操作方式 | 示例 |
|---|---|---|
| KV数据库 | 通过key找到整个value | 简单键值对,一个key对应一个值 |
| 数据结构数据库 | 通过key找到value,再通过内部结构操作 | key对应一个list/hash/set等复杂结构 |
Redis的应用定位
Redis通常不作为主力数据库使用,而是作为缓存层配合MySQL等磁盘数据库一起使用:
用户请求 → Redis缓存(命中) → 直接返回
↓(未命中)
→ MySQL磁盘数据库 → 写入Redis缓存 → 返回
这种架构被称为Cache-Aside模式,是Redis最经典的使用方式。
一、Redis的通信模式
请求-响应模式
Redis采用请求-响应模式工作:
- 建立连接:客户端先与Redis服务器建立TCP连接
- 发送指令:客户端发送操作指令(指令中包含要操作的key)
- 等待响应:Redis执行指令后返回结果
- 处理结果:客户端解析结果
注意:Redis是单线程执行的,但基于IO多路复用(Linux的epoll/kqueue),可以同时处理大量并发连接。这意味着Redis不会出现线程安全问题。
启动Redis
bash
redis-server # 启动Redis服务器(默认端口6379)
redis-cli # 启动Redis客户端(连接本地6379)
连接远程Redis:
bash
redis-cli -h 192.168.1.100 -p 6379
基本命令练习
bash
PING # 测试连接,返回PONG
PING "Hello Redis" # 返回 "Hello Redis"
二、Redis数据类型全景图
在深入学习之前,先来看一下Redis的整体数据结构体系:
Redis数据类型
├── String(字符串)
├── List(列表)
├── Hash(哈希)
├── Set(集合)
├── ZSet(有序集合)
└── 特殊类型
├── Bitmaps(位图)
├── HyperLogLog(基数统计)
├── Geospatial(地理位置)
└── Stream(流)
本文重点介绍前五种基本数据类型,它们是Redis的骨架。
如何选择数据结构?
这个问题几乎是Redis面试必问题。简单总结:
| 需求 | 推荐类型 |
|---|---|
| 存储单个字符串/数字 | String |
| 存储有序列表(队列、栈) | List |
| 存储对象/属性变更频繁的数据 | Hash |
| 去重、集合运算(好友关系) | Set |
| 排序+去重(排行榜) | ZSet |
三、String(字符串)
3.1 什么是String
String是Redis最基本的数据类型 ,它是安全的二进制字符串。
"安全"是什么意思?
在C语言中,字符串以\0作为结束标志。如果字符串内容本身包含\0,程序可能会被截断。Redis的String用长度字段来描述字符串,而不是依赖特定分隔符,所以可以存储任何二进制数据(图片、视频、音频、压缩包等)。
String的最大长度是512MB。
3.2 底层实现原理
Redis的String底层实现叫做SDS(Simple Dynamic String,简单动态字符串),它比C语言的字符串更高效和安全。
SDS的结构(Go语言风格的伪代码):
c
struct sdshdr {
int len; // 已使用长度
int free; // 剩余可用长度
char[] buf; // 实际存储字符串的数组
};
这样做有什么好处?
- O(1)获取字符串长度 :直接读
len字段,不用遍历计数 - 防止缓冲区溢出:扩容前先检查空间,不够就自动扩展
- 减少内存重分配 :通过
free字段记录剩余空间,避免频繁扩容
3.3 扩容策略
Redis的String扩容遵循以下规则:
当字符串长度 < 1MB时
→ 扩容策略:空间翻倍(free = len)
当字符串长度 >= 1MB时
→ 扩容策略:每次只增加1MB(free = 1MB)
这样设计是为了平衡内存使用和扩容频率。
3.4 常用命令详解
bash
# 基本操作
SET key value # 设置单个key-value
GET key # 获取value
MGET key1 key2 key3 # 批量获取
MSET key1 value1 key2 value2 # 批量设置
DEL key # 删除key-value
# 数字操作(Redis会自动识别数字字符串并支持自增)
INCR key # value + 1(原子操作)
INCRBY key 10 # value + 10
DECR key # value - 1
DECRBY key 5 # value - 5
INCRBYFLOAT key 3.5 # 浮点数自增
# 原子操作-setnx
SETNX key value # 仅当key不存在时设置(分布式锁常用)
SETEX key 10 value # 设置值并指定过期时间(10秒)
PSETEX key 1000 value # 设置值并指定过期时间(毫秒)
# 截取和替换
APPEND key "world" # 字符串追加
SUBSTR key 0 4 # 截取子串(返回"hello")
STRLEN key # 获取字符串长度
# 位图操作(进阶)
SETBIT key offset value # 设置某位的值(0或1)
GETBIT key offset # 获取某位的值
BITCOUNT key # 统计位为1的数量
BITOP AND destkey key1 key2 # 位运算AND
3.5 String的应用场景
场景1:分布式锁
分布式锁是String最经典的应用之一。利用SETNX命令的原子性:
bash
# 加锁
SETNX lock:order:12345 "locked"
EXPIRE lock:order:12345 30 # 锁的过期时间30秒
# 更好的方式是原子操作(Redis 2.6.12+)
SET lock:order:12345 "locked" NX EX 30
# 解锁
DEL lock:order:12345
⚠️ 注意:解锁前应该检查value是否是自己设置的,防止误删别人的锁。更完善的做法是使用Lua脚本或Redisson框架。
场景2:计数器
bash
# 文章阅读量
INCR article:1001:reads
INCRBY article:1001:reads 10
# 点赞数
INCR like:post:12345
DECR like:post:12345
为什么要用Redis做计数器而不是直接更新数据库?
- 性能:Redis操作在内存中完成,比磁盘快10万倍
- 并发:Redis单线程天然保证原子性,高并发下不会出错
- 抗压:瞬时大量请求先在Redis累加,再异步落库,保护数据库
场景3:Session共享
传统Web服务如果有多个实例,Session存在单机内存中,用户每次访问可能负载均衡到不同机器,导致Session丢失。
bash
# 存储用户登录信息
SET session:user:10001 '{"userId":10001,"username":"mark","loginTime":"2024-01-01"}'
EXPIRE session:user:10001 3600 # 1小时后过期
场景4:缓存JSON对象
bash
# 存一个用户对象
SET user:10001:info '{"name":"mark","age":30,"city":"nanjing"}'
# 取出来使用
GET user:10001:info
⚠️ 什么时候用String存JSON,什么时候用Hash?
用String的场景:
- 对象属性几乎不变化
- 每次都是整体读取、整体更新
- 不关心某个字段的独立性
用Hash的场景(推荐大多数情况):
- 属性经常变化(单独改一个字段)
- 需要按字段查询
- 需要对单个字段做自增等操作
四、List(列表)
4.1 什么是List
Redis的List是双向链表结构。这意味着:
- 头尾操作 (LPUSH、RPUSH、LPOP、RPOP)时间复杂度是 O(1)
- 中间元素查找 时间复杂度是 O(n)
- 索引访问 (通过下标访问)时间复杂度是 O(n)
4.2 链表结构示意
LPUSH RPUSH
↓ ↓
[head] ←→ [node1] ←→ [node2] ←→ [node3] ←→ [tail]
4.3 常用命令详解
bash
# 插入操作
LPUSH key element1 element2 element3 # 从左边插入(头插)
RPUSH key element1 element2 element3 # 从右边插入(尾插)
# 弹出操作
LPOP key # 从左边弹出(头删)
RPOP key # 从右边弹出(尾删)
# 范围查询
LRANGE key 0 -1 # 获取所有元素
LRANGE key 0 4 # 获取前5个元素
LRANGE key -3 -1 # 获取最后3个元素
# 长度
LLEN key # 获取列表长度
# 删除
LREM key count value # 删除count个value(count>0从头删,<0从尾删,=0删所有)
LTRIM key 0 4 # 只保留索引0-4的元素(常用于截断)
# 阻塞操作
BRPOP key timeout # 阻塞等待从右边弹出(队列空时等待)
BLPOP key timeout # 阻塞等待从左边弹出
# 高级操作
RPOPLPUSH source destination # 从source弹出尾元素,插入destination头部
BRPOPLPUSH source destination timeout # 阻塞版本
LINDEX key index # 按索引获取元素
LSET key index value # 按索引设置元素
LINSERT key BEFORE|AFTER pivot value # 在pivot前后插入
4.4 List的实现细节
Redis 3.2之前使用**ziplist(压缩列表)和linkedlist(双向链表)**两种实现:
- 元素少且短时用ziplist(节省内存)
- 元素多或长度大时用linkedlist
Redis 3.2之后统一使用quicklist结构:
- quicklist = 多个ziplist拼接而成的双向链表
- 兼顾内存效率和操作性能
4.5 应用场景
场景1:栈(Stack)
特性:先进后出(Last In First Out)
bash
LPUSH stack:data 1 2 3 4 5
# 链表:[5, 4, 3, 2, 1]
LPOP stack:data # 弹出5
LPOP stack:data # 弹出4
场景2:队列(Queue)
特性:先进先出(First In First Out)
bash
LPUSH queue:task task1 task2 task3
# 链表:[task3, task2, task1]
RPOP queue:task # 弹出task1(先入先出)
场景3:阻塞队列
bash
# 客户端A(消费者)- 阻塞等待消息
BRPOP queue:message 0 # 0表示无限等待
# 客户端B(生产者)- 发送消息
LPUSH queue:message "hello"
BRPOP和BLPOP会阻塞等待,直到队列中有元素可弹出。新版本支持多队列阻塞:
BRPOP queue1 queue2 queue3 0
场景4:异步消息队列
bash
# 生产者:不断发送消息
LPUSH queue:job job1 job2 job3 job4 job5
# 消费者:取出后截断,只保留最新5条
LPOP queue:job
LTRIM queue:job 0 4
⚠️ 这种方式不够优雅,实际生产环境推荐使用Redis Stream(Redis 5.0+)
场景5:朋友圈说说列表
bash
# 用户发了一条新说说,插入时间线头部
LPUSH user:10001:posts "post:20001"
LPUSH user:10001:posts "post:20002"
# 获取前10条说说
LRANGE user:10001:posts 0 9
五、Hash(哈希)
5.1 什么是Hash
Hash(哈希)类型是Redis中用来描述对象属性 的数据结构。它特别适合存储属性经常变化的对象。
Hash的key-value结构:
-
key:键名
-
field:字段(相当于对象中的属性名)
-
value:字段对应的值
key (哈希键) field (字段) value (值)
user:10001:info → name = "mark"
→ age = 30
→ height = 175
5.2 为什么不用String存对象?
先看看String存对象的痛点:
bash
# 用String存用户对象
SET user:10001 '{"name":"mark","age":30}'
# 现在要把age改成31
GET user:10001 # 取出完整的JSON字符串
# 解码 → 修改age字段 → 重新编码
SET user:10001 '{"name":"mark","age":31}' # 再存回去
问题:
- 步骤繁琐:需要先GET → 解码 → 修改 → 编码 → SET
- 并发问题:两个请求同时修改,可能产生覆盖
- 资源浪费:每次都要操作整个对象,哪怕只改一个字段
用Hash就简单多了:
bash
HMSET user:10001 name "mark" age 30 height 175
HINCRBY user:10001 age 1 # 直接对age字段+1,非常高效
5.3 常用命令详解
bash
# 基本操作
HSET key field value # 设置单个字段
HGET key field # 获取单个字段
HMSET key field1 value1 field2 value2 # 批量设置字段
HMGET key field1 field2 # 批量获取字段
HGETALL key # 获取所有字段和值
HDEL key field1 field2 # 删除字段
HLEN key # 获取字段数量
# 判断存在性
HEXISTS key field # 判断字段是否存在(返回1或0)
# 自增自减
HINCRBY key field 1 # 字段值+1(必须是整数)
HINCRBYFLOAT key field 0.5 # 字段值+0.5(可以是浮点数)
# 其他操作
HKEYS key # 获取所有字段名
HVALS key # 获取所有值
5.4 Hash的底层实现
Redis的Hash有两种底层实现:
- ziplist(压缩列表):当hash的字段数量少、value长度短时使用,节省内存
- hashtable(哈希表):当hash的字段数量多或value长度大时自动转换
配置参数:
hash-max-ziplist-entries 512 # 字段数量超过512时转换为hashtable
hash-max-ziplist-value 64 # 单个value超过64字节时转换
5.5 Hash的应用场景
场景1:存储对象属性
这是Hash最经典的应用,几乎所有Redis入门教程都会以此为例:
bash
# 存储文章信息
HSET article:1001 title "Redis入门教程" author "洛水水" views 0
# 文章阅读量+1
HINCRBY article:1001 views 1
# 获取文章信息
HGET article:1001 title
HGET article:1001 views
场景2:热点数据缓存
bash
# 缓存商品信息(热门商品会被频繁访问)
HSET product:10001 name "iPhone15" price 5999 stock 100
HSET product:10001 color "蓝色" memory "256G"
# 前端查询商品
HGET product:10001 price
为什么不用String存商品?
因为商品可能有10+个属性,用Hash可以只改某一个属性,不影响其他属性。
场景3:记录实时数据
bash
# 记录朋友圈点赞数、评论数、转发数
HSET moments:10001 likes 100 comments 20 forwards 5
# 用户点赞,点赞数+1
HINCRBY moments:10001 likes 1
# 用户取消点赞,点赞数-1
HINCRBY moments:10001 likes -1
六、Set(集合)
6.1 什么是Set
Set是无序且不重复的集合。
特性:
- 无序:元素没有顺序,插入顺序和存储顺序无关
- 唯一:同一个集合内不会有重复元素
- 支持交、并、差集运算
6.2 常用命令详解
bash
# 基本操作
SADD key member1 member2 member3 # 添加元素(重复元素自动忽略)
SREM key member1 member2 # 删除元素
SMEMBERS key # 获取所有元素
SCARD key # 获取元素数量(集合长度)
SISMEMBER key member # 判断元素是否存在(返回1或0)
# 随机操作
SRANDMEMBER key count # 随机获取count个元素(不删除)
SPOP key count # 随机弹出count个元素(会删除)
# 集合运算
SDIFF key1 key2 # 差集(key1有、key2没有的)
SDIFFSTORE destkey key1 key2 # 差集并存储到destkey
SINTER key1 key2 # 交集(两者都有的)
SINTERSTORE destkey key1 key2 # 交集并存储
SUNION key1 key2 # 并集(合并所有)
SUNIONSTORE destkey key1 key2 # 并集并存储
# 移动操作
SMOVE source destination member # 将元素从source移动到destination
6.3 Set的应用场景
场景1:微信朋友圈点赞用户列表(去重)
bash
# 用户10001点赞了说说12345
SADD moments:12345:likes 10001
SADD moments:12345:likes 10002
SADD moments:12345:likes 10001 # 重复点赞,自动忽略
# 检查用户是否已点赞
SISMEMBER moments:12345:likes 10001 # 返回1(已点赞)
# 取消点赞
SREM moments:12345:likes 10001
# 统计点赞数
SCARD moments:12345:likes
场景2:微信好友关系(交、并、差集)
bash
# 好友列表
SADD user:10001:friends 10002 10003 10004 10005
SADD user:10006:friends 10003 10004 10007 10008
# 共同好友(交集)
SINTER user:10001:friends user:10006:friends
# 结果:10003, 10004
# 我有但他没有的好友(差集)
SDIFF user:10001:friends user:10006:friends
# 结果:10002, 10005
# 可能认识的人(对方好友 - 已有好友 = 差集)
SDIFF user:10006:friends user:10001:friends
# 结果:10007, 10008
# 合并所有好友(并集)
SUNION user:10001:friends user:10006:friends
# 结果:10002, 10003, 10004, 10005, 10007, 10008
场景3:抽奖程序(随机抽取)
bash
# 参与抽奖的用户
SADD lottery:20240101 10001 10002 10003 10004 10005 10006 10007
# 抽取3个中奖者(不重复)
SRANDMEMBER lottery:20240101 3
# 结果:[10003, 10001, 10006](随机)
# 如果需要"抽完不再放回"
SPOP lottery:20240101 3
场景4:标签系统
bash
# 给文章打标签
SADD article:1001:tags "Redis" "数据库" "后端"
SADD article:1002:tags "MySQL" "数据库" "后端"
SADD article:1003:tags "Redis" "缓存" "后端"
# 找出同时打了"Redis"和"后端"标签的文章(需要遍历,但思路类似)
# 实际场景可以用Set存储标签下的文章ID列表
SADD tag:Redis articles 1001 1003
SADD tag:后端 articles 1001 1002 1003
SINTER tag:Redis:articles tag:后端:articles
七、ZSet(有序集合)
7.1 什么是ZSet
ZSet(有序集合)是Redis中最复杂的数据结构。
它有两个关键字段:
- member(成员):元素的值,用于唯一标识
- score(分数):用于排序,可以是浮点数
特点:
- 唯一性:同一个ZSet中,member唯一(和Set一样)
- 有序性:元素按score从小到大排序(可重复)
- 性能高:score排序基于**跳表(Skip List)**实现,查询/插入/删除都是O(log n)
7.2 为什么用跳表而不是红黑树?
| 数据结构 | 查找 | 插入 | 删除 | 范围查找 |
|---|---|---|---|---|
| 红黑树 | O(log n) | O(log n) | O(log n) | O(log n + k) |
| 跳表 | O(log n) | O(log n) | O(log n) | O(log n + k) |
跳表的优势:
- 实现更简单:比红黑树容易理解和实现
- 占用内存更少:不需要存储红黑树那样的平衡因子
- 范围查询更方便:跳表的底层是链表,范围遍历更自然
7.3 常用命令详解
bash
# 基本操作
ZADD key score1 member1 score2 member2 # 添加元素(带分数)
ZSCORE key member # 获取元素的分数
ZRANGE key 0 -1 # 按分数升序获取所有元素
ZREVRANGE key 0 -1 # 按分数降序获取所有元素
ZREVRANGE key 0 4 WITHSCORES # 获取前5名(带分数)
# 排名相关
ZRANK key member # 获取元素的排名(升序,0开始)
ZREVRANK key member # 获取元素的排名(降序,0开始)
# 删除操作
ZREM key member1 member2 # 删除元素
ZREMRANGEBYRANK key 0 4 # 删除排名0-4的元素(升序)
ZREMRANGEBYSCORE key min max # 删除分数范围内的元素
# 数量统计
ZCARD key # 获取元素数量
ZCOUNT key min max # 统计分数范围内的元素数量
# 高级操作
ZINCRBY key increment member # 给元素的分数加increment
ZRANGEBYSCORE key min max # 按分数范围获取元素
ZSCAN key cursor [MATCH pattern] [COUNT count] # 渐进式遍历
7.4 ZSet的应用场景
场景1:排行榜
这是ZSet最经典的应用场景。
bash
# 游戏战绩排行
ZADD game:1001:scores 9500 "user:10001"
ZADD game:1001:scores 8700 "user:10002"
ZADD game:1001:scores 10000 "user:10003"
# 获取前3名
ZREVRANGE game:1001:scores 0 2 WITHSCORES
# 结果:user:10003 10000, user:10001 9500, user:10002 8700
# 获取user:10001的排名
ZREVRANK game:1001:scores user:10001
# 结果:1(第二名)
# user:10001击败了对手,分数+500
ZINCRBY game:1001:scores 500 user:10001
# 实时查看自己的排名和分数
ZSCORE game:1001:scores user:10001
场景2:微信朋友圈评论列表(按时间排序+去重)
bash
# 用户评论,时间戳作为score
ZADD comments:post:12345 1704200000 "comment:50001"
ZADD comments:post:12345 1704200500 "comment:50002"
ZADD comments:post:12345 1704201000 "comment:50003"
# 获取前10条评论(按时间升序)
ZRANGE comments:post:12345 0 9 WITHSCORES
# 获取最新评论(按时间降序)
ZREVRANGE comments:post:12345 0 9 WITHSCORES
# 删除评论
ZREM comments:post:12345 comment:50002
场景3:热搜排行榜
bash
# 用户搜索了"Redis教程"
ZINCRBY hot:search 1 "Redis教程"
ZINCRBY hot:search 1 "Redis教程"
ZINCRBY hot:search 1 "MySQL优化"
# 获取当前热搜榜TOP10
ZREVRANGE hot:search 0 9 WITHSCORES
场景4:商品过滤与排序
bash
# 存储商品价格作为score
ZADD products:electronics 2999 "iPhone15"
ZADD products:electronics 4999 "iPhone15 Pro"
ZADD products:electronics 5999 "iPhone15 Pro Max"
# 价格从低到高排序
ZRANGE products:electronics 0 -1 WITHSCORES
# 价格从高到低排序
ZREVRANGE products:electronics 0 -1 WITHSCORES
# 筛选1000-4000元的商品
ZRANGEBYSCORE products:electronics 1000 4000
八、数据结构的选择决策树
需求场景
│
├─ 需要存储单个值(计数器、字符串)?
│ └─ YES → String
│
├─ 需要保证值的唯一性,且需要集合运算(交并差集)?
│ └─ YES → Set
│
├─ 需要排序(排行榜、热度排序、时间排序)?
│ └─ YES → ZSet
│
├─ 需要频繁修改"对象的某个属性"?
│ └─ YES → Hash
│
├─ 需要存储有序列表(栈、队列、消息队列)?
│ └─ YES → List
│
└─ 其他情况 → String(简单粗暴)
九、Key的规范化设计
Redis的key设计虽然灵活,但有一些最佳实践:
命名规范
bash
# 推荐:用冒号:分隔层级
user:10001:info # 用户基本信息
user:10001:friends # 用户好友列表
article:1001:comments # 文章评论
# 不推荐:全部写在一起
user_10001_info # 难以区分层级
userinfo10001 # 难以解析
为什么要用:分隔?
- 语义清晰:一看就知道是用户ID=10001的信息
- 便于管理 :图形化工具(如Redis Desktop Manager、Another Redis Desktop Manager)会按
:层级展示 - 支持通配符查询 :
KEYS user:10001:*可以列出某用户的所有key
常见前缀约定
bash
# 用户相关
user:{userId}:info # 用户信息
user:{userId}:friends # 好友列表
user:{userId}:followers # 粉丝列表
user:{userId}:articles # 用户文章
# 业务相关
article:{articleId}:info # 文章信息
article:{articleId}:comments # 文章评论
article:{articleId}:likes # 文章点赞
# 排行榜
rank:{type}:{date} # 某日排行
hot:search # 搜索热度
# 缓存
cache:{entity}:{id} # 缓存实体
session:{sessionId} # 会话信息
十、五大数据结构去重能力对比
| 数据结构 | 去重方式 | 说明 |
|---|---|---|
| String | key整体唯一 | 相同的key会覆盖 |
| Hash | field唯一 | 相同key+field会覆盖 |
| List | 不去重 | 允许重复元素 |
| Set | member唯一 | 相同的member只保留一个 |
| ZSet | member唯一 | 相同的member只保留一个 |
总结
| 数据结构 | 特性 | 典型应用 |
|---|---|---|
| String | 简单键值对、二进制安全、512MB | 缓存、计数器、分布式锁、Session |
| List | 双向链表、有序、头尾O(1) | 栈、队列、消息队列、时间线 |
| Hash | 键-字段-值、属性操作 | 对象存储、热点缓存 |
| Set | 无序、唯一、集合运算 | 标签系统、好友关系、去重 |
| ZSet | 有序、唯一、跳表实现 | 排行榜、评论排序、热度排行 |
核心原则:根据实际需求选择最合适的数据结构,而不是"什么场景都只用String"。
根据零声教育教学写作https://github.com/0voice