欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

redis篇(理论篇),2.中期:垂直拆分与

来源: javaer 分享于  点击 32911 次 点评:105

redis篇(理论篇),2.中期:垂直拆分与


目录

  1. mysql的演进
  2. NoSQL
  3. redis概述
  4. redis安装
  5. 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

  • 商品图片(大文件存储) → FastDFSOSS

  • 搜索关键词、商品检索 → 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 典型应用

  1. 消息队列:生产者LPUSH入队,消费者RPOP出队,或者用BRPOP支持阻塞式出队(防止无数据时忙等)
  2. 任务异步处理:后台任务推送到list,慢慢异步处理,削峰填谷。
  3. 最新的消息/帖子按时间逆序存入list。
  4. 只保留最近N条记录,比如:浏览记录、搜索热词。

2.4 注意事项

  1. 大列表大致LRANGE阻塞:若list非常大,执行LRANGE 0 1000000这种操作,redis是单线程的,会阻塞服务器,影响其他请求。--->控制LRANGE的访问范围,比如分页查询每次只取100条;或者用SCAN类的方式按游标分页(虽然list本身没有SCAN,但可以模拟)
  2. 单词插入/删除过多元素时阻塞:比如LPOP/RPOP/PUSH一次处理大量元素时,操作时间是O(N)。可能导致长时间占用CPU,阻塞其他命令。--->尽量保证单词操作粒度小,若一定要批量操作,考虑通过流水线优化。
  3. list是双向链表(quicklist),随机访问效率低:按索引取元素(LINDEX),是链表遍历查找,时间复杂度是O(N)。越往后取,速度越慢。--->不要频繁LINDEX大索引位置。如果需要高效随机访问,考虑用Zset或Hash代替。
  4. 超大列表内存占用:若一个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 应用

  1. 存储对象信息:如存用户资料、商品信息,每个hash就是一条记录。类似于数据库一行的记录。
  2. 数据分组管理:把同类数据打包放在同一个hash里,便于批量操作。
  3. 轻量级缓存:把热数据缓存成Hash,比如商品SKU属性信息,快速读取。
  4. 计数器集合:每个field是一个项目,value是数量,比如:
HINCRBY page:viewcount page1 1
HINCRBY page:viewcount page2 1

4.4 注意事项

  1. hash很大导致阻塞:若hash非常大,在执行HGETALL、HKEYS、HVALS这样的全量遍历命令,由于redis是单线程处理,容易导致阻塞整个服务器--->避免对超大hash做一次性全量操作;分批处理,如用HSCAN游标分页扫描。
  2. ziplist顺序查找慢:ziplist底层是线性扫描,若filed数量虽然少但访问频繁,线性查找有一定开销--->若访问特别频繁,主动让hash升级为dict,比如预设一定数量的元素怒,或主动插入大字段。
  3. 不合理压缩导致性能下降:如果Hash field和value很大,但仍勉强塞在ziplist里(未超过阈值),每次查找都要顺序扫描,性能下降---->调整配置参数 hash-max-ziplist-entrieshash-max-ziplist-value,适配自己业务场景。
  4. 大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是按分数排序,跳表结构,查询高效;插入/更新快速;可以直接查询某个元素的分数;也可以按范围查找。

   【注意事项】

  1. 为什么Zset会按分数排序?——因为Zset底层使用跳表,每次插入、更新都会把元素放到正确位置。元素插入跳表,redis是保证他们在物理存储上是按照分数有序排列的。Zset里面除了跳表,还有一个哈希表,其负责O(1)查询成员对应的分数,不保证有序!!
  2. 跳表是什么?怎么查询?
  • 在有序链表的基础上,加了多层“高速通道”,用空间换时间。(多加一些指针来加速索引) 
  • 随机层数设计:这里很巧妙!!每个节点在插入时,随机决定有多少层索引,假设每层索引的概率是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

  场景:用户签到、用户行为标记(是否点赞、是否浏览过)、活跃用户统计等。

相关栏目:

用户点评