要看就看最好,16万字全网最硬核redis总结,谁赞成,谁反对?(被粉丝疯狂催更,已有人反馈看完专栏拿到大厂offer!!!)

我摊牌了,这篇文章,值得99%的人收藏!!!

阅读时间较长,后续会设置成仅粉丝可见,建议收藏起来慢慢看!!!

原创不易,千万不要白嫖,真香警告⚠️。

本文评论区会送出两本我精挑细选的Redis著作,想要的兄弟们评论区留言!!!

《Redis深度历险》

《Redis设计与实现》 


目录

1、入门

1.1 安装

1.1.1 创建安装目录

1.1.2 下载Redis

1.1.3 解压

1.1.4 安装gcc依赖

1.1.5 编译安装

1.1.6 修改配置文件

1.1.7 启动Redis

1.1.8 客户端

1.1.9 停止Redis

1.1.10 配置别名

2、五大基本数据类型

2.1 String

2.1.1 简介

2.1.2 string(字符串)相关介绍

2.1.3 string(字符串)的指令

2.2 list

2.2.1 简介

2.2.2 list(列表)相关介绍

2.2.3 list(列表)的指令

2.2.4 list(列表)深入理解

2.3 hash(字典)

2.3.1 简介

2.3.2 hash(字典)相关介绍

2.3.3 hash(字典)相关指令

2.4 set(集合)

2.4.1 简介

2.4.2 set(集合)相关介绍

2.4.3 set(集合)相关指令

2.5 zset(有序集合)

2.5.1 简介

2.5.2 zset(有序集合)相关介绍

2.5.3 zset(有序集合)相关指令

2.6 跳跃列表

2.6.1 简介

2.6.2 Skip List算法分析

2.6.3 Skip List特性及其实现

2.6.4 手写实现一个简单Skip List

3、三大特殊数据类型

3.1 简介

3.2 Bitmaps

3.1.1 简介

3.1.2 基本操作

3.3 HyperLogLog

3.3.1 简介

3.3.2 命令

3.3.3 原理

3.4 Geospatial

3.4.1 简介

3.4.2 命令

3.4.3 中国省会城市的经纬度

4、高级特性

4.1 事务

4.1.1 简介

4.1.2 指令介绍

4.1.3 Jedis 使用事务

4.2 Pub/Sub

4.2.1 简介

4.2.2 实例演示

4.2.3 Pub/Sub为什么被抛弃

4.3 Stream

4.3.1 简介

4.3.2 Stream内部探索

4.3.3 Stream指令

4.3.4 关于Stream优化内存的事情

4.4 Pipeline

4.4.1 简介

4.4.2 深究pipeline

4.4.3 benchmark压测pipeline

4.4.4 Jedis使用pipeline

5、持久化

5.1 简介

5.2 RDB

5.2.1 简介

5.2.2 Fork

5.2.3 自动触发

5.2.4 手动触发

5.2.5 RDB持久化文件的备份

5.3 AOF

5.3.1 简介

5.3.2 AOF配置

5.3.4 混合持久化

5.3.5 总结

6、布隆过滤器

6.1 Redis安装布隆(Bloom Filter)过滤器

6.1.1 版本要求

6.1.2 安装&编译

6.1.3 Redis集成

6.2 布隆过滤器详述

6.2.1 什么是布隆过滤器

6.2.2 布隆过滤器的使用场景

6.2.3 布隆过滤器的原理

6.2.4 Redis集成布隆过滤器

6.2.5 Redis中布隆过滤器指令使用

6.2.6 Java本地内存使用布隆过滤器

6.2.7 Java集成Redis使用布隆过滤器

7、过期策略

7.1 简介

7.2 过期策略

7.2.1 主动删除

7.2.2 被动删除

7.3 如何正确的设置key的过期时间

7.4 从节点存在的问题

8、淘汰策略

8.1 LRU(Least Recently Used)

8.1.1 简介

8.1.2 maxmemory配置

8.1.3 内存达到maxmemory怎么办

8.1.4 LRU算法实现

8.1.5 Redis的近似LRU

8.1.6 存在问题

8.2 LFU(Least Frequently Used)

8.2.1 简介

8.2.2 实现方式

8.2.3 LFU使用

9、限流

9.1 滑动窗口限流

9.1.1 需求

9.1.2 常见的错误设计

9.1.3 滑动窗口算法

9.2 漏斗限流

9.2.1 需求

9.2.2 常见的错误设计

9.2.3 漏斗限流

9.2.4 总结

9.3 令牌桶限流

9.3.1 简介

9.3.2 Redis-Cell的安装

9.3.3 CL.THROTTLE指令

9.3.4 Java调用Redis-Cell模块实现限流

10、分布式

10.1 info指令

10.1.1 简介

10.1.2 info/info all

10.1.3 info server

10.1.4 info clients

10.1.5 info memory

10.1.6 info persistence

10.1.7 AOF

10.1.8 loading

10.1.9 status

10.1.10 info replication

10.1.11 info CPU

10.1.12 info cluster

10.1.13 info keyspace

10.2 Redis一主二从Sentinel监控配置

10.2.1 环境准备

10.2.2 配置一主二从

10.2.3 哨兵配置

10.2.4 服务启动

10.2.5 测试

10.3 CentOS 7单机安装Redis Cluster(3主3从伪集群)

10.3.1 第一步:创建数据目录

10.3.2 第二步:配置文件修改

10.3.3 第三步:启动节点

10.3.4 第四步:创建集群

10.3.5 第五步:测试集群

10.4 主从复制

10.4.1 简介

10.4.2 主从复制的演进

- 增量同步中对于断线重连后的复制,会根据情况采取不同措施;如果条件允许,仍然只发送从服务缺失的部分数据。

10.5 Sentinel(哨兵)

10.5.1 简介

10.5.2 Sentinel初始化与网络连接

10.5.3 Sentinel工作

10.6 集群

10.6.1 简介

10.6.2 集群内部

10.6.3 集群工作

10.6.4 集群故障

11、CAP

11.1 什么是分布式系统

11.2 垂直扩展和水平扩展

11.3 CAP原则

11.4 如何取舍

11.5 资源推荐

12、分布式锁

12.1 简介

12.2 分布式锁的演进

12.2.1 精细胞与卵细胞的爱情故事

12.2.2 Redis中的分布式锁

12.2.3 Redis的超时问题

12.3 集群中的分布式锁

12.3.1 集群分布式锁存在的问题

12.3.2 RedLock

12.3.3 Redisson实现分布式锁


1、入门

1.1 安装

1.1.1 创建安装目录


为了方便管理我们一般统一软件的安装目录,这里选择安装的目录是 ->

/usr/local/soft

1.1.2 下载Redis


我们通过wget命令从redis官网下载压缩包 -> Redis
当前最新版本下载地址 -> https://download.redis.io/releases/redis-6.2.4.tar.gz

cd /usr/local/soft
wget https://download.redis.io/releases/

1.1.3 解压


tar -zxvf redis-6.2.4.tar.gz

1.1.4 安装gcc依赖


Redis是C语言编写,编译需要GCC
Redis6.x.x版本支持了多线程,需要gcc的版本大于4.9,我们需要查看默认GCC版本,如果版本过低则需要升级

gcc -v

我的新安装的虚拟机CentOS显示 ->

image.png


证明我的没有安装gcc,安装gcc ->

yum install gcc

image.png

再次查看安装后的版本,发现是4.8.5,这个是CentOS默认的版本,我们需要对gcc进行升级 ->

yum -y install centos-release-scl

yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils

scl enable devtoolset-9 bash

echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile

查看升级后的版本 ->

image.png

1.1.5 编译安装

cd redis-6.2.4/src
make install

编译过程如下 ->

image.png

看到如下结果输出则编译成功 ->

image.png

​或者在src目录下出现服务端和客户端的脚本 ->

redis-sentinel
redis-server
redis-cli

image.png

1.1.6 修改配置文件

Redis的配置文件在解压目录下的 redis.conf

image.png

1.1.6.1 首先设置后台启动,防止窗口一关闭服务就挂掉

默认后台启动参数为 no->

# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
# When Redis is supervised by upstart or systemd, this parameter has no impact.
daemonize no

修改成 yes->

# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
# When Redis is supervised by upstart or systemd, this parameter has no impact.
daemonize yes

1.1.6.2 允许其他主机访问

根据Redis的文档配置注释,我们要运行其他主机访问有多种方式 ->

  1. 可以选择配置访问主机的IP address
  2. bind * -::* 相当于允许所有其它主机访问
  3. bind 0.0.0.0 相当于允许所有其它主机访问
  4. 直接注释 相当于允许所有其它主机访问
# bind 192.168.1.100 10.0.0.1     # listens on two specific IPv4 addresses
# bind 127.0.0.1 ::1              # listens on loopback IPv4 and IPv6
# bind * -::*                     # like the default, all available interfaces

我的处理方式,安装文档的注释来配置

image.png

1.1.6.3 配置访问密码

如果是要考虑安全性,一定要配置密码,找到requirepass配置处,新增如下配置(阿里云等云服务其外网访问一定要配置,作者被黑过,整台服务器重启都无法重启,损失惨重,但是穷,官方处理需要Money,建议这里一定要谨慎)

requirepass yourpassword

1.1.7 启动Redis

使用redis-server 来启动,启动的方式如下->

/usr/local/soft/redis-6.2.4/src/redis-server /usr/local/soft/redis-6.2.4/redis.conf

或者这个也一样 ->

cd /src
redis-server  ../redis.conf

查看端口是否启动成功 ->

netstat -an|grep 6379 

image.png

1.1.8 客户端

进入客户端的方式如下 ->

/usr/local/soft/redis-6.2.4/src/redis-cli

image.png

1.1.9 停止Redis

停止Redis有两种方式 :
方式一,在客户端中执行SHUTDOWN

SHUTDOWN

image.png

方式二,暴力kill -9

ps -aux | grep redis
kill -9 57927

image.png

1.1.10 配置别名

为了方便启动Redis和进入客户端,我们可以通过配置别名来实现

vim ~/.bashrc

添加如下配置,

  • 注意''很重要
  • redis与rcli后面的=两边不能有空格
alias redis='/usr/local/soft/redis-6.2.4/src/redis-server /usr/local/soft/redis-6.2.4/redis.conf'
alias rcli='/usr/local/soft/redis-6.2.4/src/redis-cli'

image.png

使配置生效

source ~/.bashrc

image.png

现在我们可以通过redis启动Redis服务,使用rcli进入Redis客户端

image.png

2、五大基本数据类型

2.1 String

2.1.1 简介

Redis中所有的的数据结构都是通过一个唯一的字符串key来获取相应的value数据。
Redis有5种基础数据结构,分别是:

  • string(字符串)
  • list(列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

本小结讲述的是Redis的5种基础数据结构中的string(字符串)

2.1.2 string(字符串)相关介绍

2.1.2.1 string(字符串)的内部结构

string(字符串)是Redis最简单也是使用最广泛的数据结构,它的内部是一个字符数组。如图所示:

string(字符串).png

Redis中string(字符串)是动态字符串,允许修改;它在结构上的实现类似于Java中的ArrayList(默认构造一个大小为10的初始数组),这是冗余分配内存的思想,也称为预分配;这种思想可以减少扩容带来的性能消耗。

string(字符串)预置空间.png

2.1.2.2 string(字符串)的扩容

当string(字符串)的大小达到扩容阈值时,将会对string(字符串)进行扩容,string(字符串)的扩容主要有以下几个点:

  1. 长度小于1MB,扩容后为原先的两倍; length = length * 2
  2. 长度大于1MB,扩容后增加1MB; length = length + 1MB
  3. 字符串的长度最大值为 512MB

2.1.3 string(字符串)的指令

2.1.3.1 单个键值对增删改查操作

set -> key 不存在则新增,存在则修改

set key value

get -> 查询,返回对应key的value,不存在返回(nil)

get key

del -> 删除指定的key(key可以是多个)

del key [key …]

示例:

127.0.0.1:6379> set name liziba
OK
127.0.0.1:6379> get name
"liziba"
127.0.0.1:6379> set name liziba001
OK
127.0.0.1:6379> get name
"liziba001"
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> get name
(nil)

2.1.3.2 批量键值对

批量键值读取和写入最大的优势在于节省网络传输开销

mset -> 批量插入

mset key value [key value …]

mget -> 批量获取

mget key [key …]

示例:

127.0.0.1:6379> mset name1 liziba1 name2 liziba2 name3 liziba3
OK
127.0.0.1:6379> mget name1 name2 name3
1) "liziba1"
2) "liziba2"
3) "liziba3"

2.1.3.3 过期set命令

过期set是通过设置一个缓存key的过期时间,使得缓存到期后自动删除从而失效的机制。

方式一:

expire key seconds

示例:

127.0.0.1:6379> set name liziba
OK
127.0.0.1:6379> get name
"liziba"
127.0.0.1:6379> expire name 10   # 10s 后get name 返回 nil
(integer) 1
127.0.0.1:6379> get name
(nil)

方式二:

setex key seconds value

示例:

127.0.0.1:6379> setex name 10 liziba    # 10s 后get name 返回 nil
OK
127.0.0.1:6379> get name
(nil)

2.1.3.4 不存在创建存在不更新

上面的set操作不存在创建,存在则更新;此时如果需要存在不更新的场景,那么可以使用如下这个指令

setnx -> 不存在创建存在不更新

setnx key value

示例:

127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> setnx name liziba        
(integer) 1
127.0.0.1:6379> get name
"liziba"
127.0.0.1:6379> setnx name liziba_98        # 已经存在再次设值,失败
(integer) 0
127.0.0.1:6379> get name
"liziba"

2.1.3.5计数

string(字符串)也可以用来计数,前提是value是一个整数,那么可以对它进行自增的操作。自增的范围必须在signed long的区间访问内,[-9223372036854775808,9223372036854775808]

2.1.3.5.1 incr -> 自增1

incr key

示例:

127.0.0.1:6379> set fans 1000
OK
127.0.0.1:6379> incr fans # 自增1
(integer) 1001

2.1.3.5.2 incrby -> 自定义累加值

incrby key increment

127.0.0.1:6379> set fans 1000
OK
127.0.0.1:6379> incr fans
(integer) 1001
127.0.0.1:6379> incrby fans 999
(integer) 2000

2.1.3.5.3 测试value为整数的自增区间

最大值:

127.0.0.1:6379> set fans 9223372036854775808
OK
127.0.0.1:6379> incr fans
(error) ERR value is not an integer or out of range

最小值:

127.0.0.1:6379> set money -9223372036854775808
OK
127.0.0.1:6379> incrby money -1
(error) ERR increment or decrement would overflow

2.2 list

2.2.1 简介

Redis中所有的的数据结构都是通过一个唯一的字符串key来获取相应的value数据。
Redis有5种基础数据结构,分别是:

  • string(字符串)
  • list(列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

其中list、set、hash、zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

  • create if not exists:容器不存在则创建
  • drop if no elements:如果容器中没有元素,则立即删除容器,释放内存

本小结讲述的是Redis的5种基础数据结构中的list(列表)

2.2.2 list(列表)相关介绍

2.2.2.1 list(列表)的内部结构

Redis的列表相当于Java语言中的LinkedList,它是一个双向链表数据结构(但是这个结构设计比较巧妙,后面会介绍),支持前后顺序遍历。链表结构插入和删除操作快,时间复杂度O(1),查询慢,时间复杂度O(n)。

list结构简图.png

2.2.2.2 list(列表)的使用场景

根据Redis双向列表的特性,因此其也被用于异步队列的使用。实际开发中将需要延后处理的任务结构体序列化成字符串,放入Redis的队列中,另一个线程从这个列表中获取数据进行后续处理。其流程类似如下的图:

list用于异步队列示意图.png

2.2.3 list(列表)的指令

2.2.3.1 右进左出—队列

队列在结构上是先进先出(FIFO)的数据结构(比如排队购票的顺序),常用于消息队列类似的功能,例如消息排队、异步处理等场景。通过它可以确保元素的访问顺序。
lpush -> 从左边边添加元素

lpush key value [value …] 

rpush -> 从右边添加元素

rpush key value [value …]

llen -> 获取列表的长度

llen key

lpop -> 从左边弹出元素

lpop key

127.0.0.1:6379> rpush code java c python    # 向列表中添加元素
(integer) 3
127.0.0.1:6379> llen code    # 获取列表长度
(integer) 3
127.0.0.1:6379> lpop code # 弹出最先添加的元素
"java"
127.0.0.1:6379> lpop code    
"c"
127.0.0.1:6379> lpop code
"python"
127.0.0.1:6379> llen code
(integer) 0
127.0.0.1:6379> lpop code
(nil)

2.2.3.2 右进右出——栈

栈在结构上是先进后出(FILO)的数据结构(比如弹夹压入子弹,子弹被射击出去的顺序就是栈),这种数据结构一般用来逆序输出。
lpush -> 从左边边添加元素

lpush key value [value …] 

rpush -> 从右边添加元素

rpush key value [value …] 

rpop -> 从右边弹出元素

rpop code

127.0.0.1:6379> rpush code java c python
(integer) 3
127.0.0.1:6379> rpop code            # 弹出最后添加的元素
"python"
127.0.0.1:6379> rpop code
"c"
127.0.0.1:6379> rpop code
"java"
127.0.0.1:6379> rpop code
(nil)

2.2.3.3 慢操作

列表(list)是个链表数据结构,它的遍历是慢操作,所以涉及到遍历的性能将会遍历区间range的增大而增大。注意list的索引运行为负数,-1代表倒数第一个,-2代表倒数第二个,其它同理。
lindex -> 遍历获取列表指定索引处的值

lindex key ind

lrange -> 获取从索引start到stop处的全部值

lrange key start stop

ltrim -> 截取索引start到stop处的全部值,其它将会被删除

ltrim key start stop

127.0.0.1:6379> rpush code java c python
(integer) 3
127.0.0.1:6379> lindex code 0        # 获取索引为0的数据
"java"
127.0.0.1:6379> lindex code 1   # 获取索引为1的数据
"c"
127.0.0.1:6379> lindex code 2        # 获取索引为2的数据
"python"
127.0.0.1:6379> lrange code 0 -1    # 获取全部 0 到倒数第一个数据  == 获取全部数据
1) "java"
2) "c"
3) "python"
127.0.0.1:6379> ltrim code 0 -1    # 截取并保理 0 到 -1 的数据 == 保理全部
OK
127.0.0.1:6379> lrange code 0 -1
1) "java"
2) "c"
3) "python"
127.0.0.1:6379> ltrim code 1 -1    # 截取并保理 1 到 -1 的数据 == 移除了索引为0的数据 java
OK
127.0.0.1:6379> lrange code 0 -1
1) "c"
2) "python"

2.2.4 list(列表)深入理解

Redis底层存储list(列表)不是一个简单的LinkedList,而是quicklist ——“快速列表”。关于quicklist是什么,下面会简单介绍,具体源码我也还在学习中,后面大家一起探讨。
quicklist是多个ziplist(压缩列表)组成的双向列表;而这个ziplist(压缩列表)又是什么呢?ziplist指的是一块连续的内存存储空间,Redis底层对于list(列表)的存储,当元素个数少的时候,它会使用一块连续的内存空间来存储,这样可以减少每个元素增加prev和next指针带来的内存消耗,最重要的是可以减少内存碎片化问题。

2.2.4.1 常见的链表结构示意图

每个node节点元素,都会持有一个prev->执行前一个node节点和next->指向后一个node节点的指针(引用),这种结构虽然支持前后顺序遍历,但是也带来了不小的内存开销,如果node节点仅仅是一个int类型的值,那么可想而知,引用的内存比例将会更大。

常见链表结构.png

2.2.4.2 ziplist示意图

ziplist是一块连续的内存地址,他们之间无需持有prev和next指针,能通过地址顺序寻址访问。

ziplist内存示意图.png

2.2.4.3 quicklist示意图

quicklist是由多个ziplist组成的双向链表。

quicklist示意图.png

2.3 hash(字典)

2.3.1 简介

Redis中所有的的数据结构都是通过一个唯一的字符串key来获取相应的value数据。
Redis有5种基础数据结构,分别是:

  • string(字符串)
  • list(列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

其中list、set、hash、zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

  • create if not exists:容器不存在则创建
  • drop if no elements:如果容器中没有元素,则立即删除容器,释放内存

本小节讲述的是Redis的5种基础数据结构中的hash(字典)

2.3.2 hash(字典)相关介绍

2.3.2.1 hash(字典)的内部结构

Redis的hash(字典)相当于Java语言中的HashMap,它是根据散列值分布的无序字典,内部的元素是通过键值对的方式存储。

hash无序字典键值对存储 (1).png

hash(字典)的实现与Java中的HashMap(JDK1.7)的结构也是一致的,它的数据结构也是数组+链表组成的二维结构,节点元素散列在数组上,如果发生hash碰撞则使用链表串联在数组节点上。

hash结构 (1).png

2.3.2.2 hash(字典)扩容

Redis中的hash(字典)存储的value只能是字符串值,此外扩容与Java中的HashMap也不同。Java中的HashMap在扩容的时候是一次性完成的,而Redis考虑到其核心存取是单线程的性能问题,为了追求高性能,因而采取了渐进式rehash策略。
渐进式rehash指的是并非一次性完成,它是多次完成的,因此需要保理旧的hash结构,所以Redis中的hash(字典)会存在新旧两个hash结构,在rehash结束后也就是旧hash的值全部搬迁到新hash之后,新的hash在功能上才会完全替代以前的hash。

hash-_rehash.png

2.3.2.3 hash(字典)的相关使用场景

hash(字典)可以用来存储对象的相关信息,一个hash(字典)代表一个对象,hash的一个key代表对象的一个属性,key的值代表属性的值。hash(字典)结构相比字符串来说,它无需将整个对象进行序列化后进行存储。这样在获取的时候可以进行部分获取。所以相比之下hash(字典)具有如下的优缺点:

  • 读取可以部分读取,节省网络流量
  • 存储消耗的高于单个字符串的存储

2.3.3 hash(字典)相关指令

2.3.3.1 hash(字典)常用指令

hset -> hash(字典)插入值,字典不存在则创建 key代表字典名称,field 相当于 key,value是key的值

hset key field value 

hmset -> 批量设值

hmset key field value [field value …]

示例:

7.0.0.1:6379> hset book java "Thinking in Java"        # 字符串包含空格需要""包裹
(integer) 1
127.0.0.1:6379> hset book python "Python code"
(integer) 1
127.0.0.1:6379> hset book c "The best of c"
(integer) 1
127.0.0.1:6379> hmset book go "concurrency in go" mysql "high-performance MySQL" # 批量设值
OK

hget -> 获取字典中的指定key的value

hget key field

hgetall -> 获取字典中所有的key和value,换行输出

hgetall key

示例:

127.0.0.1:6379> hget book java
"Thinking in Java"
127.0.0.1:6379> hgetall book
1) "java"
2) "Thinking in Java"
3) "python"
4) "Python code"
5) "c"
6) "The best of c"

hlen -> 获取指定字典的key的个数

hlen key

举例:

127.0.0.1:6379> hlen book
(integer) 5

2.3.3.2 hash(字典)使用小技巧

在string(字符串)中可以使用incr和incrby对value是整数的字符串进行自加操作,在hash(字典)结构中如果单个子key是整数也可以进行自加操作。
hincrby -> 增对hash(字典)中的某个key的整数value进行自加操作

hincrby key field increment

127.0.0.1:6379> hset liziba money 10
(integer) 1
127.0.0.1:6379> hincrby liziba money -1
(integer) 9
127.0.0.1:6379> hget liziba money
"9"

注意如果不是整数会报错。

127.0.0.1:6379> hset liziba money 10.1
(integer) 1
127.0.0.1:6379> hincrby liziba money 1
(error) ERR hash value is not an integer

2.4 set(集合)

2.4.1 简介

Redis中所有的的数据结构都是通过一个唯一的字符串key来获取相应的value数据。
Redis有5种基础数据结构,分别是:

  • string(字符串)
  • list(列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

其中list、set、hash、zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

  • create if not exists:容器不存在则创建
  • drop if no elements:如果容器中没有元素,则立即删除容器,释放内存

本文讲述的是Redis的5种基础数据结构中的set(集合)

2.4.2 set(集合)相关介绍

2.4.2.1 set(集合)的内部结构

Redis的set(集合)相当于Java语言里的HashSet,它内部的键值对是无序的、唯一的。它的内部实现了一个所有value为null的特殊字典。
集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。

set结构 (1).png

2.4.2.2 set(集合)的使用场景

set(集合)由于其特殊去重复的功能,我们可以用来存储活动中中奖的用户的ID,这样可以保证一个用户不会中奖两次。

2.4.3 set(集合)相关指令

sadd -> 添加集合成员,key值集合名称,member值集合元素,元素不能重复

sadd key member [member …]

127.0.0.1:6379> sadd name zhangsan
(integer) 1
127.0.0.1:6379> sadd name zhangsan        # 不能重复,重复返回0
(integer) 0
127.0.0.1:6379> sadd name lisi wangwu liumazi # 支持一次添加多个元素
(integer) 3

smembers -> 查看集合中所有的元素,注意是无序的

smembers key

127.0.0.1:6379> smembers name    # 无序输出集合中所有的元素
1) "lisi"
2) "wangwu"
3) "liumazi"
4) "zhangsan"

sismember -> 查询集合中是否包含某个元素

sismember key member

127.0.0.1:6379> sismember name lisi  # 包含返回1
(integer) 1
127.0.0.1:6379> sismember name tianqi # 不包含返回0
(integer) 0

scard -> 获取集合的长度

scard key

127.0.0.1:6379> scard name
(integer) 4

spop -> 弹出元素,count指弹出元素的个数

spop key [count]

127.0.0.1:6379> spop name            # 默认弹出一个
"wangwu"
127.0.0.1:6379> spop name 3    
1) "lisi"
2) "zhangsan"
3) "liumazi"

2.5 zset(有序集合)

2.5.1 简介

Redis中所有的的数据结构都是通过一个唯一的字符串key来获取相应的value数据。
Redis有5种基础数据结构,分别是:

  • string(字符串)
  • list(列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

其中list、set、hash、zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

  • create if not exists:容器不存在则创建
  • drop if no elements:如果容器中没有元素,则立即删除容器,释放内存

本文讲述的是Redis的5种基础数据结构中的zset(有序列表)

2.5.2 zset(有序集合)相关介绍

2.5.2.1 zset(有序集合)的内部结构

zset(有序集合)是Redis中最常问的数据结构。它类似于Java语言中的SortedSet和HashMap的结合体,它一方面通过set来保证内部value值的唯一性,另一方面通过value的score(权重)来进行排序。这个排序的功能是通过Skip List(跳跃列表)来实现的。
zset(有序集合)的最后一个元素value被移除后,数据结构被自动删除,内存被回收。

zset的内部数据结构.png

2.5.2.2 zset(有序集合)的相关使用场景

利用zset的去重和有序的效果可以由很多使用场景,举两个例子:

  • 存储粉丝列表,value是粉丝的ID,score是关注时间戳,这样可以对粉丝关注进行排序
  • 存储学生成绩,value使学生的ID,score是学生的成绩,这样可以对学生的成绩排名

2.5.3 zset(有序集合)相关指令

1、zadd -> 向集合中添加元素,集合不存在则新建,key代表zset集合名称,score代表元素的权重,member代表元素

zadd key [NX|XX] [CH] [INCR] score member [score member …]

127.0.0.1:6379> zadd name 10 zhangsan
(integer) 1
127.0.0.1:6379> zadd name 10.1 lisi
(integer) 1
127.0.0.1:6379> zadd name 9.9 wangwu
(integer) 1

2、zrange -> 按照score权重从小到大排序输出集合中的元素,权重相同则按照value的字典顺序排序(lexicographical order )
超出范围的下标并不会引起错误。 比如说,当 start 的值比有序集的最大下标还要大,或是 start > stop 时, zrange 命令只是简单地返回一个空列表。 另一方面,假如 stop 参数的值比有序集的最大下标还要大,那么 Redis 将 stop 当作最大下标来处理。
可以通过使用 WITHSCORES 选项,来让成员和它的 score 值一并返回,返回列表以 value1,score1, …, valueN,scoreN 的格式表示。 客户端库可能会返回一些更复杂的数据类型,比如数组、元组等。

zrange key start stop [WITHSCORES] 

127.0.0.1:6379> zrange name 0 -1 # 获取所有元素,按照score的升序输出
1) "wangwu"
2) "zhangsan"
3) "lisi"
127.0.0.1:6379> zrange name 0 1        # 获取第一个和第二个slot的元素
1) "wangwu"
2) "zhangsan"
127.0.0.1:6379> zadd name 10 tianqi    # 在上面的基础上添加score为10的元素
(integer) 1
127.0.0.1:6379> zrange name 0 2    # key相等则按照value字典排序输出
1) "wangwu"
2) "tianqi"
3) "zhangsan"
127.0.0.1:6379> zrange name 0 -1 WITHSCORES # WITHSCORES 输出权重
1) "wangwu"
2) "9.9000000000000004"
3) "tianqi"
4) "10"
5) "zhangsan"
6) "10"
7) "lisi"
8) "10.1"

3、zrevrange -> 按照score权重从大到小输出集合中的元素,权重相同则按照value的字典逆序排序
其中成员的位置按 score 值递减(从大到小)来排列。 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE key start stop [WITHSCORES] 命令一样。

zrevrange key start stop [WITHSCORES]

127.0.0.1:6379> zrevrange name 0 -1 WITHSCORES
1) "lisi"
2) "10.1"
3) "zhangsan"
4) "10"
5) "tianqi"
6) "10"
7) "wangwu"
8) "9.9000000000000004"

4、zcard -> 当 key 存在且是有序集类型时,返回有序集的基数。 当 key 不存在时,返回 0 

zcard key

127.0.0.1:6379> zcard name
(integer) 4

5、zscore -> 返回有序集 key 中,成员 member 的 score 值,如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil

zscore key member z

127.0.0.1:6379> zscore name zhangsan
"10"
127.0.0.1:6379> zscore name liziba
(nil)

6、zrank -> 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。
排名以 0 为底,也就是说, score 值最小的成员排名为 0 

zrank key member

127.0.0.1:6379> zrange name 0 -1
1) "wangwu"
2) "tianqi"
3) "zhangsan"
4) "lisi"
127.0.0.1:6379> zrank name wangwu
(integer) 0

7、zrangebyscore -> 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
min 和 max 可以是 -inf 和 +inf ,这样一来,你就可以在不知道有序集的最低和最高 score 值的情况下,使用 *ZRANGEBYSCORE这类命令。
默认情况下,区间的取值使用闭区间 (小于等于或大于等于),你也可以通过给参数前增加 ( 符号来使用可选的开区间 (小于或大于)

zrangebyscore key min max [WITHSCORES] [LIMIT offset count]

127.0.0.1:6379> zrange name 0 -1 WITHSCORES # 输出全部元素
1) "wangwu"
2) "9.9000000000000004"
3) "tianqi"
4) "10"
5) "zhangsan"
6) "10"
7) "lisi"
8) "10.1" 
127.0.0.1:6379> zrangebyscore name 9 10
1) "wangwu"
2) "tianqi"
3) "zhangsan"
127.0.0.1:6379> zrangebyscore name 9 10 WITHSCORES    # 输出分数
1) "wangwu"
2) "9.9000000000000004"
3) "tianqi"
4) "10"
5) "zhangsan"
6) "10"
127.0.0.1:6379> zrangebyscore name -inf 10 # -inf 从负无穷开始
1) "wangwu"
2) "tianqi"
3) "zhangsan"
127.0.0.1:6379> zrangebyscore name -inf +inf    # +inf 直到正无穷
1) "wangwu"
2) "tianqi"
3) "zhangsan"
4) "lisi"
127.0.0.1:6379> zrangebyscore name (10 11  #  10 < score <=11
1) "lisi"
127.0.0.1:6379> zrangebyscore name (10 (10.1  # 10 < socre < -11
(empty list or set)
127.0.0.1:6379> zrangebyscore name (10 (11 
1) "lisi"

8、zrem -> 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略

zrem key member [member …]

127.0.0.1:6379> zrange name 0 -1
1) "wangwu"
2) "tianqi"
3) "zhangsan"
4) "lisi"
127.0.0.1:6379> zrem name zhangsan # 移除元素
(integer) 1
127.0.0.1:6379> zrange name 0 -1
1) "wangwu"
2) "tianqi"
3) "lisi"

2.6 跳跃列表

2.6.1 简介

跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

Skip List(跳跃列表)这种随机的数据结构,可以看做是一个二叉树的变种,它在性能上与红黑树、AVL树很相近;但是Skip List(跳跃列表)的实现相比前两者要简单很多,目前Redis的zset实现采用了Skip List(跳跃列表)(其它还有LevelDB等也使用了跳跃列表)。

RBT红黑树与Skip List(跳跃列表)简单对比:
RBT红黑树

  1. 插入、查询时间复杂度O(logn)
  2. 数据天然有序
  3. 实现复杂,设计变色、左旋右旋平衡等操作
  4. 需要加锁

Skip List跳跃列表

  1. 插入、查询时间复杂度O(logn)
  2. 数据天然有序
  3. 实现简单,链表结构
  4. 无需加锁

2.6.2 Skip List算法分析

2.6.2.1 Skip List论文

这里贴出Skip List的论文,需要详细研究的请看论文,下文部分公式、代码、图片出自该论文。
Skip Lists: A Probabilistic Alternative to Balanced Trees

https://www.cl.cam.ac.uk/teaching/2005/Algorithms/skiplists.pdf

2.6.2.2 Skip List动态图

先通过一张动图来了解Skip List的插入节点元素的流程,此图来自维基百科。

a50f862e502f718e490117c7a6541784.gif

2.6.2.3 Skip List算法性能分析

2.6.2.3.1 计算随机层数算法

首先分析的是执行插入操作时计算随机数的过程,这个过程会涉及层数的计算,所以十分重要。对于节点他有如下特性:

  • 节点都有第一层的指针
  • 节点有第i层指针,那么第i+1层出现的概率为p
  • 节点有最大层数限制,MaxLevel

计算随机层数的伪代码:
论文中的示例

image.png

Java版本

public int randomLevel(){
    int level = 1;
    // random()返回一个[0...1)的随机数
    while (random() < p && level < MaxLevel){ 
        level += 1;
    }
    return level;
}

代码中包含两个变量P和MaxLevel,在Redis中这两个参数的值分别是:

p = 1/4
MaxLevel = 64

2.3.2 节点包含的平均指针数目

Skip List属于空间换时间的数据结构,这里的空间指的就是每个节点包含的指针数目,这一部分是额外的内内存开销,可以用来度量空间复杂度。random()是个随机数,因此产生越高的节点层数,概率越低(Redis标准源码中的晋升率数据1/4,相对来说Skip List的结构是比较扁平的,层高相对较低)。其定量分析如下:

  • level = 1 概率为1-p
  • level >=2 概率为p
  • level = 2 概率为p(1-p)
  • level >= 3 概率为p^2
  • level = 3 概率为p^2(1-p)
  • level >=4 概率为p^3
  • level = 4 概率为p^3(1-p)
  • ……

得出节点的平均层数(节点包含的平均指针数目):

2195619-b17ca123586244cc.webp


所以Redis中p=1/4计算的平均指针数目为1.33

2.3.3 时间复杂度计算

以下推算来自论文内容
假设p=1/2,在以p=1/2生成的16个元素的跳过列表中,我们可能碰巧具有9个元素,1级3个元素,3个元素3级元素和1个元素14级(这不太可能,但可能会发生)。我们该怎么处理这种情况?如果我们使用标准算法并在第14级开始我们的搜索,我们将会做很多无用的工作。那么我们应该从哪里开始搜索?此时我们假设SkipList中有n个元素,第L层级元素个数的期望是1/p个;每个元素出现在L层的概率是p^(L-1), 那么第L层级元素个数的期望是 n * (p^L-1);得到1 / p =n * (p^L-1)

1 / p = n * (p^L-1)
n = (1/p)^L
L = log(1/p)^n

所以我们应该选择MaxLevel = log(1/p)^n
定义:MaxLevel = L(n) = log(1/p)^n

推算Skip List的时间复杂度,可以用逆向思维,从层数为i的节点x出发,返回起点的方式来回溯时间复杂度,节点x点存在两种情况:

  • 节点x存在(i+1)层指针,那么向上爬一级,概率为p,对应下图situation c.
  • 节点x不存在(i+1)层指针,那么向左爬一级,概率为1-p,对应下图situation b.

image.png


设C(k) = 在无限列表中向上攀升k个level的搜索路径的预期成本(即长度)那么推演如下:

C(0)=0
C(k)=(1-p)×(情况b的查找长度) + p×(情况c的查找长度)
C(k)=(1-p)(C(k)+1) + p(C(k-1)+1)
C(k)=1/p+C(k-1)
C(k)=k/p

上面推演的结果可知,爬升k个level的预期长度为k/p,爬升一个level的长度为1/p。

由于MaxLevel = L(n), C(k) = k / p,因此期望值为:(L(n) – 1) / p;将L(n) = log(1/p)^n 代入可得:(log(1/p)^n - 1) / p;将p = 1 / 2 代入可得:2 * log2^n - 2,即O(logn)的时间复杂度。

2.6.3 Skip List特性及其实现

2.6.3.1 Skip List特性

Skip List跳跃列表通常具有如下这些特性

  1. Skip List包含多个层,每层称为一个level,level从0开始递增
  2. Skip List 0层,也就是最底层,应该包含所有的元素
  3. 每一个level/层都是一个有序的列表
  4. level小的层包含level大的层的元素,也就是说元素A在X层出现,那么 想X>Z>=0的level/层都应该包含元素A
  5. 每个节点元素由节点key、节点value和指向当前节点所在level的指针数组组成

2.6.3.2 Skip List查询

假设初始Skip List跳跃列表中已经存在这些元素,他们分布的结构如下所示:

  • 39
    点赞
  • 135
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 40
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李子捌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值