热衷学习,热衷生活!😄
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、简单的介绍一下Redis
简单的说Redis
就是一个使用C语言
开发的一个数据库,不过与传统数据库不同的是Redis
的数据是存在内存中的,它是内存数据库,所以读写速度非常快,所以Redis
被广泛应用于缓存方向。
另外,Redis
除了做缓存之外,也还经常用于做分布式锁,甚至是消息队列。
Redis
提供了多种数据类型来支持不同的业务场景。Redis
还支持事务、持久化、Lua脚本、多种集群方案。
二、Redis持久化机制
Redis
是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis
重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
实现:单独创建一个fork()
子进程,将当前父线程的数据库文件复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程就结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
RDB(Redis DataBase)
是Redis
默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot
快照存储,对应产生的数据文件为dump.rdb
,通过配置文件中的save
参数来定义快照的周期。
AOF(Append Only Field)
方式:Redis
会将每一个写命令都通过write
函数追加到文件最后,类似MySQL的binlog
,当重启Redis
会加载appendonly.aof
文件恢复数据。
三、缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级问题
缓存雪崩
缓存雪崩可以理解为:由于原有缓存失效,新缓存未到期间,而形成一些列连锁反应,而造成整个系统崩溃。
举个栗子:
我们设置缓存采用了相同的过期时间,在同一时刻出现大面积的缓存过期,然后所有的请求都去访问数据库了,对数据库CPU
和存在造成了巨大压力,严重的时候造成数据库宕机。
解决方法:
大多数系统设计者考虑用加锁或者队列的方式保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。
缓存穿透
缓存穿透是指用户查询数据库,在数据库没有,自然在缓存中也不会有,这样就导致用户在查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空,相当于两次无用的查询,这样请求就直接绕过缓存直接查询数据库,这也是经常说的缓存命中问题。
解决方法:
最常用的就是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap
中,不存的数据会被这个bitmap
拦截掉,从而避免了对底层存储系统的查询压力。
另外一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,不会超过五分钟,通过这个直接设置的默认值存放到缓存,这样子二次到缓存中获取就有值了。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样子就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
- 直接写个缓存页面,上线时手工操作下。
- 数据量不大,可以在项目启动时自动进行加载。
- 定时刷新缓存。
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的
业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数
据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过
来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,可以根据自己的应用场景来权衡。
缓存降级
当访问量剧增、服务出现问题或非核心服务影响到核心流程的性能时仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结
算)。
以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,
并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的
最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的是为了防止Redis
服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查
询,而是直接返回默认值给用户。
四、单线程的Redis为什么这么快
- 纯内存操作。
- 单线程操作,避免了频繁的上下文切换。
- 采用非阻塞I/O多路复用机制。
五、Redis的数据类型,以及每种数据类型的使用场景
string
string
数据结构是简单的key-value
类型,value
可以是字符串也可以是数字。
一般用于做一下复杂的计数功能,比如用户的访问次数、点赞功能等等。
list
list
即是链表,实现了一个双向链表,可以支持反向查找、遍历,更方便操作,但是带来了额外的内存开销。
常用于发布和订阅或者消息队列、慢查询。
hash
hash
类似JDK1.8
前的HashMap
,内存实现也差不过是数组+链表。特别适合用于储存对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。
常用于系统中对象数据的存储。
set
set
类似于Java
中的HashSet
,是一个无序且不重复的集合。可以基于 set 轻易实现交集、并集、差集的操作。
常用于需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。
sorted set
和set
相比,增加了一个权重参数score
,是的集合中的元素能够按照score
进行有序排列,还可以通过 score 的范围来获取元素的列表。
常用于需要对数据根据某个权重进行排序的场景。比如:各种排行榜、TOP N
。
bitmap
bitmap
存储的是连续的二进制数字(0和1),通过bitmap
,只需一个bit
位来表示某个元素对应的值或者状态,key
就是对应元素本身。
常用于需要保存状态信息(比如是否签到、是否登陆)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计。
六、Redis的过期策略以及内存淘汰机制
redis
采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除用一个定时器来负责监视key
,过期则自动删除。虽然内存及时释放,但是十分消耗CPU
资源。
在大量并发请求下,CPU
要将时间用在处理请求,而不是用来删除key
。
定期删除+惰性删除是怎么工作的呢?
定期删除,redis
默认每隔100ms
检查是否有过期的key
,有的话就删除。需要说明是redis
不是每隔100ms
将所有的key
检查一次,而是随机抽取进行检查,因此如果只采用定期删除策略会导致很多过期的key
没有删除,这个时候惰性删除就派上用场了,获取某个key
的时候,redis
会检查一下这个key
是否过期了,如果过期了就删除。
采用定期删除+惰性删除就没其他问题了吗?
不是的,如果定期删除没删除的key
,也没有请求去获取,这个时候惰性删除就不会生效,redis的内存会越来越高,这个时候就要用到内存淘汰机制了。
在redis.conf
中有一行配置:
maxmemory-policy volatile-lru
该配置就是配置内存淘汰策略,主要有以下策略:
volatile-re
:从已设置过期的数据集中挑选最近最少使用的数据淘汰。volatile-ttl
:从已设置过期的数据集中挑选将要过期的数据淘汰。volatile-random
:从已设置过期的数据集中随机选择数据淘汰。allkeys-lru
:从数据集中挑选最近最少使用的数据淘汰。allkeys-random
:从数据集中随机挑选数据淘汰。no-enviction
:禁止淘汰数据,新写入操作会报错。
ps:如果没有设置expire
的key
, 不满足先决条件(prerequisites
); 那么 volatile-lru
, volatile-random
和volatile-ttl
策略的行为, 和 noeviction(不删除)
基本上一致。
七、Redis为什么是单线程的
官方FAQ
表示,因为redis
是基于内存操作,CPU
不是redis
的瓶颈,Redis
的瓶颈最有可能是机器内存的大小或者网络宽带。既然单线程容易实现,而且CPU
不会成为瓶颈。那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)
Redis
利用队列技术将并发访问变为串行访问。
- 绝大部分请求是纯粹的内存操作。
- 采用单线程可以避免不必要的上下文切换和竞争条件。
Redis
采用了非阻塞I/O技术。
八、为什么Redis的操作是原子性的,怎么保证原子性的?
对Redis
而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis
的操作之所以是原子性的,是因为Redis
是单线程的。
Redis
本身提供的所有API
都是原子操作,Redis
中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性吗?
这个不一定,将get
和set
改成单命令操作可能其中一个命令成功,一个失败。可以使用Redis
事务或者使用Redis + Lua脚本
的方式实现。
九、Redis事务
Redis
事务功能是通过MULTI、EXEC、DISCARD、WATCH
四个原语实现的。
Redis
会将一个事务中的所有命令序列化,然后按顺序执行,并且不会被中途打断。
Redis
是不支持 roll back
的,因而不满足原子性的(而且不满足持久性)。
Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。
四个原语功能如下:
MULTI
:用于开始一个事务,它总是返回OK
。MULTI
命令执行完毕之后,客户端可以继续向服务器发送任意多条命令,这些命令不会被立即执行,而是被放到一个队列中,当EXEC
命令被执行时,所有队列中的命令才会被执行。EXEC
:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列,当操作被打断时返回空值。- 通过调用
DISCARD
命令,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。 WATCH
: 命令用于监听指定的键,当调用EXEC
命令执行事务时,如果一个被WATCH
命令监视的键被修改的话,整个事务都不会执行,直接返回失败。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。