https://blog.csdn.net/ThinkWon/article/details/103522351?

Redis

概述

什么是数据库?什么是关系型数据库?什么是非关系型数据库?

数据库是数据的仓库。与普通的“数据仓库”不同的是,数据库依据“数据结构”来组织数据,也是因为“数据结构”,所以我们看到的数据是比较“条理化”的(大量数据分成一个个库,分成一个个表,分成一条条记录,这些记录是多么分明) 也因为其“数据结构”式,所以有极高的查找速率 。 数据库管理系统是一个软件,是数据库管理的程序实现。
关系型数据库是依据关系模型来创建的数据库。而所谓关系模型就是“一对一、一对多、多对多”等关系模型,就是指二维表格模型,因而一个关系型数据库就是由二维表及其之间的联系组成的一个数据组织。
关系型数据可以很好地存储一些关系模型的数据,比如一个老师对应多个学生的数据(“多对多”),一本书对应多个作者(“一对多”),一本书对应一个出版日期(“一对一”)。
Oracle 、 SQL Serve、MySQL 就是属于关系型数据库。
非关系型数据库主要是基于“非关系模型”的数据库(由于关系型太大,所以一般用“非关系型”来表示其他类型的数据库)
非关系型模型比如有:

  • 列模型:存储的数据是一列列的。关系型数据库以一行作为一个记录,列模型数据库以一列为一个记录。(这种模型,数据即索引,IO很快,主要是一些分布式数据库)。
  • 键值对模型:存储的数据是一个个“键值对”,比如name:liming,那么name这个键里面存的值就是liming


什么是Redis

是什么

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。
Redis 可以存储键和五种不同类型的值之间的映射。说详细点就是键的类型只能为字符串,而值支持五种数据类型:字符串string、列表list、集合set、散列表hash、有序集合zset。
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

Redis有哪些优缺点

优点

  • 读写性能优异, Redis能读的速度是11W次/s,写的速度是8W1次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。支持主从复制,主机会自动将数据同步到从机,可以进行读写分离

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

为什么使用Redis?

主要从“高性能”和“高并发”这两点来看待这个问题。

从高并发方面来说

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

从高性能方面来说

用户第一次访问数据库中的某些数据的过程会比较慢,因为是从硬盘上读取的。第一次从硬盘中获取到查询数据后将用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。而操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

为什么要用 Redis 而不用 map/guava 做缓存?(从跨jvm方向思考)

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,而本地缓存的最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。
但缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

为什么Redis是单线程还能这么快?

从五个方面来说:
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速,达到纳秒级。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、高效的数据存储结构,有全局的hash表(类似HashMap 结构)以及各种跳表,压缩列表,链表等等。
3、命令的执行采用单线程,避免了不必要的上下文切换和竞争条件的开销,也不存在多进程或者多线程导致的切换而消耗 CPU;
4、底层基于非阻塞多路 I/O 复用模型提升IO效率;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

Redis到底是单线程还是多线程

Redis 6.0版本之前的单线程指的是所有的网络IO和键值对读写操作都是是由一个线程完成的。
其实通常说的 Redis 是单线程,主要是指 Redis 对外提供键值存储服务的主要流程,即网络 IO 和键值对读写都是由⼀个线程来完成的。
Redis6.0引入的多线程指的是网络请求过程中采用了多线程,而Redis服务器端的键值对读写操作命令仍然是单线程处理的,内部依然需要排队,所以Redis依然是并发安全的。
也就是只有网络请求模块和数据操作模块是单线程的,除此外 Redis 的其他功能,比如持久化、 异步删除、集群数据同步等,是由额外的线程执⾏的。

数据类型相关

之前面试问的问题都是五大数据类型常见的命令,现在不行了,还得知道使用场景;
细节:redis中命令不区分大小写而key是区分大小写的;

Redis传统的五大数据类型是什么

从有哪些,是什么,常用命令有哪些,应用场景是什么这几个方面回答;
Redis除了拿来做缓存,你还见过基于Redis的什么用法?(其实就是问应用场景)
一般认为Redis常用的有五大数据类型:

1、String字符串

第一个是string字符串类型,在这种类型里一个key对应一个value,是Redis最基本的数据类型 ,其他几种数据结构都是在字符串类型基础上构建的 ,一个Redis中字符串value最多可以是512M
它是二进制安全的。这就意味着Redis的string类型可以包含任何数据。比如jpg图片或者序列化的对象。
它底层的数据结构为简单动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

常用命令:set key valueget key、 设置一个key与一个获取key的值
mset k1 v1 k2 v2mget k1 k2 设置多个key与多个获取key的值
incr key1decr key1 将key的值增1、减1
incrby key1 ndecrby key1 n 将key的值增n、减n
strlen key 获取key的字符串长度
setnx key value 跟分布式锁相关,key不存在则添加,key已经存在则不做任何改变

应用场景:
缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。
商品编号、订单号采用INCR命令生成、
是否喜欢的文章:喜欢就调用incr key1命令将爱心+1,不喜欢就用decr key1将爱心-1;
阅读数:只要点击了rest地址,直接可以使用incr key命令增加一个数字1,完成记录数字。

2、List列表

Redis 列表是简单的字符串列表按照插入顺序排序。可以将一个元素添加到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

常用命令
向列表左边添加元素 LPUSH key value [value ...]
向列表右边添加元素 RPUSH key value [value ...]
查看列表 LRANGE key start stop
获取列表中元素的个数LLEN key
应用场景
微信文章订阅号:
image.png
image.png
image.png

3、Set集合

它是一个无序唯一的键值集合,也就是说它的存储顺序不会按照插入的先后顺序进行存储。 Set集合是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)
Set数据结构是dict字典,字典是用哈希表实现的。
常用命令:
添加元素**SADD key member [member ...]**
删除元素 **SREM key member [member ...]**
获取集合中的所有元素**SMEMBERS key**
判断元素是否在集合中**SISMEMBER key member**
获取集合中的元素个数**SCARD key**
从集合中随机弹出一个元素,元素不删除**SRANDMEMBER key [数字]** (数字表示弹出几个)
从集合中随机弹出一个元素,出一个删一个**SPOP key[数字]**(数字表示弹出几个)
image.png

应用场景
1、微信抽奖小程序:
image.png
用户点击立即参与抽奖,添加用户id 执行**SADD key 用户id**
抽取两个三等奖号码
image.png
2、微信朋友圈点赞
image.png
3、微博好与关注社交关系(集合运算)
4、QQ内推可能认识的人(差集)

4、哈希 Hash

hash 是一个键值对集合,是一个string类型的field和value的映射表,类似Java里面的Map>。

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

常用命令
**HSET key field value** 一次设置一个字段值
image.png
**HGET key field** 一次获取一个字段值
image.png
**HMSET key field value [field value ..]** 一次设置多个字段值
image.png
**HMGET key field [field ...]** 一次获取多个字段值
**hgetall key** 获取所有字段值
image.png
**hlen** 获取某个key内的全部数量
**hdel** 删除一个key

应用场景
中小厂的购物车实线可以使用hash来实现
image.png
image.png
image.png
image.png

5、有序集合 Zset

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

常用命令
向有序集合中加入一个元素和该元素的分数ZADD key score member [score member ...]
按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素 ZRANGE key start stop [WITHSCORES]
获取元素的分数ZSCORE key member
删除元素ZREM key member [member ...]
获取指定分数范围的元素ZRANGEBYSCORE key min max[WITHSCORES][LIMIT offset count]
增加某个元素的分数ZINCRBY key increment member
获取集合中元素的数量ZCARD key
获得指定分数范围内的元素个ZCOUNT key min max
按照排名范围除元素ZREMRANGEBYRANK key start stop
获取元素从小到大的排名 ZRANK key member
获取元素从大到小的排名ZREVRANK key member

应用场景
根据商品销售对商品进行排序显示
排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
抖音热搜
前十排名

既然你知道有序集合Zset是用跳跃表来实现的,你给我说说为什么用跳跃表?怎么实现?

Zse有序集合内部存储的东西其实是一个元素加一个分值;对于Zse有序集合的底层实现,可以用数组、平衡树、链表等。但数组不便元素的插入 、删除;链表插入简单但查询需要遍历所有元素效率低;平衡树或红黑树虽然效率高但结构复杂;
而Redis采用的是跳跃表,跳跃表效率堪比红黑树,实现远比红黑树简单。image.png
如上图,我们要查询元素为55的结点,必须从头结点,循环遍历到最后一个节点,不算-INF(负无穷)一共查询8次。那么用什么办法能够用更少的次数访问55呢?最直观的,当然是新开辟一条捷径去访问55。
image.png
如上图,我们要查询元素为55的结点,只需要在L2层查找4次即可。
在这个结构中,查询结点为46的元素将耗费最多的查询次数5次。即先在L2查询46,查询4次后找到元素55,因为链表是有序的,46一定在55的左边,所以L2层没有元素46。然后我们退回到元素37,到它的下一层即L1层继续搜索46。非常幸运,我们只需要再查询1次就能找到46。这样一共耗费5次查询。
那么,如何才能更快的搜寻55呢?有了上面的经验,我们就很容易想到,再开辟一条捷径。
image.png
如上图,我们搜索55只需要2次查找即可。这个结构中,查询元素46仍然是最耗时的,需要查询5次。即首先在L3层查找2次,然后在L2层查找2次,最后在L1层查找1次,共5次。很显然,这种思想和2分非常相似,那么我们最后的结构图就应该如下图。
image.png
我们可以看到,最耗时的访问46需要6次查询。即L4访问55,L3访问21、55,L2访问37、55,L1访问46。我们直觉上认为,这样的结构会让查询有序链表的某个元素更快。

简单来说,所谓跳表优化就是在有序链表的情况下,每相邻几个(两个)元素间隔提取出一个冗余节点作为索引节点,构造出新的一层跟原来链表有关的链表;
再简单点来说的话,跳表就是将有序链表改造为支持近似“折半查找”算法,可以进行快速的插入、删除、查找操作。

除了传统的五大数据类型你还知道哪些Redis的数据类型?

Bigmaps

现代的计算机都是用二进制(位)作为信息的基础单位,1个字节等于8位; 例如虽然“abc”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示,“abc”分别对应的ASCII 码分别是97、98、99,对应的二进制分别是01100001、01100010和01100011,如下图。
image.png
如果我们能合理地使用这些操作位就能够有效地提高内存使用率和开发效率,而Redis中提供了Bitmaps这个“数据类型”可以实现对位的操作,但实际上Bitmaps本身不是一种数据类型,而是字符串( key-value ) ,且可以对字符串的位进行操作
Bitmaps单独提供了一套命令,所以在 Redis 中使用Bitmaps和使用字符串的方法不太相同。可以把 Bitmaps想象成一个以为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps 中叫做偏移量
image.png

常用命令

1、setbit<key><offset><value> 设置Bitmaps中某个偏移量的值(0或1);offset表示偏移量,从0开始;
使用实例
现在想要统计每个独立用户是否访问过某个网站,可以将数据存放在Bitmaps 中,将访问的用户记做1,没有访问的用户记做0,用偏移量(下标)表示用户的id。
设置键的第offset个位的值(从0算起),假设现在有20个用户,只有userid=1 ,6,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图,访问过的用户其id对应的值为1;
image.png
上图效果对应的命令如下所示
image.png
当然Bitmaps 类型也有它的缺点,比如在第一次初始化 Bitmaps 时,假如偏移量非常大(不是从0或1开始,而是从十万百万之类的开始),那么整个初始化过程执行会比较慢,可能会造成 Redis的阻塞。
而且很多应用的用户id以一个指定数字(例如10000 )开头,如果直接将用户id和Bitmaps 的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit操作时将用户id减去这个指定数字。

getbit<key><offset>获取Bitmaps中第offset个偏移量的值(从0开始算)
示例
image.png
bitcount 统计字符串被设置为1的 bit数
一般情况下,给定的整个字符串都会被进行计数,而通过指定额外的 start或end参数,可以让计数只在特定的位上进行。start和end参数的设置,都可以使用负数值∶比如-1表示最后一个位,而-2表示倒数第二个位,start、end是指bit组的字节的下标数,二者皆包含。
示例
image.png

HyperLogLog

在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV( PageView页面访问量),可以使用Redis的incr、incrby指令轻松实现。
但像UV ( UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
( 1)数据存储在MySQL表中,使用distinct count计算不重复个数。
(2)使用Redis提供的 hash、set、bitmaps等数据结构来处理;
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
Redis推出了HyperLogLog,是用来做基数统计的算法,能够降低一定的精度来平衡存储空间;
它的优点是在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。在Redis里面,每个 HyperLogLog键只需要花费12 KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog不能像集合那样,返回输入的各个元素。

GEO

GEO ,Geographic,地理信息的缩写。该类型主要用于存储地理位置信息。redis 基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash 等常见操作。
geoadd:添加地理位置的坐标
geopos:获取地理位置的坐标
geodist:计算两个位置之间的距离
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
georadiusbymember:根据存储在位置集合里面的某个地点获取指定范围内的地理位置集合
geohash:返回一个或多个位置对象的geohash值

主要是用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。geo的底层实现是zset。

缓存淘汰策略

  • 生产上你们你们的redis内存设置多少?
  • 如何配置、修改redis的内存大小
  • 如果内存满了你怎么办?
  • redis清理内存的方式?定期删除和惰性删除了解过吗
  • redis缓存淘汰策略
  • redis的LRU了解过吗?可否手写一个LRU算法

Redis默认内存多少?在哪里查看?如何设置修改?Redis内存满了怎么办?

如何查看Redis最大占用内存

在配置文件redis.conf中有一个maxmemory参数设置的就是redis的最大内存值,maxmemory是bytes字节类型,注意转换;

redis默认内存多少可以用?

这里注意一个细节,如果不设置maxmemory最大内存大小或者设置最大内存大小为0的话,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB。

一般生产上你如何配置?

一般推荐Redis设置内存为最大物理内存的四分之三。

如何修改redis内存设置

一种方法是通过修改配置文件redis.conf的maxmemory参数,如:maxmemory 104857600
另一宗方法是通过命令修改
config set maxmemory 1024
config get maxmemory

什么命令查看redis内存使用情况?

info memory

redis打满内存OOM

真要打满了会怎么样?如果Redis内存使用超出了设置的最大值会怎样?
会报一个OOM错误;所以我们尽量不要让它出现内存打满的情况;这就有了内存淘汰策略;image.png

缓存过期的key删除策略

有时候Redis的Key过期了为什么内存没有释放?如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢?

Key过期了内存没有释放的原因有很多,其中有一种情况是,你设置key—value以及对应的过期时间后,在过期之前你修改这个key对应的value值但是忘记去设置它的过期时间,这个时候这个key的过期时间就变成了永不过期了;所以当我们去修改一个key的value时一定要去确定这个key是否有过期时间,在修改的时候带上过期时间;
还有一种情况跟Redis的key删除策略有关,Redis对于过期key的处理有惰性删除和定时删除两种策略

删除策略

1、惰性删除︰当读/写一个已经过期的key时,会触发惰性删除策略,判断key是否过期,如果过期了直接删除掉这个key。
如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏 – 无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息。
2、定时删除:
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。
由于惰性删除策略无法保证冷数据(很久都不会用的数据)被及时删掉,所以Redis会定期(默认每100ms)主动淘汰一批已过期的key,这里的一批只是部分过期key,所以可能会出现部分key已经过期但还没有被清理掉的情况,导致内存并没有被释放。
image.png

Redis Key没设置过期时间为什么被Redis主动删除了

这个问题涉及到redis的内存淘汰策略,当Redis已用内存超过maxmemory限定时,触发主动清理策略
主动清理策略在Redis 4.0之前一共实现了6种内存淘汰策略,在4.0之后,又增加了2种策略,总共8种:a)针对设置了过期时间的key做处理
image.png
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结
Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis淘汰Key的算法LRU与LFU区别(跟银行家算法有点关系?)

https://www.bilibili.com/video/BV1Hy4y1B78T?p=65&t=22.9
image.png

Redis的过期键的删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理的策略。
过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量
  • 惰性过期:只有当访问某个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
    • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

删除Key的命令会阻塞Redis吗

关于删除一个key的命令是否会阻塞redis这跟要删除的数据类型有关,
如果删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为0(M),M为以上数据结构内部的元素数量;当集合元素内部的数据比较多的时候,消耗的时间就比较多了,就有可能造成阻塞;
如果删除的key是String类型的数据,虽然时间复杂度为O(1);但是当key对应的value很大的时候要删除的时间也会变长,那也有可能造成阻塞;

Redis命令意然有死循环阻塞Bug

有一条randomkey命令,作用是从当前数据库中随机返回( 不删除 )一个key ;正是这条命令可能会导致死循环这个问题;
Redis对于过期Key的清理策略是定时删除与惰性删除两种方式结合来做的,而RANDOMKEY命令在随机拿出一个 key后,首先会先检查这个key是否已过期,如果该key已经过期,那么Redis 会删除它,这个过程就是惰性删除。但清理完了还不能结束,Redis还要找出一个没过期的 key,返回给客户端。
此时,Redis则会继续随机拿出一个key,然后再判断它是否过期,直到找出一个没过期的key返回给客户端。
这里就有一个问题了,如果此时Redis 中,有大量key已经过期,但还未来得及被清理掉,那这个循环就会持续很久才能结束,而且,这个耗时都花费在了清理过期key以及寻找不过期key 上,导致的结果就是,RANDOMKEY执行耗时变长,影响Redis 性能。
以上流程,其实是在master主机 上执行的情况。
如果在slave 上执行RANDOMEKY命令,那么问题会更严重。
slave自己是不会清理过期key当一个key要过期时,master 会先清理删除它,之后master向slave发送一个DEL命令,告知slave 也删除这个key,以此达到主从库的数据一致性。
假设Redis 中存在大量已过期还未被清理的 key,那在 slave 上执行RANDOMKEY时,就会发生以下问题:
1、slave随机取出一个 key,判断是否已过期。
2、key已过期,但slave不会删除它,而是继续随机寻找不过期的key。
3、由于大量key都已过期,每次随机取到的key都是过期的数据,那 slave就会寻找不到符合条件的key,此时就会陷入死循环。
也就是说,在slave 上执行RANDOMKEY,有可能会造成整个Redis 实例卡死。
这其实是Redis的一个Bug,这个Bug一直持续到5.0才被修复.修复的解决方案就是在slave中最多找一定次数,无论是否找到都退出循环;

持久化

什么是持久化?为什么需要持久化?
Redis对数据的操作都是基于内存的,当遇到了进程退出、服务器宕机等意外情况,如果没有持久化机制,那么Redis中的数据将会丢失无法恢复。而有了持久化机制,Redis在下次重启时可以利用之前持久化的文件进行数据恢复。
Redis支持的两种持久化机制:

  1. RDB:把当前数据生成快照保存在硬盘上。
  2. AOF:记录每次对数据的操作到硬盘上。

RDB持久化

什么是RBD持久化?它的持久化机制是什么?

RDB(Redis DataBase)持久化是在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
在RDB持久化机制下,Redis会单独创建一个(fork)子进程来进行持久化,先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件(dump.rdb文件)。
整个过程中,主进程是不进行任何有关持久化IO操作的,这就确保了极高的性能 。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
RDB的缺点是最后一次持久化后的数据可能丢失。(在没到持久化的时间间隔里服务器挂掉)

这个Fork子进程是什么?

| Fork子进程是复制一个与当前Redis主进程一样的进程,即新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,并作为原进程的子进程。
l 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但该子进程在此后多为exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”。
l 一般情况父进程和子进程会共用同一段物理内存只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

这个持久化文件一般存放在哪里?

(配置该文件的位置这个内容在配置文件中dir ./,表示默认情况下会根据启动的位置为准,产生dump.rdb文件的位置,最好配置一下)

什么时候生成fork子进程,或者什么时候出发rdb持久化机制?

1.shutdown时,如果没有开启aof,就会触发。
2.根据配置文件中默认的快照配置
save 900 1 (定时器900秒,有一个更改(key操作),会触发一次持久化过程)
save 300 100
save 60 10000 (优化方案,将300、60的删掉;集群环境下,rdb是删不了的)
3.执行save(主进程执行,会阻塞),bgsave(fork的进程,后台异步执行快照)
4.执行flushall,把内存数据清空了,但里面是空的,无意义。(把磁盘的数据也清空,否则会丢失数据)

RDB 持久化流程

RDB持久化可以手动触发,也可以自动触发

手动触发的情况下

save命令
执行save命令会手动触发RDB持久化,但是save命令会阻塞Redis服务,直到RDB持久化完成。当Redis服务储存大量数据时,会造成较长时间的阻塞,不建议使用。
bgsave命令
执行bgsave命令也可以手动触发RDB持久化,和save命令不同是:Redis服务一般不会阻塞。
Redis进程会执行fork操作创建子进程,RDB持久化由子进程负责,不会阻塞Redis服务进程。Redis服务的阻塞只发生在fork阶段,一般情况时间很短。Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。

bgsave命令的具体流程如下图:
image.png
1、执行bgsave命令,Redis进程先判断当前是否存在正在执行的RDB或AOF子线程,如果存在就是直接结束。2、Redis进程执行fork操作创建子线程,在fork操作的过程中Redis进程会被阻塞。
3、Redis进程fork完成后,bgsave命令就结束了,自此Redis进程不会被阻塞,可以响应其他命令。
4、子进程根据Redis进程的内存生成快照文件,并替换原有的RDB文件。
5、子进程通过信号量通知Redis进程已完成。

自动触发的情况下

除了执行以上命令手动触发以外,Redis内部可以自动触发RDB持久化。自动触发的RDB持久化都是采用bgsave的方式,流程差不多,减少Redis进程的阻塞。那么,在什么场景下会自动触发呢?
1、在配置文件中设置了save的相关配置,如sava m n,它表示在m秒内数据被修改过n次时,自动触发bgsave操作。
2、当从节点做全量复制时,主节点会自动执行bgsave操作,并且把生成的RDB文件发送给从节点。
3、执行debug reload命令时,也会自动触发bgsave操作。
4、执行shutdown命令时,如果没有开启AOF持久化也会自动触发bgsave操作。

RDB优缺点

RDB优点RDB文件是一个紧凑的二进制压缩文件,是Redis在某个时间点的全部数据快照。所以使用RDB恢复数据的速度远远比AOF的快,非常适合备份、全量复制、灾难恢复等场景。
RDB缺点每次进行bgsave操作都要执行fork操作创建子经常,属于重量级操作,频繁执行成本过高,所以无法做到实时持久化,或者秒级持久化。
另外,由于Redis版本的不断迭代,存在不同格式的RDB版本,有可能出现低版本的RDB格式无法兼容高版本RDB文件的问题。

AOF持久化

AOF持久化机制以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据。换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

AOF 持久化流程

(1)客户端的请求是写命令则会被append追加到AOF缓冲区内;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
image.png

文件同步策略(也叫AOF同步频率设置)

AOF持久化流程中的文件同步有以下几个策略:

  • always:每次写入缓存区都要同步到AOF日志文件中,硬盘的操作比较慢,限制了Redis高并发,不建议配置。
  • no:每次写入缓存区后不进行同步,同步到AOF文件的操作由操作系统负责。每次同步AOF文件的周期不可控,而且增大了每次同步的硬盘的数据量。
  • eversec:每次写入缓存区后,由专门的线程每秒钟同步一次,做到了兼顾性能和数据安全。是建议的同步策略,也是默认的策略缺点是若果宕机了本秒的数据可能会丢失。


触发文件重写

AOF持久化流程中的文件重写可以手动触发,也可以自动触发。

  • 手动触发:使用bgrewriteaof命令。
  • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage配置确定自动触发的时机。auto-aof-rewrite-min-size表示运行AOF重写时文件大小的最小值,默认为64MB;auto-aof-rewrite-percentage表示当前AOF文件大小和上一次重写后AOF文件大小的比值的最小值,默认为100。只用前两者同时超过时才会自动触发文件重写。

Rewrite重写压缩

1、是什么?

AOF采用文件追加方式,文件会越来越大。为避免出现文件过大的情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集(将多条命令压缩成一条命令,但效果是一样的)。
可以使用命令bgrewriteaof。

2、重写原理,如何实现重写?

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

3、触发机制,何时重写
  • Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到设置值的100%时开始重写(通俗来说是文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写? 100MB
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

**4、重写流程(用临时复制的文件替换旧文件)

(1)bgrewriteaof命令触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
image.png

总结

  • RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
  • AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.
  • Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
  • 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
  • 同时开启两种持久化方式在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。

AOF和RDB同时开启,redis听谁的?

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失???)
因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。

如果对性能要求较高,在Master最好不要做持久化,可以在某个Slave开启AOF备份数据,策略设置为每秒同步一次即可。|

主从、哨兵以及集群

主从复制模式

什么是主从复制模式?

主从复制是指将一台Redis服务器的数据自动更新复制到其他的Redis服务器;
虽然通过持久化功能,Redis保证了即使在单台服务器重启的情况下也不会丢失(或少量丢失)数据,但是由于数据是存储在一台服务器上的,如果这台服务器出现故障,比如硬盘坏了,那也会导致数据丢失。
所以为了避免单点故障,我们需要将数据复制多份部署在多台不同的服务器上,即使有一台服务器出现故障其他服务器依然可以继续提供服务。
这就要求当一台服务器上的数据更新后,自动将更新的数据同步到其他服务器上,这时候就用到了Redis的主从复制。
Redis提供了复制(replication)功能来自动实现多台redis服务器的数据同步 。 我们可以通过部署多台redis,并在配置文件中指定这几台redis之间的主从关系主机负责写入数据,同时把写入的数据实时同步从机器,这种模式叫做主从复制,即master/slave,并且redis默认master用于写,slave用于读,向slave写数据会导致错误。
Redis的主从复制是异步复制的,异步分为两个方面,一个是master服务器在将数据同步到slave时是异步的,因此master服务器在这里仍然可以接收其他请求,一个是slave在接收同步数据也是异步的。

使用主从复制模式的好处

  • 可以保存Redis数据副本,容灾快速恢复;当我们只是通过RDB或AOF把Redis的内存数据持久化毕竟只是在本地,并不能保证绝对的安全,而通过将数据同步slave服务器上,可以保留多一个数据备份,更好地保证数据的安全。
  • 读写分离,性能扩展;在配置了主从复制之后,如果master服务器的读写压力太大,可以进行读写分离,客户端向master服务器写入数据,在读数据时,则访问slave服务器,从而减轻master服务器的访问压力。

如何搭建主从模式

主从模式常见问题

一主二仆的情况下

在一个主服务器两个从服务器的模式中,倘若某个从服务器挂掉,然后在这个从服务器挂掉的时候,主服务器在这个期间又写入了一些数据;在这个期间写入的数据当然能够被没有挂掉的从服务器读取到。
但是对于刚才挂掉的从服务器:
第一它重启之后并不能直接作为之前主服务器的从机了,这个时候它自己又变回主机了,也就是说重启之后它不能自动变回从服务器。需要自己重新执行命令才能变回从服务器。
第二,就算在挂掉期间主服务器又写入了数据,在重连之后还是能读取到挂掉期间写入的数据;也就是说在重新连的时候会从头复制一遍主服务器中的数据。

在一个主服务器两个从服务器的模式中,倘若主服务器挂掉,它的从服务器还是它的从服务器,从服务器也知道它的主服务器挂掉了。所以在主服务器重启之后它仍然是主服务器

薪火相传

上一个Slave从机可以是下一个slave从机的Master主机,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个从机的master, 可以有效减轻master的写压力,去中心化降低风险。
有点类似管理制度,一个大老板无法直接管理一千个人,所以就有了经理,经理下面又有小组长…..
这个时候,主机的从机数量只算直接跟它相连的从机(我附庸的附庸不是我的附庸)。
风险是一旦某个slave从机宕机,在它链条后面的slave都没法备份数据了。主机挂了,从机还是从机,只是无法写数据了 。
同样是用 slaveof 命令,只不过这个IP以及端口是某个从服务器的(毕竟你是从服务器的从服务器)。

反客为主

当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。用到的命令是slaveof no one 将从机变为主机,需要自己手动执行该命令。

  1. slaveof no one

哨兵模式

简单来说,哨兵模式是主从复制中反客为主的自动版,能够在后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库,注意这里在新的主机被选举出来后,原来的主机再重启就变成新主机的从机;
image.png

哨兵模式缺点以及集群框架的改进

image.png
在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave选举出来作为master;
哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的过程中是需要一定的时间的,所以会存在访问瞬断的情况;
而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,毕竟单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率;
image.png
所以在Redis3.0版本之后推出了一个集群架构,redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。
首先是数据存储方面,假设需要存储100g的数据,在哨兵模式的情况下,单机节点存储100g的数据性能是非常差的,数据恢复起来是很慢的;而如果是在集群架构下去存储100g数据的话,它并不会将这100g的数据全部放在一个主机中,而是进行分片存储(假设如上图所示有三个主机,则每个主机分摊这100g数据);
而且这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点);
而且在这种可水平扩展的redis集群下,性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单;
同时对于哨兵模式中存在的瞬断问题,在集群模式下受到的影响也会变小;
Redis集群不需要sentinel哨兵,也能完成节点移除和故障转移的功能。

Redis集群数据hash分片算法是怎么回事

Redis Cluster将集群中所有的数据划分为16384个slots(槽位),每个节点负责其中一部分槽位,即槽位的数据信息存储于集群中的每个节点内。
基于这个前提,当使用Redis Cluster的客户端来连接集群时,它会得到一份集群的槽位配置信息并将其缓存在客户端本地中。这样客户端要查找某个key时,就可以根据槽位定位算法定位到目标节点。
Cluster默认会对 key值使用crc16算法进行hash计算得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位。
HASH_SLOT = CRC16(key) mod 16384
根据槽位值和Redis节点的对应关系就可以定位到key具体是落在哪个Redis节点上的。

Redis主从、哨兵、集群架构优缺点比较

Redis缓存相关

有遇到过缓存穿透、缓存击穿、缓存雪崩吗?
数据库一般都是架构设计的瓶颈,我们应该尽量让有效的请求到达数据库,即使这样做会让前置环节的成本以及复杂度增加;
在大多数互联网应用中,缓存的使用方式如下图所示:
image.png
也就是业务系统跟实际存储数据的数据库之间有一层缓存机制。当业务系统发起某一个查询请求时,首先判断缓存中是否有该数据;如果缓存中存在,则直接返回数据;如果缓存中不存在,则再查询数据库,然后返回数据。

缓存穿透(不存在的数据)

是什么?

当业务系统发起查询时,若果业务系统要查询的数据根本就不存在!则按照上述缓存使用的流程,首先会前往缓存中查询,由于缓存中不存在,然后再前往数据库中查询。又因为该数据压根就不存在,所以数据库也返回空。这就是缓存穿透。
通说来说,业务系统访问压根就不存在的数据,就称为缓存穿透。
如果存在海量请求去查询压根就不存在的数据(既然不存在,缓存中就不会有,就会跳过缓存),这些海量的请求最后都会落到数据库中,造成数据库压力剧增,可能会导致系统崩溃(要知道,目前业务系统中最脆弱的就是IO,稍微来点压力它就会崩溃,所以我们要想尽种种办法保护它)。

产生原因

发生缓存穿透的原因有很多,一般为如下两种:

  1. 恶意攻击,故意营造大量不存在的数据请求我们的服务,由于缓存中并不存在这些数据,因此海量请求均落在数据库中,从而可能会导致数据库崩溃。
  2. 代码逻辑错误。这是程序员的锅,没啥好讲的,开发中一定要避免!


解决方案

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的(在缓存中不存在时查询数据库),并且出于容错考虑,而从存储层查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

解决方案:
(1) 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟(防止空值key占用过多内存)。存储空值还会出现一个问题是缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5 分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
(2) 设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。 当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在。若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。若存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。
(4)进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。

怎么解决?

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决方案:
(1) 对空值进行缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟(防止空值key占用过多内存)。
存储空值还会出现一个问题是缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5 分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象
(2) 设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在
若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。
若存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。
(4)进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。

缓存击穿(热点数据集中失效或者说从来没有被缓存过的数据被大量请求)

什么是热点数据集中失效?

一般都会给缓存设定一个失效时间,过了失效时间后,该数据库会被缓存直接删除,从而一定程度上保证数据的实时性。
但是,对于一些请求量极高的热点数据而言,一旦过了有效时间,此刻将会有大量请求落在数据库上,从而可能会导致数据库崩溃。
如果某一个热点数据失效,那么当再次有该数据的查询请求[req-1]时就会前往数据库查询;但是,从请求发往数据库,到该数据更新到缓存中的这段时间中由于缓存中仍然没有该数据,因此这段时间内到达的查询请求都会落到数据库上,这将会对数据库造成巨大的压力。
此外,当这些请求查询完成后,都会重复更新缓存。

解决方案

分布式互斥锁

此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
image.png

当第一个数据库查询请求发起后,就将缓存中该数据上锁;此时到达缓存的其他查询请求将无法查询该字段,从而被阻塞等待;当第一个请求完成数据库查询,并将数据更新值缓存后,释放锁;此时其他被阻塞的查询请求将可以直接从缓存中查到该数据。

当某一个热点数据失效后,只有第一个数据库查询请求发往数据库,其余所有的查询请求均被阻塞,从而保护了数据库。
但是,由于采用了互斥锁,其他请求将会阻塞等待,此时系统的吞吐量将会下降。这需要结合实际的业务考虑是否允许这么做。
互斥锁可以避免某一个热点数据失效导致数据库崩溃的问题,而在实际业务中,往往会存在一批热点数据同时失效的场景。那么,对于这种场景该如何防止数据库过载呢?
设置不同的失效时间
当我们向缓存中存储这些数据的时候,可以将他们的缓存失效时间错开。这样能够避免同时失效。如:在一个基础时间上加/减一个随机数,从而将这些缓存的失效时间错开

永远不过期设置

“永远不过期”包含两层意思: 从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。 从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存
从实战看,此方法有效杜绝了热点 key 产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。

缓存雪崩(大量请求不经过缓存而直接来到数据库)

缓存其实扮演了一个保护数据库的角色。它帮数据库抵挡大量的查询请求,从而避免脆弱的数据库受到伤害。
但如果缓存因某种原因发生了宕机或者说平时大量不同的数据都没有被查询放进缓存,突然间大量的请求来查询这些不同的数据,那么原本被缓存抵挡的海量查询请求就会像疯了一样涌向数据库;此时数据库如果抵挡不了这巨大的压力,它就会崩溃。
这就是缓存雪崩。

解决方案:

1、使用缓存集群,保证缓存高可用。 即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务 。
2、采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底
3、缓存的过期时间用随机值,尽量让不同的key的过期时间不同(例如:定时任务新建大批量key,设置的过期时间相同)
4、使用Hystrix。Hystrix是一款开源的“防雪崩工具”,它通过 熔断、降级、限流三个手段来降低雪崩发生后的损失。Hystrix就是一个Java类库,它采用命令模式,每一项服务处理请求都有各自的处理器。所有的请求都要经过各自的处理器。处理器会记录当前服务的请求失败率。一旦发现当前服务的请求失败率达到预设的值,Hystrix将会拒绝随后该服务的所有请求,直接返回一个预设的结果。这就是所谓的“熔断”。当经过一段时间后,Hystrix会放行该服务的一部分请求,再次统计它的请求失败率。如果此时请求失败率符合预设值,则完全打开限流开关;如果请求失败率仍然很高,那么继续拒绝该服务的所有请求。这就是所谓的“限流”。而Hystrix向那些被拒绝的请求直接返回一个预设结果,被称为“降级”。

一次线上事故,Redis主从切换导致了缓存雪崩,其原因可能是什么?

我们假设,slave的机器时钟比 master走得快很多。 从机时钟是13:00;主机是12:00;
此时,Redis master里设置了过期时间的key(假设是12:30过期),从 slave角度来看,可能会有很多在master里没过期的数据其实已经过期了(从机已经13:00了)。
如果此时操作主从切换,把slave提升为新的master。
它成为master后,就会开始大量清理过期key,此时就会导致以下结果:
1. master大量清理过期key,主线程可能会发生阻塞,无法及时处理客户端请求。
2.Redis中突然数据大量过期,引发缓存雪崩。
当master与slave机器时钟严重不一致时,对业务的影响非常大。所以,我们一定要保证主从库的机器时钟—致性,避免发生这些问题。

Redis事务

Redis高并发

分布式锁的演化(图)

引入超卖问题

image.png
引言,一段Java代码程序,获取从Mysql中查询到并提前放入Redis中的产品库存数量的缓存数据,也就是说从缓存中获取库存,然后判断库存是否大于0:
大于0则库存数减一并将修改后的库存数放回Redis缓存中;
不大于0的话,库存扣减失败;
很显然,这段代码是存在线程安全问题的,也就是说可能会存在超卖问题;比如同一时间有好几个请求都执行了获取Redis缓存的代码,那么这几个请求获取到的Redis中库存信息都是一样的,比如都获取到200,那么这这个请求在减库存的时候都是在这个200数量的基础上进行扣减的;这就意味着两百只减了一次,商品却卖了好几次…..,换句话说,三个请求来减库,应该从200减到197,而你现在却是从200减到199….;这不就超卖了吗;

为了解决这个问题,很多人都是直接给这段代码加上一个synchronized锁或者ReentrantLock可重入锁(这两个锁是有区别的);这样确实可以保证线程安全,解决超卖问题;但是也只是在单机环境下解决的超卖问题,因为你这个synchronized锁或者可重入锁是一个JVM锁,而我们的系统肯定不是运行在单机环境下,而是运行在跨JVM环境下的;这个JVM锁是无法跨多个JVM进行加锁的;也就是说在高并发(非单机)的环境下,只是在代码上加一个synchronized锁或者可重入锁是无法解决超卖问题的;

分布式锁的解决演化

在分布式集群架构上这类资源争抢问题可能会借助到分布式锁来解决,而若果你的缓存使用到了Redis,就可以借助Redis的某些API来实现分布式锁;

setnx key value命令

首先,我们来看看Redis中的setnx key value这条命令,该命令的效果是当且仅当要设置的key不存在时才会设置该key对应的value值,若是该key已经存在则不做任何改变;
反映到Java代码中,setnx key value这条命令对应的方法就是setIfAbsent这个API,修改代码如下;
image.png
将之前的代码做出如上图所示的改写,上面存在的分布式超卖问题就可以解决了;当多个请求同时进入该方法试图扣减库存时,由于所有的请求最终都会来到同一个Redis缓存中设置这个key,所以第一个请求尝试扣减库存时,由于该key暂时不存在,则成功将库存扣减并将key设置到缓存中;当第二个以及后面的请求尝试调用该方法时,发现缓存中已经存在该key,所以不做任何改变,返回一个false;也就有效得避免了超卖问题;

过期时间

当然目前的修改只是最最基础的一个版本;当前线程挂掉了,执行不到finally代码块,就会存在问题;当然可以给key设置一个过期时间,这样就算某个机器没执行finally代码块之前挂掉了,其他机器也能够访问处理;

原子操作

但是这样子依然不能彻底解决,那就是机器在执行设置key之后,设置过期时间之前挂掉了,仍然会有问题。换句话说,我们得让设置key以及设置过期时间这两个操作变成一个原子操作才行(所有高并发的场景都要考虑原子性的问题);
恰好这个setIfAbsent方法有一个重载方法,该方法就将这两个操作变成了原子操作,即设置了key又设置了过期时间;代码做如下修改;
image.png

锁过期导致删到了其他线程的锁

但是这样的情况下,依然会有问题;那就是某个请求执行的比较慢,在它执行完上图中红色框中的代码之后,由于种种原因超时了,在往下执行的过程中时间超过了key的过期时间,而它的逻辑代码还没走完;此时又有其他的请求可以执行红框中的代码,这就导致有多个请求可以同时执行安全代码,这又会造成超卖问题;
这还只是小问题,当第一个请求执行到方法末尾时,需要去删除key,但是这个key是请求2加的,请求1删除的时候意味着有可能请求2还没执行完的时候又有其他的请求进入安全代码,此时请求2又有可能做了请求1做的事(删除别人的锁),那就是删除正在执行安全代码的请求所加的key….如此反复,在极端情况下会导致分布式锁失效…..,在高并发的情况下可能会超卖上万件商品….亏死商家,直接破产;

而造成这种极端情况的本质原因就是自己加的锁被其他请求或其他原因删除掉了,所以要解决这个问题,我们要限制住,自己加的锁不能被别人解锁掉;
这样的话,我们可以给所有的请求加一个唯一的标识,将该标识作为key(锁)对应的value;也就是说,谁加了锁(设置了key),就将谁的身份(唯一标识)同时设置为key的value,同时在释放锁的时候判断一下,只有身份正确才能解锁成功!
image.png

极端锁过期导致删到了其他线程的锁——锁续命

但是真正高并发情况下的分布式锁并没有这么简单,此时的代码仍然存在问题,假设现在锁的过期时间是10s,当代码执行到最下面的if判断身份时已经过了9.9s,此时if判断返回为真,进行if内部后由于某些原因,如gc回收导致卡壳,在卡壳的过程中,key的过期时间已经到了;此时key分布式锁就失效了,但是由于卡壳此时当前请求并没有执行if中删除对应的key的逻辑代码;在高并发情况下,此时立马就有其他的请求进入安全代码,这个线程2又可以加锁成功,此时刚才卡壳的请求线程恢复了,再去执行delete方法删除key,那又会出现问题了,虽然这种情况发生的概率非常小,但是还是会发生;
image.png
所以归根结底,问题还是出现在这个过期时间上;而将过期时间变长也只是治标不治本;
(lua)
所以解决问题的一个方法是,在请求持有锁开始执行安全方法的同时开启一个分线程,该线程的任务是定时去检测当前请求在执行安全方法的过程中是否仍持有锁,即锁是否已经过期,若果过期了就重新给它重置过期时间
这就是锁续命问题;
其实造成这个问题的原因还是if判断不是原子操作的缘故,还有一种解决方案是使用Redis事务+重试(类似cas)来解决;这种方法有时间再详细说说;

redisson(lua)

像上面分布式锁的演进过程如果要自己写的话,肯定会出很多bug;所以我们可以使用别人已经封装好了的分布式锁,比如redisson就帮我们封装好了一个分布式锁;
代码修改如下:
image.png
image.png
image.png
更严谨的解锁代码
image.png
只需要简单的几行代码,就能实现跟上面分布式演进锁的效果一样,下图是它的流程图:
image.png
即线程1首先尝试获取锁,加锁成功之后开启一个线程定时检测是否还持有锁,不持有锁就重置锁过期时间;
线程2尝试获取锁的时候发现key已经存在,说明别的线程已经加锁,此时它会一直尝试加锁,直至获取锁成功;

Redisson底层原理

该Redisson分布式锁的底层原理大概跟上面演化的差不多;只不过它使用lua脚本解决原子性问题;redis底层可以保证lua脚本命令执行的原子性;

主从架构异步复制导致锁失效

虽然没有太大问题,但是这段代码在高并发场景的性能大大降低;而且上面的流程中也可以看出使用的是主从架构模式,且大多数公司使用Redis的时候都是搭建集群架构的方式,这就会存在问题;
image.png
我们知道redisson加锁底层其实也是相当于setnx 命令设置一个key-value;
假设存在这个场景,当我们给redis的主机master设置一个key的时候,redis会马上告诉客户端线程1加锁成功,线程1就开始执行逻辑代码,当redis主机正准备将数据同步给从机的时候,挂掉了;这时候从机就变成了主机,由于数据没有同步,这时候来了一个线程3尝试加锁,肯定是去访问新的主机,而两个线程都是去访问同一个商品加锁,发现此时的主机中没有该商品的加锁信息,也就能获取锁成功;这就造成了两个线程一起执行安全代码的局面;这就是非常经典的Redis主从架构分布式锁失效的问题;
在同样的场景下,如果是使用zookeeper的话,在同步给zookeeper”从机”息;而且在主机挂掉之后,zookeeper选举出来的新主机一定是保持了同步数据的从机作为新主机;
所以zookeeper跟redis在这个问题上的区别就是Redis是AP,zookeeper是CP;(CAP理论)**
redis中要解决这个锁失效的问题,其实可以使用RedLock(但是性能会下降,而且跟zookeeper原理差不多,还不如使用zookeeper呢);

性能下降问题

还有就是高并发下的性能下降问题,其实分布式锁从语义的角度来看是跟高并发相违背的,因为分布式锁其实是将并发执行的请求加锁并存串行化执行了;
为了将这个分布式锁进行性能上的优化,可以仿照ConcurrentMap的实现原理,搞一个分段锁来提高并发性能;

Redis看门狗

如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。

缓存

缓存数据库双写不一致性

什么是缓存数据库双写不一致性

image.png
线程1先将数据库中的某条记录进行写操作修改为10;正常的情况下是写进数据库后立马更新缓存,然后线程2也进行写操作将该条记录进行修改然后写入缓存;
然而如上图所示,当线程1将数据写入数据库后由于某些原因导致执行时间延长,还没有将缓存更新;此时线程2将数据库中的记录进行修改并更新缓存完成,这个时候线程1才将缓存进行更新;这就产生问题了,毕竟此时缓存中真正的数据应该是线程2更新后的stock=6而不是线程1更新的10;这就是经典的缓存数据库双写不一致性;
但是一般的互联网公司使用缓存的话并不是将数据写进数据库后就更新缓存中的值的,因为这样做每次都更新缓存,只有最后一次更新的缓存才是有效的;
所以互联网公司一般都是将数据写入数据库之后,将缓存删除掉;只有在查询缓存的时候才会从数据库中加载数据到缓存中;但是这样依然会存在问题,如下图所示;
image.png
假设有这种情况,线程1 将数据修改后写入数据库,然后删除缓存;线程3此时查询缓存发现是空的,就去查询数据库,得到stock=10,正常情况下我们查询完数据之后就会更新缓存,但是由于某些原因又造成线程3的卡顿,在线程3卡顿期间线程2又将数据修改stock=6,然后又删除掉缓存;这个时候线程3才将缓存进行更新,即缓存中的数据stock=10,但是实际上数据库中的数据stock是等于6的,又产生了数据不一致性;

如何解决双写不一致性

有一个解决方案是延迟双删,就是在线程2删除缓存中睡眠一会儿,等线程3更新缓存之后线程2就睡醒了,然后再次进行删除缓存;当然这种方案是不太靠谱的,虽然能在一定程度上解决双写一致性的问题,但也只是一定程度上解决而已;而且这样做也会让每个线程在更新数据到数据库中的时候都需要去睡眠一段时间,让所有的写请求都延迟一段时间,对整个系统的吞吐量造成一定的影响;所以第二次删除我们可以设计成异步删除,再起一个线程异步执行二次删除即可减少对吞吐量的影响;

还有一个方案是加锁;其实双写不一致性问题的本质原因是因为更新数据库,更新缓存等操作不是一个原子操作,要想更好的解决这个问题的话,可以将这些操作都当前一个原子操作,然后将每个线程的这些原子操作进行排队执行;排队执行的话,可以用分布式锁来实现,在某个线程执行这些操作之前加一个分布式锁,然后在执行完这些操作之后将锁解开;这样就可以让每个线程的这些原子操作排队执行了;将并发执行变成串行化,就百分百解决了缓存双写一致性的问题了;当然加仅仅是分布式锁这种解决方法会让高并发性能下降,所以得想办法去优化;
一种方法是使用读写锁,Redisson中也提供了基于Redis实现的读写锁,基本原理跟Java的读写锁差不多,都是读读并发,读写互斥;只不过redis中的读写锁是结合lua脚本实现的;
读写锁这种解决方法适合于读多写少的情况下;
虽然读写锁可以解决百分之八十的情景;但是如果公司的业务是读多写也多的情况下的话,再用读写锁来优化就不太行了,依然会存在性能上的下降问题;所以读多写也多的情况下一般都不会再用缓存了;