Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。
当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,
持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
1.1 RDB:
RDB是Redis默认的持久化方式。
按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,
对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。(快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
-
RDB持久化缺点
无法做到实时持久化,具有较大可能丢失数据
存储数量较大时,效率较低,I/O性能较低
基于fork创建子进程,内存产生额外消耗
宕机带来的数据丢失风险
1.2AOF:
Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。
当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
AOF 优点
-
AOF 可以更好的保护 数据不丢失,一般 AOF 会每隔 1 秒,最多丢失 1 秒钟的数据。
-
写入性能非常高,而且文件不容易破损
-
适合做灾难性的误删除的紧急恢复
AOF 缺点
-
对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。恢复速度较慢
-
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
2. 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
2.1缓存雪崩
解决方案:
1. 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。
-
给数据加随机的过期时间,确保一瞬间不会出现大面积的失效
2.2缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。 如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
-
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
-
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截
-
如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
2.3缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了 DB。而缓存击穿不同的是缓存击穿是指一个/几个Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存
解决方案:
-
设置热点数据永远不过期。
-
加互斥锁
2.4缓存预热
缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系
统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据
库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
-
解决思路
(1)直接写个缓存刷新页面,上线时手工操作下;
(2)数据量不大,可以在项目启动的时候自动进行加载;
(3)定时刷新缓存;
(4)缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
2.5缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
2.6热点数据和冷数据
热点数据,缓存才有价值对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不场景。对于热点数据, 比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。
再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。数据更新前至少读取两次,缓存才有意义。 这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,
比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力
2.7Memcache与Redis的区别都有哪些?
1、存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,redis可以持久化其数据
2、数据支持类型 memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 ,提供list,set,zset,hash等数据结构的存储
3、使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、 value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。
5、redis的速度比memcached快很多
6、Redis支持数据的备份,即master-slave模式的数据备份。
3. 单线程的redis为什么这么快
1、纯内存操作
2、单线程操作,避免了频繁的上下文切换
3、采用了非阻塞I/O多路复用机制
4. redis的数据类型
1、String,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
2、hash这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
3、list使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。
4、set因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
5、sorted set
5. redis的过期策略
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。
在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
-
定期删除
redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。
为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
因此,如果只采用定期删除策略,会导致很多key到时间没有删除。于是,惰性删除派上用场。
也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?
如果过期了此时就会删除。
惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。
采用定期删除+惰性删除就没其他问题了么?
不是的,但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢?
redis 内存淘汰机制。
6.redis数据淘汰机制
当内存到达最大内存限制时进行的数据淘汰策略
-
新写入操作会报错。(Redis 默认策略)
-
在键空间中,移除最近最少使用的 Key。(LRU推荐使用)
-
在键空间中,随机移除某个 Key。
-
在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。
-
在设置了过期时间的键空间中,随机移除某个 Key。
-
在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。
-
LRU 算法实现:
1.通过双向链表来实现,新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。
LinkedHashMap:HashMap 和双向链表合二为一即是 LinkedHashMap。HashMap 是无序的,LinkedHashMap 通过维护一个额外的双向链表保证了迭代顺序。该迭代顺序可以是插入顺序(默认),也可以是访问顺序。
-
拓展:
Redis的LRU具体实现
传统的LRU是使用栈的形式,每次都将最新使用的移入栈顶,但是用栈的形式会导致执行select *的时候大量非热点数据占领头部数据,
所以需要改进。Redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳。Redis初始的实现算法很简单,
随机从dict中取出五个key,淘汰一个lru字段值最小的。在3.0的时候,又改进了一版算法,首先第一次随机选取的key都会放入一个pool中(pool的大小为16),
pool中的key是按lru大小顺序排列的。接下来每次随机选取的keylru值必须小于pool中最小的lru才会继续放入,直到将pool放满。
放满之后,每次如果有新的key需要放入,需要将pool中lru最大的一个key取出。淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰。
上代码:
在redis.conf中有一行配置
maxmemory-policy volatile-lru
复制代码该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
补充:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
7.Redis 为什么是单线程的
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)
Redis利用队列技术将并发访问变为串行访问
1、绝大部分请求是纯粹的内存操作(非常快速)
2、采用单线程,避免了不必要的上下文切换和竞争条件
3、非阻塞IO优点:
1)速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
2)支持丰富数据类型,支持string,list,set,sorted set,hash
3)支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
4)丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决
redis的并发竞争key问题同时有多个子系统去set一个key。这个时候要注意什么呢?
不推荐使用redis的事务机制。
因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到
多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的
事务机制,十分鸡肋。
(1)如果对这个key操作,不要求顺序: 准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可作了。以此类推。
(2) 利用队列,将set方法变成串行访问也可以redis遇到高并发,如果保证读写key的一致性对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内部已经帮你处理好并发的问题了
8.Redis 常见性能问题和解决方案?
1、Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
2、 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
3、为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内
4、尽量避免在压力很大的主库上增加从库
5、主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3…
9.为什么Redis的操作是原子性的,怎么保证原子性的?
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现
10. Redis事务
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的Redis会将一个事务中的所有命令序列化,然后按顺序执行。
1、redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
2、如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
3、如果在一个事务中出现运行错误,那么正确的命令会被执行。
-
(1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
-
(2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
-
(3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
-
(4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
11. 应用场景
-
缓存
-
共享Session
-
消息队列系统
-
分布式锁
12.zset跳表的数据结构
增加了向前指针的链表叫作跳表跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
-
原理:
跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。
由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。
-
为什么使用跳跃表
首先,因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?
-
性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 (下面详细说);
-
实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
13. redis 设置过期时间
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?(数据过期策略)
14.怎么保证缓存和数据库数据的一致性?
分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。
我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。
-
合理设置缓存的过期时间。
-
新增、更改、删除数据库操作时同步更新 Redis,可以使用事务机制来保证数据的一致性。
-
缓存失败时增加重试机制。
15. redis 怎么实现分布式锁?
Redis 分布式锁其实就是在系统里面占一个“坑”,其他程序也要占“坑”的时候,占用成功了就可以继续执行,失败了就只能放弃或稍后重试。
占坑一般使用 setnx(set if not exists)指令,只允许被一个程序占有,使用完调用 del 释放锁
也可以配合EXPIRE key seconds自动释放锁
设置key的生存时间,当key过期时(生存时间为0) ,会被自动删除
缺陷 :原子性没有得到满足,所以不建议。
16.实际项目中使用缓存有遇到什么问题或者会遇到什么问题你知道吗?
缓存和数据库数据一致性问题
主从复制
作用:
读写分离:master写、slave读,提高服务器的读写负载能力
负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量
故障恢复:当master出现问题时,由slave提供服务,实现快速的故障恢复
数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式
高可用基石:基于主从复制,构建哨兵模式与集群,实现Redis的高可用方案
过程:
-
从节点执行 slaveof IP,port 发送指令
-
主节点响应
-
从节点保存主节点信息(IP,port),建立和主节点的 Socket 连接。
-
从节点发送 Ping 信号,主节点返回 Pong,确定两边能互相通信。
-
连接建立后,主节点将所有数据发送给从节点(数据同步)。
-
主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。
复制/数据同步过程分为两个阶段
-
全量复制:
slave接收到master生成的RDB文件,先清空自身的旧数据,然后执行RDB恢复过程,然后告知master已经恢复完毕。
-
部分复制(增量复制)
主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。master把自己之前创建的复制缓冲区的数据发送到slave,slave接收到aof指令后执行重写操作,恢复数据。
主从复制会存在以下问题:
-
一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
-
主节点的写能力受到单机的限制。
-
主节点的存储能力受到单机的限制。
哨兵:
哨兵(sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。
作用:
-
监控
不断的检查master和slave是否正常运行。master存活检测、master与slave运行情况检测
-
通知(提醒)
当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知。
-
自动故障转移
断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址
浙公网安备 33010602011771号