Redis 总结
1.NoSQL
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
-
不遵循SQL标准
-
不支持ACID
-
远超于SQL的性能
2.Redis 简介
2.1 概述
-
几乎覆盖了 Memcached 的绝大部分功能
-
数据都在内存中,支持持久化,主要用作备份恢复
-
除了支持简单的 key-value 模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
-
一般是作为缓存数据库辅助持久化的数据库
2.2 优点
-
性能优异
-
采用单线程设计,避免上下文切换和竞争条件
-
完全基于内存
-
数据结构简单
-
使用多路 I/O 复用模型,非阻塞 IO
-
-
支持持久化数据
-
支持事务
-
支持多做数据结构
-
支持主从复制
-
高性能:将用户访问的数据存在缓存中
-
高并发:将数据库中部分数据移到缓存中
2.2.1 补充 - Redis 单线程 + 多路 IO 复用技术
Redis 的底层采用Nio中的多路IO复用的机制,能够非常好的支持这样的并发,从而保证线程安全问题。
Redis 单线程,也就是底层采用一个线程维护多个不同的客户端 IO 操作。但是 Nio 在不同的操作系统上实现的方式有所不同,在我们 windows 操作系统使用 select 实现轮训时间复杂度是为o(n),而且还存在空轮训的情况,效率非常低,其次是默认对我们轮训的数据有一定限制,所以支持上万的 tcp 连接是非常难。
所以在 linux 操作系统采用 epoll 实现事件驱动回调,不会存在空轮训的情况,只对活跃的 socket 连接实现主动回调这样在性能上有大大的提升,所以时间复杂度是为o(1)。
所以为什么 nginx、redis 都能够非常高支持高并发,最终都是 linux 中的 IO 多路复用机制 epoll,Redis 底层采用 nio epoll 实现。
2.3 缺点
- 受物理内存限制,不能作为海量数据高速读写
- 不具备自动容错和恢复功能,主从机的宕机都会导致部分请求的失败
- 主机宕机前,部分数据未同步从机,会引起数据不一致和数据丢失的问题
- 在线扩容比较难
2.4 应用场景
- 计数器
- 缓存(内容可以失效)
- 数据库缓存、会话缓存、全网页缓存(FPC)
- 查找表(内容不能失效)
- 消息队列(订阅/发布功能)
- 分布式锁
- redis支持的setnx实现分布式锁
- 官方推荐的RedLock分布式锁
2.5 数据类型
2.5.1 基本数据类型
-
字符串、整数、浮点数(String)
-
列表(List)
-
集合(Set)
-
散列表(Hash)
-
有序集合(ZSet)
2.5.2 特殊数据类型
- 地理位置(geospatial)
- HyperLogLog 是用来做基数统计的算法
- 位存储(bitmaps)
- 发布和订阅
2.6 持久化
- RDB
- AOF
2.6.1 RDB (redis database缩写快照)
按照一定的时间将内存数据以快照的形式保存到硬盘中,采用写时复制技术。
1.优点
-
只有一个 dump.rdb 文件,方便持久化
-
容灾性好,一个文件可以保存在安全磁盘
-
性能最大化,子进程完成数据备份,不影响主进程
-
数据集大时比AOF恢复快
-
性能比 AOF 好
2.缺点
数据安全性低,隔一段时间备份会有数据丢失的问题
2.6.2 AOF(append only file)
将redis执行的每次命令放到一个日志文件中记录;当开启两种方式备份时,优先使用AOF恢复数据。
1.优点
- 数据安全,可以做到记录备份每一条数据
- rewrite 模式会对过大的文件进行合并重写,删除其中的某些命令
- 比 RDB 方式更安全
2.缺点
AOF文件比RDB文件大,数据恢复启动时慢。
3.基本数据类型详解
指令查询:http://www.redis.cn/commands.html
3.1 Redis字符串(String)
3.1.1 概述
-
String是Redis最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value
-
String类型是二进制安全的。意味着 Redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象
-
String类型是Redis最基本的数据类型,一个 Redis 中字符串value最多可以是512M
3.1.2 数据结构
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList ,采用预分配冗余空间的方式来减少内存的频繁分配。
如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度。**当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M的空间。**需要注意的是字符串最大长度为 512M。
3.2 Redis 列表(List)
3.2.1 概述
单键多值,Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
3.2.2 数据结构
List的数据结构为快速链表 quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成 quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next。
Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
3.3 Redis 集合(Set)
3.3.1 概述
Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是list所不能提供的。
Redis 的 Set 是 string 类型的无序集合。它底层其实是一个 value 为 null 的 hash 表,所以添加,删除,查找的复杂度都是O(1)。
一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变
3.3.2 数据结构
Set数据结构是dict字典,字典是用哈希表实现的。
Java 中 HashSet 的内部实现使用的是 HashMap,只不过所有的 value 都指向同一个对象。Redis 的 set 结构也是一样,它的内部也使用 hash 结构,所有的 value 都指向同一个内部值。
3.4 Redis 哈希(Hash)
3.4.1 概述
Redis hash 是一个键值对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。类似Java里面的Map<String,Object>。
用户 ID 为查找的 key,存储的 value 用户对象包含姓名、年龄、生日等信息,如果用普通的 key/value 结构来存储。
hmset user:pox id 1 name pox sex nan
3.4.2 数据结构
Hash类型对应的数据结构是两种:ziplist(压缩列表)、hashtable(哈希表)。当 field-value 长度较短且个数较少时,使用 ziplist,否则使用 hashtable。
3.5 Redis 有序集合 Zset (sorted set)
3.5.1 概述
Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以重复。
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
3.5.2 数据结构
SortedSet(zset) 是 Redis 提供的一个非常特别的数据结构,一方面它等价于 Java 的数据结构 Map<String, Double>,可以给每一个元素 value 赋予一个权重 score,另一方面它又类似于 TreeSet,内部的元素会按照权重 score 进行排序,可以得到每个元素的名次,还可以通过 score 的范围来获取元素的列表。
zset底层使用了两个数据结构:
- hash,hash 的作用就是关联元素 value 和权重 score,保障元素 value 的唯一性,可以通过元素 value 找到相应的 score 值
- 跳跃表,跳跃表的目的在于给元素 value 排序,根据 score 的范围获取元素列表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pBI7qfs-1644166542653)(F:\StudyNotepad\img\image-20220206193525509.png)]
3.6 总结
基本数据类型的集合和 Java 的集合类很相似,List 可以对应 ArrayList、Set 可以对应 HashSet 底层都是 HashTable(HashMap), Java 中 HashSet 的 value 被指定为 PRESENT,在 Redis中相同也对 value 进行了指定、Hash 对应 Java 中的 HashMap、Zset 对应 TreeSet,可以通过编写排序规则对集合中的数据进行排序遍历输出。
4.特殊数据类型详解
4.1 Bitmaps
4.1.1 概述
现代计算机用二进制(位) 作为信息的基础单位,1个字节等于8位,例如“abc”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示,“abc”分别对应的 ASCII 码分别是97、 98、 99,对应的二进制分别是01100001、 01100010和01100011,如下图:
合理地使用操作位能够有效地提高内存使用率和开发效率。
Redis 提供了 Bitmaps 这个“数据类型”可以实现对位的操作:
- Bitmaps本身不是一种数据类型,实际上它就是字符串(key-value),但是它可以对字符串的位进行操。
- Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
4.2 HyperLogLog
4.2.1 概述
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用 Redis 的incr、incrby轻松实现。
但像 UV(UniqueVisitor,独立访客)、独立 IP 数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
- 数据存储在MySQL表中,使用distinct count计算不重复个数
- 使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否能够降低一定的精度来平衡存储空间? Redis 推出了 HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
补充:什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5个。 基数估计就是在误差可接受的范围内,快速计算基数。
4.3 Geospatial
4.3.1 概述
Redis 3.2 中增加了对 GEO(地理)类型的支持。GEO、Geographic、地理信息的缩写。该类型,就是元素的 2 维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
5.事务详解
-
单独的隔离操作
-
没有隔离级别的概念
-
不保证原子性
5.1 概述
Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set 机制实现事务的。
5.2 过程
- Multi:开启事务
- Exec:执行事务
- discard:取消事务
从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行。
组队的过程中可以通过 discard 来放弃组队。放弃事务执行。
5.3 事务的错误处理
-
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消(1)
-
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚(2)
5.4 WATCH key [key …]
在执行multi之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
6.持久化详解
6.1 RDB(Redis DataBase)
6.1.1 概述
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的 Snapshot 快照,它恢复时是将快照文件直接读到内存里。
6.1.2 过程
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
RDB的缺点是最后一次持久化后的数据可能丢失。
6.1.3 Fork
- Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
- 在 Linux 程序中,fork() 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了“写时复制技术”
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程
6.1.4 持久化的策略
在 Redis 的配置文件中可以设置持久化触发策略。
补充:命令 save VS bgsave
- save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。
- bgsave:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
6.1.5 优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间,文件紧凑
- 恢复速度快
6.1.6 缺点
-
Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
-
虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
-
在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外宕机的话,就会丢失最后一次快照后的所有修改
6.2 AOF(Append Only File)
6.2.1 概述
以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
6.2.2 过程
- 客户端的请求写命令会被 append 追加到 AOF 缓冲区内;
- AOF 缓冲区根据 AOF 持久化策略 [always,everysec,no] 将操作同步到磁盘的 AOF 文件中;
- AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量;
- Redis 服务重启时,会重新加载 AOF 文件中的写操作达到数据恢复的目的;
6.2.3 同步频率设置
-
appendfsync always
-
appendfsync everysec
-
appendfsync no
6.2.4 Rewrite 压缩
1.概述
AOF 采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制。
当 AOF 文件的大小超过所设定的阈值时(128M),Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。
2.重写原理
AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再 rename ),redis4.0 版本后的重写,是指把 rdb 的快照,以二级制的形式附在新的 aof 头部,作为已有的历史数据,替换掉原来的流水账操作。
3.触发机制
Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定 Redis 要满足一定条件才会进行重写。
- auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
- auto-aof-rewrite-min-size:设置重写的基准值,最小文件 64MB。达到这个值开始重写。
4.重写流程
-
bgrewriteaof 触发重写,判断是否当前有 bgsave 或 bgrewriteaof 在运行,如果有,则等待该命令结束后再继续执行
-
主进程 fork 出子进程执行重写操作,保证主进程不会阻塞
-
子进程遍历 redis 内存中数据到临时文件,客户端的写请求同时写入 aof_buf 缓冲区和 aof_rewrite_buf 重写缓冲区保证原 AOF 文件完整以及新 AOF 文件生成期间的新的数据修改动作不会丢失。
-
数据写入
-
子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息
-
主进程把 aof_rewrite_buf 中的数据写入到新的AOF文件。
-
-
使用新的 AO F文件覆盖旧的 AOF 文件,完成AOF重写
6.2.5 优势
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文本,通过操作AOF稳健,可以处理误操作
6.2.6 缺点
-
比起 RDB 占用更多的磁盘空间
-
恢复备份速度要慢
-
每次读写都同步的话,有一定的性能压力
-
存在个别 Bug,造成恢复不能
6.3 总结
- 官方推荐两个都启用
- 如果对数据不敏感,可以选单独用 RDB
- 不建议单独用 AOF,因为可能会出现 Bug
- 如果只是做纯内存缓存,可以都不用
1.官方建议:
-
RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储
-
AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以redis协议追加保存每次写的操作到文件末尾.
-
Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大
-
只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式
-
同时开启两种持久化方式
-
在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整
-
RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只使用 AOF 呢?
2.性能建议
因为RDB文件只用作后备用途,建议只在 Slave(从机)上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留save 900 1 这条规则。
如果使用 AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的 AOF文 件就可以了。
代价:
- 是带来了持续的IO
- AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的
只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M 太小了,可以设到 5G 以上。
默认超过原大小100%大小时重写可以改到适当的数值。
7.Redis 主从复制
7.1 概述
主机数据更新后根据配置和策略, 自动同步到备机的 master/slaver 机制,Master 以写为主,Slave 以读为主。
- 读写分离
- 容灾快速恢复
7.2 复制模式
7.2.1 薪火相传
上一个 Slave 可以是下一个 slave 的 Master,Slave 同样可以接收其他 slaves 的连接和同步请求,那么该slave 作为了链条中下一个的 master,可以有效减轻 master 的写压力,去中心化降低风险。
-
中途变更转向:会清除之前的数据,重新建立拷贝最新的
-
风险是一旦某个slave宕机,后面的slave都没法备份
-
主机挂了,从机还是从机,无法写数据了
7.3.2 反客为主
当一个 master 宕机后,后面的 slave 可以立刻升为 master,其后面的 slave 不用做任何修改。
7.3 复制原理
- Slave 启动成功连接到 master 后会发送一个同步命令
- Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后, master 将传送整个数据文件到 slave,以完成一次完全同步
- 全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步
- 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行
7.4 哨兵模式
7.4.1 概述
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
需要启动相关服务 执行 redis-sentinel /myredis/sentinel.conf
,主机恢复后,成为新主机的从机。
7.4.2 选举机制和过程
1.选举机制,优先度从上往下
-
优先级设置
-
偏移量大小
-
从机 runid 数值大小
8.Redis 在应用中的问题
8.0 Redis 用作缓存
通常 Redis 作为缓存使用,提高数据访问、高性能、高并发等,但数据主要存储在内存中,当内存不够时,会触发淘汰策略:
- noeviction:当内存使用达到阈值的时候,执行命令直接报错。(默认)
- allkeys-lru:在所有的key中,优先移除最近未使用的key。(推荐)
- volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
- allkeys-random:在所有的key中,随机移除某个key。
- volatile-random:在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
###8.1 缓存穿透
8.1.1 概述
key 对应的数据在数据源并不存在,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。
比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
8.1.2 解决方案
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决方案:
-
对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
-
设置可访问的名单(白名单):使用 bitmaps 类型定义一个可以访问的名单,名单 id 作为 bitmaps 的偏移量,每次访问和 bitmap 里面的 id 进行比较,如果访问 id 不在 bitmaps 里面,进行拦截,不允许访问
-
采用布隆过滤器:布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
将所有可能存在的数据 哈希 到一个足够大的 bitmaps 中,一个一定不存在的数据会被这个 bitmaps拦截掉,从而避免了对底层存储系统的查询压力(1) -
进行实时监控:当发现 Redis 的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
1.补充 - 布隆过滤器
推荐文章:https://developer.aliyun.com/article/773205
8.2 缓存击穿
8.2.1 概述
key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。
8.2.2 解决方案
key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决问题:
-
预先设置热门数据:在 redis 高峰访问之前,把一些热门数据提前存入到 redis 里面,加大这些热门数据 key 的时长**(推荐)**
-
实时调整:现场监控哪些数据热门,实时调整 key 的过期时长
-
使用锁:
-
就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
-
先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
-
当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
-
当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
8.3 缓存雪崩
8.3.1 概述
key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key。
8.3.2 解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕!
解决方案:
- 构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
- 使用锁或队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。
- 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际 key 的缓存。
- 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。(推荐)
9.分布式锁
9.1 概述
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 Java API 并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等)
- 基于 Nacos
每一种分布式锁解决方案都有各自的优缺点:
- 性能:redis最高
- 可靠性:Nacos最高
9.2 过程
- 多个客户端同时获取锁(setnx)
- 获取成功,执行业务逻辑 {从db获取数据,放入缓存},执行完成释放锁(del)
- 其他客户端等待重试
9.3 总结
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
- 加锁和解锁必须具有原子性