文章目录

一、前言

起手式(Redis应用场景 + 缓存 + 分布式锁 + 消息队列 + 数据类型 + 三个问题),这些是Redis基本的,必定提问的问题。

二、redis应用场景(以csdn博客网站为例)

2.1 因为缓存所以需要Redis

问题:为什么使用redis?
回答:两个字,缓存。
解释:因为传统的关系型数据库如Mysql已经不能适用所有的场景了,比如秒杀的库存扣减,APP首页的访问流量高峰等等,都很容易把数据库打崩,所以引入了缓存中间件,目前市面上比较常用的分布式缓存中间件有Redis和Memcached不过中和考虑了他们的优缺点,最后选择了Redis。

2.2 Redis使用场景(以csdn博客网站为例)

redis使用场景(以csdn博客网站为例)
1、记录帖子的点赞数、评论数和点击数 (hash)。
2、记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。
3、记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。
4、记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。
5、缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。
6、记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。
7、如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。
8、收藏集和帖子之间的关系 (zset)。
9、记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。
10、缓存用户行为历史,进行恶意行为过滤 (zset,hash)。
11、数据推送去重Bloom filter
12、pv,uv统计(pv page view 一个用户访问n次,记为n,uv user visitor 一个用户访问n次,记为1)

三、Redis两个作用:缓存 + 分布式锁

Redis开发中两大功能:缓存 + 分布式锁
缓存就是本地缓存、分布式缓存、分级缓存
分布式锁,就是分布式锁

3.1 Redis第一作用:分布式缓存(本地缓存、分布式缓存、分级缓存)

本地缓存、分布式缓存、分级缓存 在高并发的五利器博文中讲到过,这里不再赘言。

3.2 Redis第二作用:分布式锁

3.2.1 分布式锁的引入(分布式互斥 分布式通信)

分布式锁:多个系统同时操作(并发)Redis带来的数据问题?处理方式?
问题:多个系统同时操作(并发)Redis带来的数据问题?
嗯嗯这个问题我以前开发的时候遇到过,其实并发过程中确实会有这样的问题,比如下面这样的情况
走进Redis,显微镜下窥真身_1024程序员节
系统A、B、C三个系统,分别去操作Redis的同一个Key,本来顺序是1,2,3是正常的,但是因为系统A网络突然抖动了一下,B,C在他前面操作了Redis,这样数据不就错了么。
A 下单
B 支付
C 退款
就好比A下单,B支付,C退款三个顺序你变了,变成B支付、C退款、A下单,订单还没生成你却支付,退款了,明显走不通了。

处理方式:分布式锁
引入第三方中间件,找个管家帮我们管理好数据,比如zookeeper。
走进Redis,显微镜下窥真身_# (1)缓存Redis_02
在java并发中,要保证多线程安全,就要实现线程互斥(synchronized和lock)和线程通信(标志位+wait+notify()/notifyAll() 标志位+await()+signal()/signalAll())
现在在redis中,使用分布式锁,将各个系统看做各个线程,redis看做服务端程序,
java并发中,各个线程可以访问java程序中的共享变量,即非ThreadLocal变量;
在redis中,各个系统可以访问redis中的共享变量,一个意思。
分布式锁保证互斥:某个时刻,多个系统实例都去更新某个 key。可以基于 Zookeeper 实现分布式锁。每个系统通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。
分布式锁保证通信
理论-时间戳保证通信:要写入缓存的数据,都得写入 MySQL 中进行持久化,写入 MySQL 中的时候必须保存一个时间戳,从 MySQL 查出来的时候,时间戳也查出来。
实践-时间戳保证通信:每次要写之前,先判断一下当前这个 Value 的时间戳是否比缓存里的 Value 的时间戳要新,如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据;每次读操作,读取时间戳,给下一次写对比数据库中时间戳用的。

3.2.2 分布式锁的实现(三个命令 setnx expire del)

Redis分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑,先来先占, 用完了,再调用 del 指令释放茅坑。

setnx aobing // setnx 如果没有别人加锁的话,我加锁
expire aobing // 设置过期时间,这里表示执行中,操作过程
del aobing // 用完了,释放锁

但是引入后会有问题,原子性,怎么解决,就比如setnx成功,设置失效时间expire的时候失败,怎么办?

当时出现了很多的第三方插件,redis为了解决这个乱象,就在Redis 2.8 版本中作者加入了 set 指令的扩展参数:

set aobing ture ex 5 nx
del aobing

但是这样还是有问题超时问题,可重入问题等等,这个时候,第三方的一些插件就横空出世了,Redission ,Jedis,他们的底层我就不过多描述了,都是通过lua脚本去保证的,底层源码跟我们代码实现是差不多的。

就比如去删除的时候,去校验是否当前线程锁定的,就把比较和删除这样一些动作都放到一起了:

# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then  
    return redis.call("del",KEYS[1])
else
    return 0
end

俄罗斯套娃式问题:Redis分布式锁
第一,Redis分布式锁介绍?
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
第二,如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

3.3 redis延时队列

3.3.1 三个消息队列

我们平时习惯于使用 Rabbitmq 、RocketMQ和 Kafka 作为消息队列中间件,来给应用程序之间增加异步消息传递功能。这三个中间件都是专业的消息队列中间件,特性之多超出了大多数人的理解能力。

正因为是专业的消息队列中间件,所以,使用起来比较复杂,比如 Rabbitmq ,发消息之前要创建 Exchange,再创建 Queue,还要将 Queue 和 Exchange 通过某种规则绑定起来,发消息的时候要指定 routing-key,还要控制头部信息。消费者在消费消息之前也要进行上面一系列的繁琐过程。

但是绝大多数情况下,虽然我们的消息队列只有一组消费者,但还是需要经历上面这些繁琐的过程。

3.3.2 redis自带的简易队列

金手指:
mysql架构中,自带缓存层,但是实现的不是很好,一个insert/delete/update操作,缓存就都会消失,所以出现了专业的缓存redis
mysql架构:mysql服务层(连接器Connection+缓存层+分析器+优化器+执行器)+存储引擎层
redis作为缓存,在高并发的情况下,需要对请求进行管理,所以实现了一个延迟队列,但是,redis的延迟队列只是一个简易的,所以出现了专业的消息队列rabbitmq、activemq kafka

问题1:为什么需要缓存?
回答1:对于相同的查询(怎么查询是相同的,有判断依据),2-n次就可以直接用第一次的结果了,提高效率。
问题2:为什么需要mq?
回答2:假设没有mq,只有redis和mysql,高并发写场景下,比如说一个用户业务操作里要频繁搞数据库几十次,增删改增删改(注意,因为不是相同select操作,所以redis缓存没用),疯了。那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以大量的增删改压力一瞬间都会到mysql上面去,造成崩溃。
假设使用mq,在redis和mysql的基础上就加一个mq,mq将大量的写请求灌入队列里面,排队慢慢玩儿,后边系统消费后慢慢写,将高并发的大量增删改请求控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok 的。

有了 Redis,它就可以让我们从专业的MQ中解脱出来,对于那些只有一组消费者的消息队列,使用 Redis 就可以非常轻松的搞定(多组消费者的,还是要专业的MQ)

Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。

走进Redis,显微镜下窥真身_1024程序员节_03

解释:
lpop 左边出
lpush 左边进
rpop 右边出
rpush 右边进

rpush notify-queue apple banana pear // 名为notify-queue的变量中放入三个元素,右边进
(integer) 3
llen notify-queue // 查看名为notify-queue的变量长度
(integer) 3
lpop notify-queue // 第一次,pop队首,左边出
“apple”
llen notify-queue // 查看名为notify-queue的变量长度,为2
(integer) 2
lpop notify-queue // 第二次,pop队首,左边出
“banana”
llen notify-queue // 查看名为notify-queue的变量长度,为1
(integer) 1
lpop notify-queue // 第三次,pop队首,左边出
“pear”
llen notify-queue // 查看名为notify-queue的变量长度,为0
(integer) 0
lpop notify-queue // 第四次,pop队首,左边出,返回为pop的元素,这里返回nil,表示没有pop任何元素,因为队列中没有了元素
(nil)

但是这样有问题大家发现没有?队列会空是吧,那怎么解决呢?

队列消费者的客户端是通过队列的 pop 操作来获取消息,然后进行处理,处理完了再接着获取消息,再进行处理。如此循环往复,这便是作为队列消费者的客户端的生命周期。

如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据,这就是浪费生命的空轮询。

空轮询不但拉高了客户端的 CPU,redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。

解决方式很简单,让线程睡一秒 Thread.sleep(1000)

应用:
Redis延迟队列,在一些简易后台可以用到,因为没必要接入专门的消息队列中间件。

四、Redis 五种基本数据类型 + 七种特殊数据类型

Redis有哪些数据结构呀?5种基本(底层+应用) + 7种特殊 (点到就好)
String、Hash、List、Set、SortedSet。
HyperLogLog、Geo、Pub/Sub、Redis Module(BloomFilter,RedisSearch,Redis-ML),
BloomFilter是重点,有单独一篇文章

4.1 五种基本数据类型的底层

4.1.0 起手式,redisObject对象(类型type+编码encoding)和sds(free+len+buf)

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

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

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

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

typedef struct redisObject {
    //类型
    unsigned type:4;
    
    //编码
    unsigned encoding:4;

    //指向底层实现数据结构的指针
    void *ptr;

    // ...
} robj;

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

4.1.0.1 类型type 五种类型string hash list set zset

类型常量 对象名称 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命令时,命令返回的结果是数据库键对应的值对象的类型,而不是键对象的类型。
其实,这些东西都是一些概念理论上的纠结,实际开发中,我们以实现需求为主,也不一定要区分的这么清楚,当然面试中可能用得到。

4.1.0.2 编码encoding

编码常量 编码对应的底层数据结构 redis中具体类型(5种) OBJECT ENCODING命令输出
REDIS_ENCODING_INT long类型整数(编码常量后缀是INT,但是其实现的底层数据结构是long) REDIS_STRING “int”
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串(SDS simple dynamic string) REDIS_STRING “embstr”
REDIS_ENCODING_RAW 简单动态字符串(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”

string int embstr raw
set inset hashtable
hash ziplist hashtable
list ziplist linkedlist
zset ziplist skiplist

string int embstr raw
hash ziplist hashtable
list ziplist linkedlist
set intset hashtable
zset ziplist skipedlist

4.1.0.3 sds(简单动态字符串,这个很重要,下面会用到)

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

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

结构(下面介绍五种基本类型底层结构会用到):
走进Redis,显微镜下窥真身_1024程序员节_04

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

4.1.1 String底层(唯一三种编码,其他四种都是两种编码)

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

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

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

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

4.1.1.1 String类型 int编码

走进Redis,显微镜下窥真身_# (1)缓存Redis_05

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

4.1.1.2 String类型 raw编码

走进Redis,显微镜下窥真身_其他_06

对于这个图的解释:
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’.

4.1.1.3 String类型 embstr编码

走进Redis,显微镜下窥真身_其他_07

对于这个图的解释:
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中也是字符串来表示的,了解即可。

4.1.2 list底层

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

4.1.2.1 ziplist编码

走进Redis,显微镜下窥真身_1024程序员节_08

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

4.1.2.2 linkedlist编码

走进Redis,显微镜下窥真身_1024程序员节_09

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

4.1.3 哈希对象hash(map)

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

4.1.3.1 ziplist编码

走进Redis,显微镜下窥真身_其他_10

对于这个图的解释:
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的尾部

4.1.3.2 hashtable编码

走进Redis,显微镜下窥真身_# (1)缓存Redis_11

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

4.1.4 集合对象set

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

4.1.4.1 intset编码

走进Redis,显微镜下窥真身_# (1)缓存Redis_12

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

4.1.4.2 hashtable编码

走进Redis,显微镜下窥真身_其他_13

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

4.1.5 有序集合对象sortedset

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

4.1.5.1 ziplist编码

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

走进Redis,显微镜下窥真身_1024程序员节_14

对于这个图的解释:
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的尾部

4.1.5.2 skiplist编码

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

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

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

走进Redis,显微镜下窥真身_其他_15

对于这个图的解释:
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第三个元素

4.2 五种基本数据类型的应用

4.2.1 String的特点 + String的应用(三个:缓存+计数器+分布式锁共享session)

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不够优雅。
但是,总原则还是:在最合适的场景使用最合适的数据结构,对象找不到最合适的但是类型可以选最合适的。

4.2.2 Hash的特点 + Hash的应用(0 单一hash无法满足)

hash的特点:
这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。
hash的应用:
hash的应用场景很少,因为现在很多对象都是比较复杂的,比如你的商品对象可能里面就包含了很多属性,其中也有对象,单一hash无法满足

4.2.3 List的特点 + List的应用(3个 列表型数据item-detail、分页数据存储、消息队列)

List特点:有序列表,有序,元素可以重复
List应用
(1)列表型数据item-detail:比如可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。比如,对于csdn博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表。
(2)分页数据存储:比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。比如:对于csdn博客网站,当文章多时,item需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能,大大提高查询效率。
(3)消息队列:比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

4.2.4 Set的特点 + Set的应用(2个 自带数据去重+集合交并补运算)

Set特点:无序集合,不保证顺序但是提供自动去重的功能,我们最重要的是使用他这个去重功能,下面两个实例都是
Set应用
(1)分布式多服务器全局数据去重:直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。
(2)高中知识,集合的交并补运算:可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,可以得到俩人的共同好友是谁,比如qq中你和一个人有多少个共同好友。

4.2.5 SortedSet的特点 + SortedSet的应用(3个 排序:item排序、视频访问量排行榜、权重排行榜)

SortedSet的特点: 是排序的 Set,去重但可以排序,我们最重要的使用它这个自定义排序的功能
SortedSet的应用
(1)当前item排序:写进去的时候给一个分数,自动根据分数排序:有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。
(2)各种排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单排序依据可能是多方面:按照时间、按照播放量、按照获得的赞数等。微博热搜榜,就是有个后面的热度值,前面就是名称。为什么不用list来做排行榜?list无法自定义顺序比较,无法保证无重复元素。
(3)权重排序:用Sorted Sets来做带权重的队列:比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

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

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

4.3 七种特殊数据类型(点到就好)

4.3.1 位图bitmap(核心:用位来存储bool类型的true|false,业务:统计签到和日活)

4.3.1.1 统计签到

位图情景1(redis统计一年签到,bool类型,签了是 1,没签是 0) + 解决1
业务情景1:在我们平时开发过程中,会有一些 bool 型数据需要存取(即只有两个值 true|false 0|1),比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。
解决1:为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
具体实现1
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 统计日活/月活

统计日活/月活,0 表示不活跃,1 表示活跃
业务情景2:当我们要统计日活/月活的时候,因为需要去重,需要使用 set 来记录所有活跃用户的 id,这非常浪费内存。
解决2:可以看作是存储bool类型数据问题,所以可以考虑使用位图来标记用户的活跃状态。每个用户会都在这个位图的一个确定位置上,0 表示不活跃,1 表示活跃。然后到第二天/月底遍历一次位图就可以得到日度活跃用户数/月度活跃用户数。

4.3.1.3 位图bitmap底层原理

问题1:为什么bitmap可以用最小的空间存放最大量的数据?bitmap底层原理
回答1:对于bool类型,按位存放,比如,上面的,由于一个字节8位,365/8=45.6字节,所以是46字节。

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

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

4.3.2 HyperLogLog,核心:计数,业务:统计PV UV

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

业务情景:从页面的统计PV到统计页面的UV
如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。
但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。
问题:PV直接统计数量就好了,UV要在统计PV数量上根据用户id去重
注意1:无论是PV,还是UV,都不需要特别精准的数据,一个大致数据就好了
注意2:无论是PV,还是UV,都是针对页面来说的,页面的PV,页面的UV

方案一:使用set数据类型(理由:set数据结构自带去重,只要value是用户id,会自动去重)
用法与优点:使用set数据结构(理由:set数据结构自带去重只要value是用户id,会自动去重),为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。
缺点:第一,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。
第二,如果页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?

方案二:使用HyperLogLog数据类型
HyperLogLog的两个命令:HyperLogLog 提供了两个指令 pfadd 和 pfcount,顾名思义,一个是增加计数,一个是获取计数。
从set集合到HyperLogLog
pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是;
pfcount 和 scard 用法是一样的,直接获取计数值。
具体实现(HyperLogLog数据类型统计UV)
127.0.0.1:6379> pfadd codehole user1 // 对于HyperLogLog类型变量codehole,添加变量user1
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为1
(integer) 1
127.0.0.1:6379> pfadd codehole user2 // 对于HyperLogLog类型变量codehole,添加变量user2
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为2
(integer) 2
127.0.0.1:6379> pfadd codehole user3 // 对于HyperLogLog类型变量codehole,添加变量user3
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为3
(integer) 3
127.0.0.1:6379> pfadd codehole user4 // 对于HyperLogLog类型变量codehole,添加变量user4
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为4
(integer) 4
127.0.0.1:6379> pfadd codehole user5 // 对于HyperLogLog类型变量codehole,添加变量user5
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为5
(integer) 5
127.0.0.1:6379> pfadd codehole user6 // 对于HyperLogLog类型变量codehole,添加变量user6
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为6
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10 // 对于HyperLogLog类型变量codehole,添加变量user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole // pfcount命令对于HyperLogLog类型变量codehole计数,为10
(integer) 10

4.3.2.2 HyperLogLog底层原理

问题1:pfadd pfcount这个 pf 是什么意思?
回答1:pf是 HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的首字母缩写,
问题2:为什么使用hyperLogLog不使用set?hyperLogLog底层原理?
回答2:和上面使用位图bitmap一样,在完成同一业务下,使用更小的空间存储数据,底层略。

4.3.2.3 HyperLogLog局限:只能添加和计数,无法判断是否存在contains exists,布隆过滤器的引入

HyperLogLog局限
HyperLogLog 数据结构来进行估数,它非常有价值,可以解决很多精确度不高的统计需求。
但是如果我们想知道某一个值是不是已经在 HyperLogLog 结构里面了,它就无能为力了,它只提供了 pfadd 和 pfcount 方法,没有提供 pfcontains 这种方法。
业务场景
比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?

方案一:在服务端记录用户看过的所有历史记录,
当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上无法满足。
127.0.0.1:6379> bf.add codehole user1 // bf.add 添加 user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2 // bf.add 添加 user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3 // bf.add 添加 user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1 // bf.exists user1,返回为1,存在
(integer) 1
127.0.0.1:6379> bf.exists codehole user2 // bf.exists user2,返回为1,存在
(integer) 1
127.0.0.1:6379> bf.exists codehole user3 // bf.exists user3,返回为1,存在
(integer) 1
127.0.0.1:6379> bf.exists codehole user4 // bf.exists user4 返回为0,不存在
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6 // bf.madd 添加user4 user5 user6
(1) (integer) 1
(2) (integer) 1
(3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 // bf.exists user4 user5 user6 user7 前面三个返回1,存在,user7返回为0,不存在
(1) (integer) 1
(2) (integer) 1
(3) (integer) 1
(4) (integer) 0

方案二:使用布隆过滤器,判断是否存在 contains|exists
第一,布隆过滤器的initial_size设置
布隆过滤器的initial_size估计的过大,会浪费存储空间,
布隆过滤器的initial_size估计的过小,就会影响准确率,
用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。
第二,布隆过滤器的error_rate设置
布隆过滤器的error_rate越小,需要的存储空间就越大,对于不需要过于精确的场合,error_rate设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。
第三,布隆过滤器的应用(核心:布隆过滤器快速判断是否存在,去重功能)
1、爬虫去重,解释:爬虫快速判断是否存在,对于已经存在的去重
在爬虫系统中,我们需要对 URL 进行去重,已经爬过的网页就可以不用爬了。但是 URL 太多了,几千万几个亿,如果用一个集合装下这些 URL 地址那是非常浪费空间的。这时候就可以考虑使用布隆过滤器。它可以大幅降低去重存储消耗,只不过也会使得爬虫系统错过少量的页面。
2、NoSQL数据库,解释:不在数据库的row不到数据库磁盘中去找
布隆过滤器在 NoSQL 数据库领域使用非常广泛,我们平时用到的 HBase、Cassandra 还有 LevelDB、RocksDB 内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询。
3、垃圾邮件过滤,解释:设置判断算法,如果是满足判断算法,算做垃圾邮件
邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低。

4.3.3 Geospatial,略,点一下就好了

作用:用来保存地理位置,并作位置距离计算或者根据半径计算位置等。即可以用Redis来实现附近的人或者计算最优地图路径。

业务实际:查找附近的人,使用GeoHash数据类型,使用布隆过滤器去重

4.3.4 Pub/Sub,略,点一下就好了

作用:功能是订阅发布功能,可以用作简单的消息队列。

4.3.5 Pipeline,略,点一下就好了

作用:可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。

Pipeline有什么好处,为什么要用pipeline?
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

4.3.6 Lua脚本,略,点一下就好了

Redis 支持提交 Lua 脚本来执行一系列的功能。电商项目中,秒杀场景经常使用Lua脚本,利用他的原子性。

4.3.7 Redis事务,略,点一下就好了

最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。

4.4 小结

第一,Bitmap位图
原理:支持按 bit 位来存储信息
作用:用来实现 布隆过滤器(BloomFilter);
第二,HyperLogLog
作用:供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV;
第三,Geospatial
作用:用来保存地理位置,并作位置距离计算或者根据半径计算位置等。即可以用Redis来实现附近的人或者计算最优地图路径。
第四,pub/sub
作用:功能是订阅发布功能,可以用作简单的消息队列。
第五,Pipeline
作用:可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
第六,Lua脚本
Redis 支持提交 Lua 脚本来执行一系列的功能。电商项目中,秒杀场景经常使用Lua脚本,利用他的原子性。
高并发博客中“电商项目的秒杀设计”中讲到,库存预热带来的问题(解释:现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的),使用redis中的Lua脚本的原子性处理。
第七,事务
最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。

五、缓存雪崩 + 缓存穿透 + 缓存击穿 + 缓存更新 + 缓存数据一致性问题

见《高并发五种利器》博客。

Redis从三个时间段去分析下、Redis知识点也是这些:
事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL被打死。
事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

六、面试金手指 七、小结

【Redis 第一篇】起手式(Redis应用场景 + 缓存 + 分布式锁 + 消息队列 + 数据类型 + 三个问题),完成了。

天天打码,天天进步!!!