目录
- mysql的演进
- NoSQL
- redis概述
- redis安装
- redis基本知识
一、MySQL的演进过程
1. 初期:单机架构,简单高效
- 90年代,随着互联网初期发展,单机数据库架构(APP → Middleware → MySQL)完全够用,压力小,易于维护。
2. 中期:垂直拆分与读写分离
- 随着业务量增加,单机无法满足需求,出现了垂直拆分(不同模块用不同数据库)与读写分离(主库写,从库读)策略。
3. 缓存层优化
- 进一步为了减轻MySQL压力,开始在应用层引入文件缓存机制。
- 后续逐渐普及Memcached作为分布式缓存,大大提高了系统性能。
4. 后期:水平扩展与集群
- 业务不断膨胀,最终演变为:
APP → Middleware → 缓存层(Memcached) → 多个MySQL集群(集群1、集群2、集群3)
- 水平拆分(分库分表)+ 多实例集群成为标配。
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环境:
sudo apt-get install redis-server systemctl start redis redis-cli -p 6379
- 测试性能:
redis-benchmark -h localhost -p 6379 -c 100 -n 10000 (测试100个并发连接,每个连接发送10000次请求)
- 默认端口:6379
- 测试连接:
ping → pong
四、Redis基础知识
1. 数据库管理
- 默认16个逻辑数据库(编号0~15)
- 常用命令:
select 0 # 切换数据库 DBSIZE # 查看当前数据库中Key数量 flushdb # 清空当前数据库 flushall # 清空所有数据库
2. 单线程但高性能
- Redis采用单线程事件循环处理请求,避免多线程锁竞争,效率极高。
- 性能瓶颈往往在于内存带宽和网络带宽,不是CPU!
3. 存储机制
- 主要存储在内存(RAM),访问速度极快。
- 持久化方式(可选):
- RDB快照:周期性生成内存数据快照到磁盘。
- AOF日志:记录每一次写操作,支持按日志重放恢复。
- 数据结构组织方式:
RedisDB └── dict哈希表 ├── key1 → value1 ├── key2 → value2 └── ...
4. 内存管理与淘汰策略
- 配置 maxmemory 限制最大内存占用。
- 超出后可按策略淘汰数据:
- LRU(最近最少使用)
- LFU(最少访问次数)
- TTL(按过期时间)
5. Redis的常见误区
误区正确理解多线程一定比单线程快错!多线程有锁竞争、上下文切换开销Redis单线程性能不足错!Redis单线程可支撑几十万QPS,瓶颈一般是带宽和内存 五、Redis五大数据类型及常用命令
1. String(字符串类型)
1.1 基础操作
最基础、最常用的数据类型,类似单纯的Key-Value存储。
- 基本操作
set name "hh" get name
- 过期时间设置
expire name 10 # 设置10秒后过期 ttl name # 查看剩余生存时间
type name
- 字符串追加
append name "haha" strlen name
set views 0 # 初始浏览量设为0 incr views # 自增1 decr views # 自减1 INCRBY views 10 # 设置步长为10
getrange key1 0 3 # 截取0-3下标的字符串
getrange key1 0 -1
setrange key2 1 xx # 将下标为1后面插入xx
setex key3 30 "hello" # 设置过期时间,30秒过期
setnx mykey "redis" # 当前值不存在再设置(在分布式锁常试用)若存在值则返回0(创建失败);不存在则创建返回1
mset k1 v1 k2 v2 k3 v3
mget k1 k2 k3
msetnx k1 v1 k4 v4 # k1存在,k4成功,但最后还是返回0(是一个原子性操作)
set user:1 {name: zhangsan, age :3} # 设置一个user:1对象 值为json字符串来保存对象
mset user:1:name zhangsan user:1:age 2
mget user:1:name user:1:age
getset db redis # 若不存在值,则返回nil
get db # redis
getset db mongodb # 若存在值,获取原来的值,redis
get db # mongodb
1.2 底层原理
String是redis中最基础、最常用的数据类型。本质上是二进制安全的字节序列(byte array)。可以存:文本字符串、整数、二进制数据(如一张图片的二进制流)。
内容特点 | 底层结构 | 特点 | 小的简单字符串(短文本) | SDS(简单动态字符串) | 内存连续、长度可变 | 能表示为整数 | 直接以整数编码 | 省内存,操作更快 | 【SDS是什么:Simple Dynamic String】redis自定义的一种字符串结构,比C语言的裸char数组更安全、更高效。- struct SDS {
- int len; // 实际使用的长度
- int alloc; // 分配的总容量
- char buf[]; // 字符数组
- }<br>- 记录长度(O(1)取长度,不用遍历)<br>- 预分配空间(减少频繁扩容)<br>- 二进制安全(允许存、0)<br>- 自动扩容&缩容 --->简单来说,SDS是<strong>高性能、可变长、带长度标记</strong>的字符串,比传统C字符串安全得多。
复制代码 【整数优化(int编码)】如果你存的是一个纯整数,比如SET key 10086,redis底层并不会用SDS,而是直接把值存成int64类型,编码方式设置为int编码。好处是:内存更小(只有8字节),运算更快(比如INCR直接整数加法,不需要字符串转数字)
1.3 注意事项
- SDS预分配导致内存浪费:SDS在扩容时,会预留一部分多余空间,为了减少频繁扩容,如果存大量小字符串,整体可能存在不少内存冗余。
- redis支持最大512MB的字符串,但如果存一个超长字符串,每次GET/SET都要传输这么一大片数据,网络I/O、redis单线程处理、内存拷贝压力都会暴增。-->把大对象切分成多个小key分片存储,或直接用外部存储(如OSS),redis里只存索引ID。
- String除了能存文本,还能存 压缩包、图片、密文,但应用层自己要负责编码/解码(redis不理解格式,只当作byte处理)
- redis的SET操作是覆盖式的--->使用SETNX(不存在时才写入)或则和GETSET等原子指令保护。
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" # 给用户打兴趣标签<br>SMEMBER user: 123: tags # 查询用户有哪些标签<br>
复制代码 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(压缩列表)代替哈希表+跳表:
<ul>ziplist是一个连续内存块,减少内存碎片,提高小数据场景下的效率。默认触发条件:元素数量 5 -----> 9 -----> 13Level 1: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15[/code]<ul>
假如要查询11:从最高层2的头节点(1)开始;然后看下一个节点的值(5);再看下一个节点的值(9);再往右跳到13,不能跳了; 下到level 1,当前还在9,往右边移动,右边是11,刚好找到。(如果level 2 走到头还没找到,就降级到level 1继续找。)
插入元素6:随机抛硬币来决定它的层数(如第一次抛硬币得到正面,则加一层,第二次抛为反面,则停止);然后在每一层找到插入位置(在level 2找到5 3 -> 5 -> 6 -> 7 -> 9[/code]
- 删除元素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,可以存储经纬度信息,并支持:按城市名查经纬度、查询指定范围内的城市、计算两个点之间的距离。- ZADD game:rankings 1500 player_001
- ZADD game:rankings 2200 player_002
复制代码 场景:附近商家、附近车主、外卖派单。
6.2 HyperLogLog
用于估算集合中不重复元素的数量(即去重后有多少个元素)。误差 1# 统计 key 中值为 1 的 bit 数(即有多少人“签到”了)BITCOUNT user_sign:20240509 => 2[/code] 场景:用户签到、用户行为标记(是否点赞、是否浏览过)、活跃用户统计等。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |