Redis,性能加速的催化剂

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis,性能加速的催化剂相关的知识,希望对你有一定的参考价值。

文章目录

一、前言

redis引入,什么是redis?

Redis 是一个开源的、使用ANSI C编写的、支持网络、基于内存的、可持久化的Key-Value 型的数据库,通过提供多种键值数据类型(5种基本数据类型 string hash list set sortedset)来适应不同场景下的存储需求,并且提供多种语言的API(当然包括Java语言的API)。redis官网如图:

理清几个易混淆的概念

SQL:全称为Structured Query Language,译为结构化查询语言,是一种计算机程序语言,一种解释型语言。

NoSQL:全称Not Only SQL,译为"不仅仅是SQL"(注意,NoSQL不是不使用SQL的意思),泛指所有的非关系型数据库。

关系型数据库:即RDB,全称为Relational Database,其实,一个更加常见的英文简称是RDBMS,Relational Database Management System,关系型数据库管理系统,所以,RDBMS就被认为是关系型数据库的简称。

非关系型数据库:不使用数据库表结构存储数据,用NoSQL表示。

相互对比辨析相近概念
(1) SQL与关系型数据库:SQL是SQL,关系型数据库是关系型数据库,两者是完全不同的两个东西,SQL是一种解释型语言,关系型数据库是数据库的一种类型,两者的关系是关系型数据库的CRUD操作使用SQL语言来完成,SQL语言被认为是关系型数据库的一种特征。
(2) NoSQL与非关系型数据库:对于程序员的工作中,NoSQL就是指非关系型数据库,即NoSQL==非关系型数据库,两者是同一个东西。
(3) SQL与NoSQL:SQL是一种语言,NoSQL表示非关系型数据库,一个是语言,一个是数据库,两个不同关系。

关系型数据库与非关系型数据库,如下表:

关系型数据库RDBMS非关系型数据库NoSQL
存储格式支持数据库表结构不使用数据库表结构存储,包括列存储、文档存储、key-value存储、图存储、对象存储、xml存储
特点

高度组织化结构化数据;

结构化查询语言(SQL);

数据和关系都存储在单独的表中;

数据操纵语言,数据定义语言;

严格的一致性;

基础事务

代表着不仅仅是SQL;

没有声明性查询语言;

没有预定义的模式;

键值对存储,列存储,文档存储,图形数据库;

最终一致性,而非ACID属性;

非结构化和不可预知的数据;

CAP定理;

高性能,高可用性和可伸缩性

设计原则ACID:A (Atomicity) 原子性、C (Consistency) 一致性、I (Isolation) 独立性、D (Durability) 持久性,表示任何一个关系型数据库(使用表格式存储的数据库)必须同时满足四个特性要求

BASE原则(同时满足CAP中的CA)

Basically Availble --基本可用;

Soft-state --软状态/柔性事务。 “Soft state” 可以理解为"无连接"的, 而 “Hard state” 是"面向连接"的;

Eventual Consistency – 最终一致性, 也是是 ACID 的最终目的。

分类mysql sqlserver oracle

1)列存储:按列存储数据的,如Hbase、Cassandra、Hypertable

2)文档存储:用类似json的格式存储,存储的内容是文档型的,如MongoDB、CouchDB

3)key-value存储:Tokyo Cabinet / Tyrant、Berkeley DB、MemcacheDB、Redis

4)图存储:图形关系的最佳存储,如Neo4J、FlockDB

5)对象存储:通过对象的方式存取数据,如db4o、Versant

6)xml存储:存储XML数据,如Berkeley DB XML、BaseX

由上表可知,Redis是一种使用key-value键值对来存储数据的非关系型数据库。

redis与NoSQL的关系:NoSQL可以表示非关系型数据库,redis一种使用key-value键值对存储的非关系型数据库,这就是两者的关系。

redis查询速度快,但是由于是存储在内存中,适用于存放临时、少量的数据,比如验证码有效期5秒;

mongdb查询速度快,但是由于是存储在磁盘上的,适用于存放永久、大量的数据,比如 购物车中的商品。

本文主要包括四个部分的内容,包括redis基础知识、redis底层原理、单机版redis及Java开发实践、集群版redis及Java开发实践。

二、redis基础知识

既然Redis是一种使用key-value键值对来存储数据的非关系型数据库,我们先来介绍这种非关系型数据库的基础知识。

2.1 从“处理器-缓存-内存”到“后台-redis-数据库”

回顾学生年代《计算机组成原理》,由于处理器CPU与内存的速度不匹配问题,所有我们在处理器和内存之间加一个高速缓存,

缓存的数据是主存中热点数据的副本,处理器读取数据时,优先读取缓存中的数据,缓存中没有,再到主存中取,同时这个数据成为热点数据,写入到缓存中,下次处理器直接从缓存中取。

对于写操作,为了保证主存缓存中数据一致性问题,有“写直达法”和“写回法”两种方式。整个架构变化如图:

工作中,web项目开发,由于网络请求与数据库查询数据不匹配问题,所以我们在后台程序与数据库之间加一个redis/redis-cluster缓存,其读写操作与计算机的存储一样,优先读写redis缓存,整个架构变化如下:

这里用硬件对比软件后台,用高速缓存cache对比redis/redis-cluster,两者基本上是一样的,唯一不同的恐怕就是Cache是硬件,redis是软件。

2.2 不使用缓存与使用缓存(读操作+写操作)

在介绍redis读写之前,引入一个知识,redis支持的数据类型(我们起码要知道读写操作,读写的是什么)

redis支持五种数据类型

目前为止Redis支持的键值数据类型一共五种,如下:String字符串类型、hash散列类型、list列表类型、set集合类型、sorted set有序集合类型。

不使用缓存读

不使用缓存写

使用缓存读

使用缓存写(先更新数据库,再更新redis缓存,类似写直达法)

使用缓存写(先更新redis缓存,再更新数据库,类似写回法)

2.3 redis典型问题:缓存穿透、缓存雪崩和缓存击穿(以淘宝双11抢购为例)

2.3.1 缓存穿透,不存在的商品X

从名称上来解释含义:传统意义上的穿透,即水滴石穿,滴水能把石穿透,就是说水滴穿透石头的整个过程。

这里的缓存穿透,是指网络请求查询一个数据库一定不存在的数据(假设为-1,一个无意义数字)。因为这是一个数据库中绝对不存在的数据,所有redis缓存中也一定不存在,执行过程中,因为redis中一定找不到,所有一定会去数据库中找,结果就是数据库也找不到。因为这个网络请求查询过程是 “前端/客户端/移动端—网络请求—redis缓存—数据库” ,整个过程穿透redis,直达数据库,与水滴石穿有类似之意,所以称为缓存穿透。

正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存(这是重点,查不到就不进入缓存,所以第二次请求同样的数据还是要查询数据库)。

缓存穿透的问题再哪里?在于它每次都要请求数据库,redis缓存形同虚设,起不到减少数据库查询、提升性能的作用。

我们知道,每一次查询数据库的代价是比较大的(所以我们引用了redis缓存),因为请求的是一个数据库一定不存在的数据,所有每一次都要查数据库,而且因为数据库查询为空,这次的数据也不会放入缓存,下一次还是查询这个数据又要到数据库中查询,不断循环,一个不存在的数据多次请求就可以让后台系统崩溃。

假如有恶意攻击,就可以利用这个漏洞(网络请求一个数据库中一定不存在的数据,不断请求),对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的KEY,进行攻击。

举例:

解决方案:

思考:第一次请求redis中找不到,访问数据库不是什么大问题,后面N-1次都要访问数据库这就是个大问题了。核心在于:如果数据库查询对象为空,则不放进缓存。这是默认规则,如果能消除这条规则,即数据库查询为空也写入redis,后面进直接从redis中取,取不到就结束(因为数据库和redis已经同步了)。

解决:会采用缓存空值null的方式,如果从数据库查询的对象为空,也放入缓存,即将null放入缓存,将key:value=(x,null)写入redis,下一次查询key=x,直接在redis中返回value=null.

2.3.2 缓存雪崩,双十一抢购

从名称上来解释含义:传统意义上的雪崩就是指一种当山坡积雪内部的内聚力抗拒不了它所受到的重力拉引时,便向下滑动,引起大量雪体崩塌的自然现象。

雪崩之所以可怕,是因为其规模之大,局部雪崩可能引起全局雪崩,一次严重的雪崩可能造成整座雪山的崩塌。这里的缓存雪崩是指在某一个时间段,缓存集中过期失效,造成整个redis不可用(对应整座雪山崩塌)。

关于缓存雪崩,粗体标记,注意两个词语,一是“集中”,二是“过期”,一是集中失效,二是过期失效

关于集中:redis默认有16个库,db0~db15,“集中”表示redis缓存中的大部分库是失效了

关于过期:表示redis缓存雪崩中多个库是由于缓存时间到期而失效的

缓存雪崩就是缓存失效,“集中”告诉我们是大部分库失效了,不是小部分或者个别;“过期”是指这种库失效是由于缓存时间到期而失效的(即是正常的失效),不是异常错误导致库失效

举例:产生雪崩的原因之一,以淘宝双十一抢购为例,假设淘宝后台将热门商品放入redis/redis-cluster(像淘宝这么大的肯定是redis-cluster redis集群喽),设置缓存时间为一小时(当然淘宝系统不会如此愚蠢,这里是假设,皮),那么午夜12点开始抢购,到了午夜1点,所有的热门商品的缓存都过期了,如果用户再购买商品,后台就是读写数据库(而不是直接读写redis)了,这样造成访问速度慢,带来无法容忍的用户体验。如图:

这样的缓存集体过期就是缓存雪崩,是使用缓存的一种危险,开发者一定要记住。

解决:

思考方式一:如果发生了集体缓存过期,即缓存雪崩,是非常可怕且很难挽救的,在发生后的一段时间内相当于没有使用redis缓存技术,退化为原始的持久层数据库操作。所以,我们的思考不是发生缓存雪崩之后如何解决,而是如何避免缓存雪崩的发生。

解决方案一——过期错开:将key的过期时间后面加上一个随机数,让key均匀的失效;或者使用一种特定的算法,使过期时间赋值更符合实际业务。

思考方式二:第一种方案均摊过期时间或使用特定算法,旨在最大程度在避免出现缓存雪崩,但是如果缓存雪崩确实发生了,程序如何应对呢?

解决方案二——排队处理:使用优先队列或者锁让程序执行在压力范围之内,如果访问量达到阈值,排队处理业务请求,即为了保证系统的不会崩溃,不要同时处理所有请求。

2.3.3 缓存击穿,iphoneX上市了

从名称上来解释含义:传统意义上的击穿(电压击穿)是指在电场作用下绝缘体内部产生破坏性的放电,绝缘电阻下降,电流增大,并产生破坏和穿孔的现象。

这里的缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存(类似电压穿破绝缘体),直接请求数据库,就像在一个屏障上凿开了一个洞。

缓存穿透与缓存击穿异同:

相同点:都是数据库承受不了巨大压力,导致崩溃。

不同点:

缓存穿透是指redis没作用,形同虚设,每一次访问都要查询数据库,导致崩溃,这是技术上可以解决的,redis中记录一个(x,null)键值对;

缓存击穿是指redis作用了,但是数据量实在是太大了,实在是承受不了这么大的数据量,数据库连带redis缓存一起崩溃,这时在固定的硬件成本下,缓存、数据库软件方面已经达到理论上的最优了,技术上解决不了。

举例:以iphoneX发布为例,一下子就成了热款,所有人通过淘宝线上购买,巨大的并发量某一时刻击穿缓存,直接请求数据库,而数据库又无法高速查表,导致系统崩溃。如图:

解决方案

思考:现在的问题是数据量实在太大了,redis和数据库的设计已经达到最优了。

步骤一:热卖商品redis有效期设置为永久,绝对不要出现过期问题,redis方面达到最优。

步骤二:在固定的硬件成本下,数据库(mysql或oracle)在表设计达到最优,框架(如mybatis)sql查询语句设计达到最优

步骤三:设置一个优先队列(如12306 买春运往返票),控制并发数,防止系统崩溃。

实际上,其实,大多数情况下这种爆款很难对数据库服务器造成压垮性的压力,能达到这种并发的可能也只有“12306春运购票”、“淘宝双十一” 、“春晚跨年”这样的事情了,从另外一个方面来讲,如果真的有某个单一商品销售量达到使用让redis、数据库崩溃,公司钱也赚了不少了,赶紧偷着笑吧!

三、redis五种基本类型的底层结构与应用

3.1 redisObject对象(类型type+编码encoding)和sds(free+len+buf)

这个很重要,要看懂后面五个类型的底层结构,要先搞懂redisObjet和sds(sdshdr)的结构

3.1.1 redisObject对象

Redis基于以上的数据结构创建了一个对象体系,包含了字符串对象,列表对象,哈希对象,集合对象,有序集合对象这五种对象.

Redis的对象体系还实现了基于引用计数技术的内存回收机制,同时基于引用计数技术实现了对象共享机制,在适当条件,通过多个数据库键共享同一个对象来节约内存.

Redis中的每一个对象都由一个redisObject结构表示,这个redisObject对象结构中和保存数据有关的三个属性:type属性、encoding属性、ptr属性,如下:

typedef struct redisObject {
    //类型
    unsigned type:4;
    
    //编码
    unsigned encoding:4;
 
    //指向底层实现数据结构的指针
    void *ptr;
 
    // ...
} robj;

下面分别对类型type、编码encoding、指针ptr分别介绍:

3.1.2 类型type

类型常量对象名称TYPE命令输出
REDIS_STRING字符串对象string“string”
REDIS_LIST列表对象list“list”
REDIS_HASH哈希对象hash(map)“hash”
REDIS_SET集合对象set“set”
REDIS_ZSET有序集合对象sorted-set“zset”

注意,看这个表,一定要区分好“类型常量”、“对象名称”,如下:

1)当我们称呼一个数据库键为“字符串键”时,我们指的是“这个数据库键对应的值为字符串对象”;当我们称呼一个数据库键为“列表键”时,我们指的是“这个数据库键对应的值为列表对象”;

2)TYPE命令输出(上表第三列):当我们对一个数据库键执行TYPE命令时,命令返回的结果是数据库键对应的值对象的类型,而不是键对象的类型。

其实,这些东西都是一些概念理论上的纠结,实际开发中,我们以实现需求为主,也不一定要区分的这么清楚,当然面试中可能用得到。

3.1.3 编码encoding

编码常量编码对应的底层数据结构redis中具体类型(5种)OBJECT ENCODING 命令输出
REDIS_ENCODING_INTlong类型整数(编码常量后缀是INT,但是其实现的底层数据结构是long)REDIS_STRING“int”
REDIS_ENCODING_EMBSTRembstr编码的简单动态字符串(SDS simple dynamic string)REDIS_STRING“embstr”
REDIS_ENCODING_RAWraw编码的简单动态字符串(SDS simple dynamic string)REDIS_STRING“raw”
REDIS_ENCODING_HT字典(编码常量后缀为HT,表示dictionary/hashtable,即字典)REDIS_HASH、REDIS_SET“hashtable”
REDIS_ENCODING_LINKEDLIST双向链表/双端链表(linkedlist,见名达意,不解释)REDIS_LIST“linkedlist”
REDIS_ENCODING_ZIPLIST压缩列表(ziplist,见名达意,不解释)REDIS_LIST、REDIS_HASH、REDIS_ZSET“ziplist”
REDIS_ENCODING_INTSET整型集合(intset,见名达意,不解释)REDIS_SET“intset”
REDIS_ENCODING_SKIPLIST跳跃表和字典(skiplist,见名达意,不解释)REDIS_ZSET“skiplist”

3.1.4 sds(这个很重要,下面会用到)

sds英文全称 simple dynamic string,这里是简单动态字符串

struct sdshdr{
// 记录buf数组中未使用字节的数量
int free;
// 记录buf数组中已使用字节的数量,等于sds所保存字符串的长度
int len;
// 字节数组,用于保存字符串
char buff[];
}

结构(下面介绍五种基本类型底层结构会用到):

free:0 表示这个sds没有分配任何未使用空间;
len:5 表示这个sds保存了一个5个字节的字符串;
buf:Hello 表示一个char类型的数组,数组的前五个字节分别保存了‘H’‘e’‘l’‘l’‘o’,最后一个字符保存了空字符‘\\0’.

3.2 字符串对象string

由上面的编码表可以知道,字符串编码包括三种 int raw embstr,三者对比:

如果保存的是整数值,且可用long类型表示,那么编码设为int;

如果保存的是一个字符串,并且长度大于32字节,那么使用SDS(simple dynamic string,简单动态字符串)保存,编码设为raw;

如果保存的是一个字符串,并且长度小于或等于32字节,那么编码设为embstr;

3.2.1 int编码

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为string
encoding,redis任何一种基本类型至少有两种编码,这里是int
ptr:指针,指向底层数据结构的指针,这里指向底层long类型数据7758258

3.2.2 raw编码

对于这个图的解释:
redisObject: 分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为string
encoding,redis任何一种基本类型至少有两种编码,这里是raw
ptr:指针,指向底层数据结构的指针,这里指向sdshdr
sdshdr :
sds simple dynamic string,简单动态字符串 ; hdr High available Data Replication,高可用性复制 ; 合在一起 sds hdr 简单动态字符串高可用性复制,包括三个 free 表示
free:0 表示这个sds没有分配任何未使用空间;
len:36表示这个sds保存了一个36个字节的字符串;
buf:Hello 表示一个char类型的数组,数组的前36个字节分别保存了’H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’'H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’‘H’‘e’‘l’‘l’‘o’‘ ’‘w’‘o’‘r’‘l’‘d’’.’‘.’‘.’,
最后一个字符保存了空字符‘\\0’.

3.2.3 embstr编码

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为string
encoding,redis任何一种基本类型至少有两种编码,这里是embstr
ptr:指针,指向底层数据结构的指针,这里指向sdshdr
sdshdr :
sds simple dynamic string,简单动态字符串
hdr High available Data Replication,高可用性复制
合在一起 sds hdr 简单动态字符串高可用性复制,包括三个 free 表示
free:0 表示这个sds没有分配任何未使用空间;
len:5 表示这个sds保存了一个5个字节的字符串;
buf:Hello 表示一个char类型的数组,数组的前五个字节分别保存了‘H’‘e’‘l’‘l’‘o’,最后一个字符保存了空字符‘\\0’.

最后点一下,用long double类型表示的浮点数在redis中也是字符串来表示的,了解即可。

编码是指存储类型的方式,字符串对象中embstr编码和raw编码有什么区别?
当字符串长度小于32字节,字符串对象将使用emstr编码,大于32字节,字符串使用raw。
embstr直接一次性创建一块内存,内存一定是连续的;raw会分别两次创建redisObject结构与sdshdr结构,内存不一定是连续的。

embstr优势:
(1) 内存释放更快:内存释放是embstr只需要释放一次,而raw需要释放两次
(2) 查找更快: embstr查找的更快

3.3 列表对象list

由上面的编码表可以知道,字符串编码包括两种:linkedlist ziplist

3.3.1 ziplist编码

zlbytes:表示的是总长度,总字节数
zllen:表示的是数据部分的长度
两个不一样的。

3.3.2 linkedlist编码

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为list
encoding,redis任何一种基本类型至少有两种编码,这里是linkedlist
ptr:指针,指向底层数据结构的指针
1 表示ziplist第一个元素
“three” 表示ziplist第二个元素
5 表示ziplist第三个元素

list两种编码方式(ziplist和linkedlist区别)
选用:当列表对象保存的所有字符串元素的长度都小于64字节,并且列表对象保存的元素数量小于512时,list使用ziplist编码;不能满足这两种情况就是用linkedlist编码。
优缺点:ziplist的特点是节省内存,但是插入速度慢;linkedlist是一个双向列表,特点就是插入速度快,但是占内存。
参考:https://blog.csdn.net/weixin_33775572/article/details/92675537

3.4 哈希对象hash(map)

由上面的编码表可以知道,字符串编码包括两种:ziplist hashtable

3.4.1 ziplist编码

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为list
encoding,redis任何一种基本类型至少有两种编码,这里是ziplist
ptr:指针,指向底层数据结构的指针
zlbytes: 表示整个ziplist的字节数(总长度)
zltail: 表示整个ziplist的头部
zllen: 表示整个ziplist 数据部分的长度
(key,value)=(name,Tom) 表示ziplist第一个元素
(key,value)=(age,25) 表示ziplist第二个元素
(key,value)=(career,Programmer) 表示ziplist第三个元素
zlled 表示整个ziplist的尾部

3.4.2 hashtable编码

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为list
encoding,redis任何一种基本类型至少有两种编码,这里是hashtable
ptr:指针,指向底层数据结构的指针

hash对象的ziplist编码和hashtable编码
(参考:https://blog.csdn.net/zwx900102/article/details/109707329)

当一个哈希对象可以满足以下两个条件中的任意一个,哈希对象会选择使用ziplist编码来进行存储:1、哈希对象中的所有键值对总长度(包括键和值)小于64字节(这个阈值可以通过参数hash-max-ziplist-value 来进行控制)。
2、哈希对象中的键值对数量小于512个(这个阈值可以通过参数hash-max-ziplist-entries 来进行控制)。
一旦不满足这两个条件中的任意一个,哈希对象就会选择使用hashtable来存储。

3.5 集合对象set

由上面的编码表可以知道,字符串编码包括两种:intset hashtable

3.5.1 intset编码

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为set
encoding,redis任何一种基本类型至少有两种编码,这里是intset
ptr:指针,指向底层数据结构的指针

set对象inset和hashtable编码转换:
当Set对象可以同时满足以下两个条件时, 对象使用 intset 编码:
1.Set对象保存的所有元素都是整数值;
2.Set对象保存的元素数量不超过 512 个;
不能满足这两个条件的Set对象需要使用 hashtable 编码。
参考:https://blog.csdn.net/chongfa2008/article/details/118754079

3.5.2 hashtable编码

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为set
encoding,redis任何一种基本类型至少有两种编码,这里是hashtable
ptr:指针,指向底层数据结构的指针

3.6 有序集合对象sortedset

有序集合是list和set的一种居中,如下:

由上面的编码表可以知道,字符串编码包括两种:ziplist skiplist

3.6.1 ziplist编码

ziplist编码的有序集合对象使用压缩列表作为底层实现。每个集合使用2个紧挨在一起的压缩列表节点来保存,第一个保存元素的成员,第二个保存元素的分值。压缩列表内的集合按分值从小到大排序,分值较小的元素被放置在靠近表头的位置,分值较大的元素在靠近表尾的位置。

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为sorted set
encoding,redis任何一种基本类型至少有两种编码,这里是ziplist
ptr:指针,指向底层数据结构的指针
zlbytes: 表示整个ziplist的字节数(总长度)
zltail: 表示整个ziplist的头部
zllen: 表示整个ziplist 数据部分的长度
(key,value)=(apple,8.5) 表示ziplist第一个元素
(key,value)=(banana,5.0) 表示ziplist第二个元素
(key,value)=(cherry,6.0) 表示ziplist第三个元素
zlled 表示整个ziplist的尾部

3.6.2 skiplist编码

skiplist编码的有序集合对象使用 zset结构作为底层实现,zset结构同时包含一个字典和一个跳跃表。如下:

typedef struct zset{
dict *dict;                // 字典dict
zskiplist  *zsl;           // 跳跃表zsl
}zset;   //zset是有序集合,同时由字典dict和跳跃表zsl实现

为什么有序集合zset(sorted set)要同时由字典dict和跳跃表实现?

跳跃表利于执行范围操作(跳跃表是排好序的),而字典有利于执行分值查找操作。同时由于Redis里的跳跃表和字典元素很多都是用指针实现的,所以不会浪费内存。

对于这个图的解释:
redisObject:
分为三个 类型type 编码encoding 指针ptr
type为五种基本类型,这里为sorted set
encoding,redis任何一种基本类型至少有两种编码,这里是skiplist
ptr:指针,指向底层数据结构的指针

zset 表示sorted set 实体
dict 表示字典
zsl 表示sorted skiplist 跳跃表
…… 表示跳跃
(key,value)=(apple,8.5) 表示ziplist第一个元素
(key,value)=(banana,5.0) 表示ziplist第二个元素
(key,value)=(cherry,6.0) 表示ziplist第三个元素

有序集合zset两个编码类型

问题:ziplist数据结构是如何保证zset有序的?
回答:通过value值,ziplist要保证集合中的数据有序,会将key放在前面一位,然后将key所对应value放在key的后一位,这样就能够保证集合的有序。

问题:skiplist数据结构是如何保证zset有序的?
回答:跳跃链表本来就是有序的,直接使用即可

问题:ziplist和skiplist的编码转换?
回答: 当有序集合对象同时满足以下两个条件时,对象使用压缩链表编码:
(1) 有序集合保存的元素数量小于128个;
(2) 有序集合保存的所有元素成员的长度都小于64字节;
如果不满足上述两个条件,那么ZipList会转化为SkipList,同时,当后面的SkipList的元素数量和元素成员的长度满足要求时,也不会回退为ZipList。
参考:https://blog.csdn.net/ABOOMMMMM/article/details/117607126

3.7 五种基本类型的特点与应用

3.7.1 String数据类型的特点与应用

String的特点:最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。

String的应用

第一,缓存功能,数据库缓存用redis的string类型实现

因为String字符串是各种语言都支持的、最常用的数据类型,不仅仅是Redis;因此,在redis连接各种语言的时候,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。

金手指:redis中String作为缓存,也是String的最常用的,redis最常用的。

第二,计数器,计数用redis的string类型实现

许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。

金手指:redis持久化包括AOF热备份和RDB冷备份。

第三,共享用户Session,共享用户session放到redis的String中存储

用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成,大大提高效率。

注意:真实的开发环境中,很多人可能会把很多比较复杂的结构也统一转成String去存储使用,比如有的人就喜欢把对象或者List转换为JSONString进行存储,拿出来再反序列化,但是啥都是用的String不够优雅。但是,总原则还是:在最合适的场景使用最合适的数据结构,对象找不到最合适的但是类型可以选最合适的。

3.7.2 Hash数据类型的特点与应用

Hash的特点

这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。

Hash的应用

hash的应用场景很少,因为现在很多对象都是比较复杂的,比如你的商品对象可能里面就包含了很多属性,其中也有对象,单一hash无法满足。

3.7.3 List数据类型的特点与应用

List特点

有序列表,有序,元素可以重复

List应用

(1)列表型数据item-detail:比如可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。比如,对于csdn博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表。

(2)分页数据存储:比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。比如:对于csdn博客网站,当文章多时,item需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能,大大提高查询效率。

(3)消息队列:比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

3.7.4 Set数据类型的特点与应用

Set特点:无序集合,不保证顺序但是提供自动去重的功能,我们最重要的是使用他这个去重功能,下面两个实例都是。

Set应用

(1)分布式多服务器全局数据去重:直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。

(2)集合的交并补运算:可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,可以得到俩人的共同好友是谁,比如qq中你和一个人有多少个共同好友。

3.7.5 SortedSet数据类型的特点与应用

SortedSet的特点: 是排序的 Set,去重但可以排序,我们最重要的使用它这个自定义排序的功能。

SortedSet的应用

(1) 当前item排序:写进去的时候给一个分数,自动根据分数排序:有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。

(2) 各种排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单排序依据可能是多方面:按照时间、按照播放量、按照获得的赞数等。微博热搜榜,就是有个后面的热度值,前面就是名称。为什么不用list来做排行榜?list无法自定义顺序比较,无法保证无重复元素。

(3) 权重排序:用Sorted Sets来做带权重的队列:比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

sortedSet和list都排序,两者区别:
(1) 特点不同:sortedSet元素自动去重,list无法完成;sortedSet自定义排序器,list无法完成。
(2) 应用不同:sortedSet用于对当前item排序,排行榜,权重排序,都是一样的;list用于制作item,lrange制作item分页,消息队列FIFO.

小结:Redis基础数据类型有五种,总原则就是:在最合适的场景使用最合适的数据结构,不要什么都用string去实现。一般来说,一个好的面试题,是对于不同层级的人可以给出不同深度的答案,就比如redis五种基本类型,一定要从底层结构图和项目实践在什么地方使用这个数据类型两方面来说,同时兼顾底层和项目。

四、Redis七种特殊数据类型

4.1 位图Bitmap

4.1.1 位图的特点

位图可以用最小的空间存放最大量的数据,比如,对于bool类型,按位存放,用 0|1 来表示 false|true,由于一个字节8位,相对于String类型用一个字节来存放 true|false ,使用位图的方式,空间缩小为原来的 1/8 。

值得注意的是,位图不是一个独立的数据结构,从底层来说,它是string字符串类型。

位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 将 byte 数组看成位数组来处理。

4.1.2 位图的应用

位图的最常见的应用就是用位来存储bool类型的true|false,比如统计签到和统计日活/月活。

4.1.2.1 统计签到

业务情景1:在我们平时开发过程中,会有一些 bool 型数据需要存取(即只有两个值 true|false 0|1),比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。

解决:为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。

具体实现:
key 可以设置为 “前缀:用户id:年月” ,譬如 setbit sign:123:1909 0 1 代表用户ID=123签到,签到的时间是19年9月份,0代表该月第一天,1代表签到了。第二天没有签到,无需处理,系统默认为0,第三天签到 setbit sign:123:1909 2 1,可以查看一下目前的签到情况,显示第一天和第三天签到了,前8天目前共签到了2天,如下:

127.0.0.1:6379> setbit sign:123:1909 0 1     set 第一天签到了
0
127.0.0.1:6379> setbit sign:123:1909 2 1    set 第三天签到了
0
127.0.0.1:6379> getbit sign:123:1909 0       get  查看第一天是否签到,返回为1,第一天签到了
1
127.0.0.1:6379> getbit sign:123:1909 1     get 查看第二天是否签到,返回为0,第二天没签到
0
127.0.0.1:6379> getbit sign:123:1909 2    get 查看第三天是否签到,返回为1,第三天签到了
1
127.0.0.1:6379> getbit sign:123:1909 3    get查看第四天是否前端,返回为0,第四天没签到
0
127.0.0.1:6379> bitcount sign:123:1909 0 0    count查看所有签到天数,返回为2,一共两天签到
2

4.3.1.2 统计日活/月活

业务情景:当我们要统计日活/月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。

解决:可以看作是存储bool类型数据问题,所以可以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活跃。然后到第二天/月底遍历一次位图就可以得到日度活跃用户数/月度活跃用户数。

4.2 HyperLogLog

从统计页面PV到统计页面UV,从set数据类型到HyperLogLog数据类型

4.2.1 HyperLogLog的特点

HyperLogLog数据结构,添加操作命令为pfadd,查看数量命令为pfcount,两个命令中这个 pf 是HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的首字母缩写。

HyperLogLog与上面使用的位图bitmap一样,在完成同一业务下,使用更小的空间存储数据(相对于set数据结构)。其核心应用在于计数,常见的业务是统计网页的PV和UV。

4.2.2 HyperLogLog的应用

业务情景:统计网页的PV和UV

如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。但是 UV 不一样,它要去重,即同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。

当前问题:PV直接统计数量就好了,UV要在统计PV数量上根据用户id去重
注意1:无论是PV,还是UV,都不需要特别精准的数据,一个大致数据就好了
注意2:无论是PV,还是UV,都是针对页面来说的,页面的PV,页面的UV

对于统计UV,这里提供两种方案,Set数据类型和HyperLogLog数据类型

方案一:使用set数据类型

理由:set数据结构自带去重,只要value是用户id,会自动去重

用法与优点:使用set数据结构,为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。

缺点:第一,爆款页面:如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。
第二,多个页面:如果页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?

方案二:使用HyperLogLog数据类型

HyperLogLog的两个命令:HyperLogLog 提供了两个指令 pfadd 和 pfcount,顾名思义,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是;pfcount 和 scard 用法是一样的,直接获取计数值。

具体实现(HyperLogLog数据类型统计UV)

127.0.0.1:6379> pfadd codehole user1     // 对于HyperLogLog类型变量codehole,添加变量user1   
(integer) 1
以上是关于Redis,性能加速的催化剂的主要内容,如果未能解决你的问题,请参考以下文章

Redis,性能加速的催化剂

服务端主流中间件博文汇总

Excel催化剂开源第51波-Excel催化剂遍历单元格操作性能保障

Redis 性能调优——缓存设计优化

区块链行业简报 | 疫情后会央行会加速推出数字货币

Excel催化剂开源第16波-VSTO开发之脱离传统COM交互以提升性能