redis篇(理论篇)
目录
- mysql的演进
- NoSQL
- redis概述
- redis安装
- redis基本知识
一、MySQL的演进过程
1. 初期:单机架构,简单高效
-
90年代,随着互联网初期发展,单机数据库架构(APP → Middleware → MySQL)完全够用,压力小,易于维护。
2. 中期:垂直拆分与读写分离
-
随着业务量增加,单机无法满足需求,出现了垂直拆分(不同模块用不同数据库)与读写分离(主库写,从库读)策略。
3. 缓存层优化
-
进一步为了减轻MySQL压力,开始在应用层引入文件缓存机制。
-
后续逐渐普及Memcached作为分布式缓存,大大提高了系统性能。
4. 后期:水平扩展与集群
-
业务不断膨胀,最终演变为:
-
水平拆分(分库分表)+ 多实例集群成为标配。
5. 存储引擎演变
-
早期 MySQL 采用MyISAM引擎(表锁,效率低,事务支持差)。
-
后来转向InnoDB引擎(行级锁,支持事务、崩溃恢复、外键约束)。
6. 新问题:应对更复杂、变化更快的数据需求
-
随着移动互联网、社交应用爆发,MySQL遇到瓶颈。
-
引入NoSQL体系,存储如用户画像、地理位置、社交关系等数据,减轻关系型数据库压力。
二、NoSQL的兴起与应用
1. 什么是NoSQL?
-
NoSQL,全称 Not Only SQL,即“不仅仅是SQL”。
-
特点:
-
格式灵活:基于 Key-Value 或文档,数据类型多样。
-
无需固定表结构:方便迭代开发,支持敏捷开发和极限编程。
-
高扩展性和高性能:适应海量数据场景。
-
【扩展:敏捷开发】强调的是快速迭代、小步快跑、持续反馈、持续优化,而不是像传统开发那样一开始就写一大堆文档、一次性做完。项目分成很多小周期(通常2-4周叫做一个Sprint),每个周期都交付一个可以运行的版本。强调人与人的沟通(团队交流比写文档更重要)。
举个例子:
比如开发一个电商网站:
-
第一个Sprint:只做出首页+商品列表的最小功能;
-
第二个Sprint:增加商品详情页+简单下单功能;
-
第三个Sprint:接入支付;
-
中间每两周就交付一个小版本,客户可以体验,随时调整方向,比如改UI、换支付方式。
这样比起传统一次性做完一整年开发,灵活得多,也风险小得多!
【扩展:极限编程】简称 XP,是敏捷开发的一种具体实践方法。
2. 关系型数据库(RDBMS)与NoSQL的区别
| 对比点 | 关系型数据库(RDBMS) | NoSQL |
|---|---|---|
| 数据结构 | 结构化(表) | 灵活(键值、文档、列存、图) |
| 查询语言 | SQL | 多样(API调用、类SQL) |
| 数据一致性 | 强一致性 | 最终一致性(CAP理论) |
| 事务支持 | 支持ACID事务 | 弱化事务以换取扩展性 |
| 典型应用 | 金融、传统企业 | 社交、内容分发、日志存储 |
企业典型组合:MySQL + Redis
3. NoSQL应用示例(以淘宝商品页为例)
-
商品基本信息、评论(结构化文档) → MongoDB
-
商品图片(大文件存储) → FastDFS、OSS
-
搜索关键词、商品检索 → ElasticSearch
-
热门商品、秒杀活动缓存 → Redis、Tair、Memcached
-
交易、支付系统 → 仍以关系型数据库为主(如MySQL)
4. NoSQL四大主流分类
| 类型 | 代表产品 | 说明 |
|---|---|---|
| 键值(K-V)存储 | Redis、Tair、Memcached | 快速访问,缓存场景最佳 |
| 文档型数据库 | MongoDB | 类似JSON存储,灵活结构 |
| 列式数据库 | HBase、Cassandra | 适合大规模写入和分析 |
| 图数据库 | Neo4j | 适合存储复杂网络关系(社交、推荐系统) |
三、Redis详解
1. 什么是Redis?
-
全称:Remote Dictionary Server(远程字典服务)
-
特点:
-
基于内存存储,极致高效
-
支持持久化
-
丰富的数据结构
-
用作缓存、数据库、消息中间件
-
支持发布订阅、地理位置分析、计时器、计数器等应用
-
2. Redis安装与使用(快速上手)
-
Windows环境:下载 redis-cli.exe、redis-server.exe、redis-benchmark.exe 等。
-
Linux环境:
-
测试性能:
(测试100个并发连接,每个连接发送10000次请求)
-
默认端口:6379
-
测试连接:
四、Redis基础知识
1. 数据库管理
-
默认16个逻辑数据库(编号0~15)
-
常用命令:
2. 单线程但高性能
-
Redis采用单线程事件循环处理请求,避免多线程锁竞争,效率极高。
-
性能瓶颈往往在于内存带宽和网络带宽,不是CPU!
3. 存储机制
-
主要存储在内存(RAM),访问速度极快。
-
持久化方式(可选):
-
RDB快照:周期性生成内存数据快照到磁盘。
-
AOF日志:记录每一次写操作,支持按日志重放恢复。
-
-
数据结构组织方式:
4. 内存管理与淘汰策略
-
配置
maxmemory限制最大内存占用。 -
超出后可按策略淘汰数据:
-
LRU(最近最少使用)
-
LFU(最少访问次数)
-
TTL(按过期时间)
-
5. Redis的常见误区
| 误区 | 正确理解 |
|---|---|
| 多线程一定比单线程快 | 错!多线程有锁竞争、上下文切换开销 |
| Redis单线程性能不足 | 错!Redis单线程可支撑几十万QPS,瓶颈一般是带宽和内存 |
五、Redis五大数据类型及常用命令
1. String(字符串类型)
1.1 基础操作
最基础、最常用的数据类型,类似单纯的Key-Value存储。
-
基本操作
-
过期时间设置
- 类型查询
-
字符串追加
- 自增/自减
2. List数据类型
2.1 基础操作
在redis里面,可以把list当成栈、队列、阻塞队列。list的操作还是push和pop比较多。

往尾部添加元素





rpoplpush:组合命令,从一个列表的尾部取出元素,加入到另一个列表的头部

lset:存在则相当于更新,不存在则报错。

linsert:将某个具体的值插入到列表具体的值前面/后面。

【总结】实际上是一个链表,左边和右边都可以插入。若key不存在,创建新的链表。若存在链表,则新增内容。若移除key,所有的value则消失。在两边插入或改动值,效率最高!中间元素,效率低一些。
消息排队:消息队列(LPUSH RPOP), 栈(LPUSH RPOP)
2.2 底层原理
redis中的list是有序的元素集合,支持从两端推入元素、从两端弹出元素、按下标访问、截取部分列表。典型的【队列】【栈】功能,都可以基于list实现。
| 场景 | 底层结构 | 特点 |
| 元素数量少且元素小 | ziplist压缩列表 | 连续内存块,节省空间 |
| 元素多或元素大 | quicklist快速列表 | 多个ziplist组成的双向链表 |
(redis 3.2之前是ziplist和linkedlist,之后旧统一成quicklist了)
2.3 典型应用
- 消息队列:生产者LPUSH入队,消费者RPOP出队,或者用BRPOP支持阻塞式出队(防止无数据时忙等)
- 任务异步处理:后台任务推送到list,慢慢异步处理,削峰填谷。
- 最新的消息/帖子按时间逆序存入list。
- 只保留最近N条记录,比如:浏览记录、搜索热词。
2.4 注意事项
- 大列表大致LRANGE阻塞:若list非常大,执行LRANGE 0 1000000这种操作,redis是单线程的,会阻塞服务器,影响其他请求。--->控制LRANGE的访问范围,比如分页查询每次只取100条;或者用SCAN类的方式按游标分页(虽然list本身没有SCAN,但可以模拟)
- 单词插入/删除过多元素时阻塞:比如LPOP/RPOP/PUSH一次处理大量元素时,操作时间是O(N)。可能导致长时间占用CPU,阻塞其他命令。--->尽量保证单词操作粒度小,若一定要批量操作,考虑通过流水线优化。
- list是双向链表(quicklist),随机访问效率低:按索引取元素(LINDEX),是链表遍历查找,时间复杂度是O(N)。越往后取,速度越慢。--->不要频繁LINDEX大索引位置。如果需要高效随机访问,考虑用Zset或Hash代替。
- 超大列表内存占用:若一个list积压了几百万条任务,内存消耗巨大,而且链表碎片化严重,还可能导致AOF日志巨长,恢复慢。-->加任务消费保护(限流、超时清理);给key设置合理过期时间ttl;定时清理无效任务。
3. Set数据类型
3.1 基本操作
Set中的值不能重复。无序不重复集合。

Sismember:查看某个元素是否在集合中。

获取元素的个数:Scard myset

移除元素:Srem myset hello

随机抽元素:Srandmember myset 1 # 随机抽取一个元素

删除指定的key,随机删除key:Spop myset # 随机删除

将一个指定的key移动到另外一个set集合中。

微博等共同关注(差集、交集、并集):

总结:A用户将所有关注的人放在一个集合中,粉丝也放在一个集合中,还可以求共同关注。
3.2 底层原理
Set是一种不允许元素重复的无序集合,支持常见集合操作。查询、插入、删除元素时间复杂度是O(1)。
| 元素数量和大小 | 底层数据结构 | 特点 |
| 数量小,元素是整数 | intset(整数集合) | 节省内存,连续存储 |
| 元素多或包含非整数 | 哈希表 | 快速查找,支持任意字符串 |
【举个例子】假如在redis里执行:SADD myset 1; SADD myset 2; SADD myset 3. 全是整数,数量只有3个,远小于512(默认set-max-intset-entries)。若这样要使用传统哈希表(需要哈希桶数组,每个元素还要单独存一份key,甚至有指针开销),小量元素时dict的内存浪费很明显。而intset直接用连续的一块内存,存放整数,没有指针也没有额外哈希开销,纯粹存数据。所以这时候用intset更好,每个元素占用内存极小,且intset是连续数组,可以直接二分查找(但哈希就要做哈希元素、哈希冲突处理,流程更复杂)。
redis设计得很灵活,当intset元素数量超过阈值(默认512个),或者插入了非整数元素,就会自动升级成dict,保证兼容性,不需要人工干预。
【Set的扩容机制】当intset超过一定数量或插入非整数元素,会自动转成哈希表。dict元素太多时自动扩容rehash,保持低负载因子,提高性能。(负载因子 = 当前元素个数/哈希桶总数量)简单来说就是每个哈希桶平均有多少元素,若负载因子太高,冲突就多,查找、插入的性能就会下降。所以为了让哈希表保持查找接近O(1)的速度,redis 要动态扩容,让桶变多,保持低负载因子。
(1)触发扩容条件:当redis中的哈希表满足以下情况之一时就会触发扩容:元素个数>哈希表大小(负载因子>1); 若当前执行BGSAVE、AOF rewrite等持久化动作期间,为了避免内存膨胀,负载因子上限提高到5。-->一般情况下,元素数一旦超过桶数量,就触发扩容。
(2)redis扩容不是简单地扩大桶数组,而是 新建一个更大的哈希表(一般是原来的2倍大小),把老哈希表的元素一个个搬迁(rehash)到新表里。这个过程叫做rehash(再哈希)。
(3)渐进式搬迁:rehash不会一次性把所有元素伴奏,而是每次写操作时顺便搬迁一点。(这样做的目的是防止一次性阻塞redis,避免卡顿,适合高并发环境)
- 假如现在桶的数量为4个,要插入的元素是:【k1, k2, k3, k4, k5】。当插入k5时,会触发扩容。这时候会新建一个哈希表,桶数量变成8个桶。
- ht[0]是旧表,ht[1]是新的8桶表。先搬桶0(k1,k1重新计算哈希值,放到新表对应的位置)--再搬桶2(k2重新计算哈希值,放到新表)--以此类推,慢慢把k3、k4搬走-->最后插入新元素(k5)直接插入到新表ht[1]-->所有元素搬迁完毕,ht[0]置空,rehash完成。
- 最后哈希表状态:只有一个新表Ht[1] 存在(8桶),元素分布更稀疏了,负载因子降低了,查询/插入效率提升。
3.3 典型应用举例
1. 用户标签管理
SADD user: 123:tags "sports" "reading" "coding" # 给用户打兴趣标签
SMEMBER user: 123: tags # 查询用户有哪些标签
2. 去重系统
SADD access:2025-04-28 192.168.1.1 # 统计每天独立IP访问 SADD access:2025-04-28 192.168.1.2 SCARD access:2025-04-28 # 统计当天UV(独立用户数)
3. 好友关系/关注关系
SINTER user:123:following user:456:following # 一个用户关注了哪些人,交集求共同好友
3.4 注意事项
1. 内存膨胀问题:当set中元素特别多时(比如几百万个用户),底层哈希表会不断扩容,占用大量内存。-->需要合理估计元素数量,可能时用HyperLogLog或bitmap来替代。
- 预估容量+分桶设计:在业务设计时,根据预期数据量,提前拆分成多个小集合。比如按天、按业务维度拆分
user:active:2025-04-28
user:active:2025-04-29 - 改用更轻量的数据结构:
- 若只需要去重/计数而不关心具体元素:用HyperLogLog代替,大幅降低内存(12KB级别常量大小)
- 若只需要判断是否存在,则用bitmap(位图)代替,极度压缩空间。
2. 集合运算消耗大:SUNION、SINTER、SDIFF在集合很大时,时间复杂度是O(N)-->可能导致redis阻塞,特别是超大集合千万级别。
- 尽量避免在超大集合上直接做集合运算;分组、分页处理,比如按用户id取mod分到不同集合,小集合运算再聚合结果。
- 在业务层做拆分,比如先拿集合元素处理,自己在后台异步慢慢计算交并集,而不是直接让redis做。
- 使用SCAN分批遍历集合,每次取一点,自己在应用层慢慢做集合逻辑。
3. 元素过大导致性能下降:若set中的元素本身很大,如超长字符串,哈希冲突变多,查询效率下降。
- 元素存索引而非大对象:详细数据放到其他地方,如用hash或string保存
- 尝试对元素进行简单编码压缩,如用bse64、哈希(MD5/SHA)表示
- 大对象分离到对象存储系统(如OSS),set里只存object id。
4. Hash数据类型
4.1 基础操作
key-Map集合,这时候的值是一个map集合。
存一个值、取值; 多次存值取值;获取所有的值。

删除某个值:Hdel myhash field1
看这个key里面有多少个map:Hlen myhash
判断某个key对应的值的key的value是否存在:Hexists myhash field1
只获得所有的field、value:Hkeys myhash、Hvals myhash
自增自减:Hincrby myhash filed3 1、Hdecrby myhash filed3 1
存在则创建失败,不存在则创建:Hsetnx filed1 xiaoq(报错)
应用:hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息。更适合于对象的存储,String更适合字符串存储。
4.2 底层原理
| 场景 | 底层结构 | 特点 |
| filed数量少且filed和value都很小 | ziplist | 内存连续,节省空间 |
| filed数量多或某个filed/value很大 | 哈希表 | 查找快,支持大规模数据 |
【ziplist】单块连续内存,把field和value依次紧凑存放:[field1][value1][field2][value2]...
节省内存,但查找要顺序扫描O(N)。适合小数据量场景,比如几十个短字段的小对象。
【dict】标准哈希表实现(桶+连败哦/rehash),和普通redis dict一致。支持快速查找O(1),支持大对象(很多filed没货field/value很长)
【自动切换机制】field数量超过hash-max-ziplist-entries(默认512),或某个field/value长度超过 hash-max-ziplist-value(默认64字节)。只要任一条件满足,Hash会自动从ziplist升级成dict。
4.3 应用
- 存储对象信息:如存用户资料、商品信息,每个hash就是一条记录。类似于数据库一行的记录。
- 数据分组管理:把同类数据打包放在同一个hash里,便于批量操作。
- 轻量级缓存:把热数据缓存成Hash,比如商品SKU属性信息,快速读取。
- 计数器集合:每个field是一个项目,value是数量,比如:
HINCRBY page:viewcount page1 1 HINCRBY page:viewcount page2 1
4.4 注意事项
- hash很大导致阻塞:若hash非常大,在执行HGETALL、HKEYS、HVALS这样的全量遍历命令,由于redis是单线程处理,容易导致阻塞整个服务器--->避免对超大hash做一次性全量操作;分批处理,如用HSCAN游标分页扫描。
- ziplist顺序查找慢:ziplist底层是线性扫描,若filed数量虽然少但访问频繁,线性查找有一定开销--->若访问特别频繁,主动让hash升级为dict,比如预设一定数量的元素怒,或主动插入大字段。
- 不合理压缩导致性能下降:如果Hash field和value很大,但仍勉强塞在ziplist里(未超过阈值),每次查找都要顺序扫描,性能下降---->调整配置参数
hash-max-ziplist-entries、hash-max-ziplist-value,适配自己业务场景。 - 大hash导致RDB/AOF膨胀:超大Hash在持久化(RDB快照或AOF日志)时,整个Hash要一次性序列化存盘,内存高峰、持久化慢。解决建议:大对象拆小对象(比如按用户ID分段存储)AOF重写周期合理控制。
5. Zset数据类型
底层原理:元素唯一、每个元素关联一个浮点型分数的集合,按照分数大小有序排列。
Zset主要由两部分结构组成(双结构设计):
- 哈希表:键(成员)、值(分数)-->作用:O(1)快速查找某个成员的分数。(纯哈希表不能保证有序!!)
- 跳表:按分数有序排列节点-->按分数范围查询、按排名查询效率高,支持快速区间查找。
- 多层索引
- 平均查询、插入、删除时间复杂度都是O(logN)
- 有序性天然支持范围操作,比如Zrangebyscore、Zrange等
当元素数量很少而每个元素内容很小时,redis用ziplist(压缩列表)代替哈希表+跳表:
- ziplist是一个连续内存块,减少内存碎片,提高小数据场景下的效率。默认触发条件:元素数量<128,成员和分数长度都比较短(小于64字节)。
- 配置项:zset-max-ziplist-entries、zset-max-ziplist-value
- 元素变多或变大时,会自动升级为跳表+哈希表。
【举个例子】比如一个游戏需要实时显示【玩家积分排行榜】,每个玩家有一个唯一的playID,且有一个不断变化的积分score。要支持:1)查询积分最高的前100名;2)某个玩家当前排名;3)玩家积分增加或减少;4)按积分范围查询玩家(如找出在5000-6000之间的玩家)。问:如何用redis Zset实现?
【步骤一:存储玩家积分】
ZADD game:rankings 1500 player_001 ZADD game:rankings 2200 player_002
【步骤二:更新玩家积分】
ZINCRBY game:rankings 200 player_001 # 玩家1积分增加200分
【步骤三:查询积分最高的前100名】
ZREVRANGE game: rankings 0 99 WITHSCORES # 返回分数最高的100个玩家及其积分
【步骤四:查询某个玩家当前排名】
ZREVRANK game:rankings player_001 # 返回玩家1当前的名次(注意是0-based排名)
【步骤五:查找积分在某个区间的玩家】
ZRANGEBYSCORE game:rankings 5000 6000 WITHSCORES # 查找积分在2000-6000分之间的玩家
为什么用Zset适合这种场景?——首先Zset是按分数排序,跳表结构,查询高效;插入/更新快速;可以直接查询某个元素的分数;也可以按范围查找。
【注意事项】
- 为什么Zset会按分数排序?——因为Zset底层使用跳表,每次插入、更新都会把元素放到正确位置。元素插入跳表,redis是保证他们在物理存储上是按照分数有序排列的。Zset里面除了跳表,还有一个哈希表,其负责O(1)查询成员对应的分数,不保证有序!!
- 跳表是什么?怎么查询?
- 在有序链表的基础上,加了多层“高速通道”,用空间换时间。(多加一些指针来加速索引)
- 随机层数设计:这里很巧妙!!每个节点在插入时,随机决定有多少层索引,假设每层索引的概率是1/2,即50%的概率只出现在level 1;25%的概率出现在level 2;12.5%的概率出现在level 3 ...。这样会形成一个自然的金字塔结构:上层节点很稀疏,少量关键节点快速跳跃;下层节点密集,细粒度精确查找。
- 为什么要随机:这样可以让跳表保持整体结构“自然平衡”,不用像红黑树、AVL树那样复杂地做旋转或重平衡。(具体说,假如每次插入的层数是固定的,比如总是在第一层,跳表就退化完成普通链表,查询就是O(N),很慢;若层数完全手动控制,也需要频繁调整。
- 这种概率性分布保证了:插入、删除简单(只改指针,不调节点);查询快;空间开销合理(不会层数太多),而且数学证明,这种随机跳表,99%以上概率查询时间不会超过O(logN)。
- redis中默认最大层数是32层(极少出现)
Level 2: 1 -----> 5 -----> 9 -----> 13 Level 1: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15
- 假如要查询11:从最高层2的头节点(1)开始;然后看下一个节点的值(5);再看下一个节点的值(9);再往右跳到13,不能跳了; 下到level 1,当前还在9,往右边移动,右边是11,刚好找到。(如果level 2 走到头还没找到,就降级到level 1继续找。)
- 插入元素6:随机抛硬币来决定它的层数(如第一次抛硬币得到正面,则加一层,第二次抛为反面,则停止);然后在每一层找到插入位置(在level 2找到5<6<9,插入到中间;在level 1查找,5<6<7,插入到中间)
- 插入6之后连接指针,跳表变成:
Level 2: 1 -------> 5 -----> 6 -----> 9 Level 1: 1 -> 3 -> 5 -> 6 -> 7 -> 9
- 删除元素5:从高层到低层寻找5,我们在第二层找到了5,在这里断开1->5的指针,重新连接1->6。再到level 1,找到5之后,断开指针,重新连接3->6。删除完成后,5彻底从跳表移除。
3. reid跳表有没有保护机制?(比如随机分布出现问题,比如极端都很低)
- 首先redis跳表最大层数默认是32层,定义在源码server.h里面。
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */ 也就是说,不管怎么随机,最多只能出现32层索引,防止跳表层数异常高导致查询变慢或浪费内存。为什么设置32?理论上32层可以支撑接近2^32个元素(40亿个节点)而仍保持查询效率在O(logN)。大多实际应用中,元素数量远达不到40亿。
- 受控的随机概率(ZSKIPLIST_P):redis控制每一层出现的概率为50%(概率银因子P=0.25或P=0.5,源码中是0.25: #define ZSKIPLIST_P 0.25)。每高一层,节点数量按1/4递减,非常快形成金字塔结构。为什么是0.25?因为0.25收敛得更快,高层节点更少,查询时需要跳更少次指针,减少跳跃操作,实际效率更高(在内存结构访问中尤其明显)
6. 三种特殊数据结构(后续补充)
6.1 Geospatial地理位置
redis提供了一套基于地理坐标的DPI,可以存储经纬度信息,并支持:按城市名查经纬度、查询指定范围内的城市、计算两个点之间的距离。
GEOADD china 116.40 39.90 "Beijing" GEOADD china 121.47 31.23 "Shanghai" GEOADD china 113.26 23.13 "Guangzhou" GEODIST china Beijing Shanghai km # 计算距离 GEORADIUS china 116.40 39.90 1000 km WITHDIST # 查询北京周围1000km内的城市
场景:附近商家、附近车主、外卖派单。
6.2 HyperLogLog
用于估算集合中不重复元素的数量(即去重后有多少个元素)。误差<1%,空间始终固定为~12KB。
PFADD uv_count user1 PFADD uv_count user2 PFADD uv_count user1 # 重复添加,不影响结果 PFCOUNT uv_count # 返回大概有多少唯一用户(=2)
场景:网站UV统计(unique visitor)、活跃用户数、独立IP数量统计;日志去重计数。
6.3 Bitmap
位图在redis中其实是对字符串类型的一种位级操作扩展,本质上是一个二进制数组。每个位只能是0或1,位数可以很多,理论上512MB的字符串可支持2^32个bit。
SETBIT user_sign:20240509 1001 1 # 表示用户1001签到 SETBIT user_sign:20240509 1002 1 SETBIT user_sign:20240509 1003 0 GETBIT user_sign:20240509 1001 => 1 # 统计 key 中值为 1 的 bit 数(即有多少人“签到”了) BITCOUNT user_sign:20240509 => 2
场景:用户签到、用户行为标记(是否点赞、是否浏览过)、活跃用户统计等。
浙公网安备 33010602011771号