redis篇(理论篇),2.中期:垂直拆分与
redis篇(理论篇),2.中期:垂直拆分与
目录
- 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[]; // 字符数组 }
- 记录长度(O(1)取长度,不用遍历)
- 预分配空间(减少频繁扩容)
- 二进制安全(允许存、0)
- 自动扩容&缩容 --->简单来说,SDS是高性能、可变长、带长度标记的字符串,比传统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" # 给用户打兴趣标签
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 14.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场景:用户签到、用户行为标记(是否点赞、是否浏览过)、活跃用户统计等。
用户点评