1.Redis 是什么?
Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能非关系型(NoSQL)的(key-value)键值对数据库。可以用作数据库、缓存、消息中间件等。
2.Redis 的基础存储结构有哪些?
- String,字符串,是 redis 的最基本的类型,一个 key 对应一个value。是二进制安全的,最大能存储 512MB。
- Hash,散列,是一个键值(key=>value)对集合。string 类型的field 和value的映射表,特别适合用于存储对象。每个hash 可以存储 232 -1 键值对(40多亿)
- List,列表,是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列边或者尾部(右边)。最多可存储 232 - 1元素(4294967295, 每个列表可存储40 亿)
- Set,集合,是 string 类型的无序集合,最大的成员数为 232-1(4294967295, 每个集合可存储 40 多亿个成员)。
- Sorted set,有序集合,和 set 一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset 的成员是唯一的,但分数(score)却可以重复。
使用场景:
- String
- 这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计 数功能的缓存。
- hash
- 这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟 出类似session的效果。
- list
- 使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。
- set
- 因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
- sorted set
- sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。
3.Redis 的优点?
- 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过10 万次读写操作,是已知性能最快的 Key-Value 数据库。Redis支持事务 、持久化
- 单线程操作,避免了频繁的上下文切换。
- 采用了非阻塞 I/O 多路复用机制。I/O多路复用就是只有单个线程,通过跟踪每个 I/O 流的状态,来管理多个 I/O 流。
- 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
- 支持数据持久化,支持AOF和RDB两种持久化方式。
- 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
- 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
4.为什么要用 Redis
- 高性能:
- 假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
- 假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
- 高并发
- 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
- 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
5.redis 的持久化
Redis 提供了两种持久化的方式,分别是 RDB(Redis DataBase)和AOF(AppendOnly File)。
- RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上。
- AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。 RDB 和 AOF
- 两种方式也可以同时使用,在这种情况下,如果redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF方式的数据恢复完整度更高。
6.Redis 的缺点
- 缓存和数据库双写一致性问题
- 一致性的问题很常见,因为加入了缓存之后,请求是先从 redis 中查询,如果redis 中存在数据就不会走数据库了,如果不能保证缓存跟数据库的一致性就会导致请求获取到的数据不是最新的数据。
- 解决方案
- 编写删除缓存的接口,在更新数据库的同时,调用删除缓存的接口删除缓存中的数据。这么做会有耦合高以及调用接口失败的情况。
- 消息队列:ActiveMQ,消息通知。
- 缓存的并发竞争问题
- 并发竞争,指的是同时有多个子系统去 set 同一个 key 值。
- 解决方案
- 最简单的方式就是准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可
- 缓存雪崩问题
- 缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
- 解决方案
- 给缓存的失效时间,加上一个随机值,避免集体失效。
- 使用互斥锁,但是该方案吞吐量明显下降了。
- 搭建 redis 集群。
- 缓存击穿问题
- 缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
- 解决方案
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
- 采用异步更新策略,无论 key 是否取到值,都直接返回,value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。
7.Redis 集群
7.1 主从复制
原理:
从服务器连接主服务器,发送 SYNC 命令。主服务器接收到 SYNC 命名后,开始执行BGSAVE 命令生成 RDB文件并使用缓冲区记录此后执行的所有写命令。主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令。从服务器收到快照文件后丢弃所有旧数据,载入收到的快照。主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令。从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令(从服务器初始化完成)。主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)。
优点:
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。为了分载Master 的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master 来完成 Slave 同样可以接受其它 Slaves的连接和同步请求,这样可以有效的分载Master 的同步压力 Master Server 是以非阻塞的方式为 Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis 则返回同步之前的数据。
缺点:
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP才能恢复。主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了系统的可用性。Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
7.2 哨兵模式
当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。为此,Redis2.8 中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。哨兵的作用就是监控 Redis 系统的运行状况,它的功能包括以下两个。
- 监控主服务器和从服务器是否正常运行。
- 主服务器出现故障时自动将从服务器转换为主服务器。
工作方式:
每个 Sentinel (哨兵)进程以每秒钟一次的频率向整个集群中的Master 主服务器,Slave 从务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过down-after-milliseconds 选项所指定的值, 则这个实例会被Sentinel(哨兵)进程标记为主观下线(SDOWN)。如果一个 Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master 主服务器的确进入了主观下线状态。当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master 主服务器进入了主观下线状态(SDOWN),则Master 主服务器会被标记为客观下线(ODOWN)。
在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一 169 / 196 次的频率向集群中的所有 Master主服务器,Slave 从服务器发送 INFO 命令。当Master 主服务器被Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave从服务器发送 INFO 命令的频率会从10 秒一次改为每秒一次。若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线,Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel (哨兵)进程发送 PING命令返回有效回复,Master 主服务器的主观下线状态就会被移除。
- 优点
- 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。主从可以自动切换,系统更健壮,可用性更高。
- 缺点
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
7.3 Redis-Cluster 集群
redis 的哨兵模式基本已经可以实现高可用,读写分离,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0 上加入了cluster 模式,实现的redis 的分布式存储,也就是说每台 redis
节点上存储不同的内容。Redis-Cluster 采用无中心结构,它的特点如下: 所有的 redis 节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。节点的 fail 是通过集群中超过半数的节点检测失效时才生效。客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
工作方式:
在 redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis 会根据 crc16的算法得出一个结果,然后把结果对16384 求余数,这样每个 key 都会对应一个编号在 0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点 ping 一个主节点 A时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点A1都宕机了,那么该集群就无法再提供服务了。
8.Redis 的分布式锁
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:
- 安全特性:互斥访问,即永远只有一个 client 能拿到锁
- 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的client crash 了或者出现了网络分区
- 容错性:只要大部分 Redis 节点存活就可以正常提供服务
redis实现分布式锁:
- Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis 的连接并不存在竞争关系 Redis 中可以使用SETNX 命令实现分布式锁。
- 当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key已经存在,则SETNX不做任何动作
- SETNX 是『SET if Not eXists』(如果不存在,则SET)的简写。
- 返回值:设置成功,返回 1 。设置失败,返回 0 。
使用 SETNX 完成同步锁的流程及事项如下:
- 使用 SETNX 命令获取锁,若返回 0(key 已存在,锁已存在)则获取失败,反之获取成功
- 为了防止获取锁后程序出现异常,导致其他线程/进程调用 SETNX 命令总是返回0 而进入死锁状态,需要为该 key 设置一个“合理”的过期时间
- 释放锁,使用 DEL 命令将锁数据删除
9.热点数据和冷数据是什么
热点数据,缓存才有价值,对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存对于两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。
再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
10.Memcache与Redis的区别都有哪些?
- 存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,redis可以持久化其数据
- 数据支持类型 memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据 类型,提供list,set,zset,hash等数据结构的存储
- 使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。
- redis的速度比memcached快很多
- Redis支持数据的备份,即master-slave模式的数据备份。
11.单线程的redis为什么这么快
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 采用了非阻塞I/O多路复用机制
12.为什么Redis的操作是原子性的,怎么保证原子性的?
- 对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
- Redis的操作之所以是原子性的,是因为Redis是单线程的。
- Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
- 多个命令在并发中也是原子性的吗?
- 不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.
13.Redis 为什么是单线程的
官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问
1)绝大部分请求是纯粹的内存操作(非常快速)
2)采用单线程,避免了不必要的上下文切换和竞争条件
3)非阻塞IO优点:
- 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度 都是O(1)
- 支持丰富数据类型,支持string,list,set,sorted set,hash
- 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
- 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发 竞争key问题
14.Redis有哪些使用场景?
- 缓存数据
- Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。
- 排行榜
- 很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。
- 计数器
- 如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。
- 分布式会话
- 集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
- 分布式锁
- 分布式锁实现方案,常见有三种:数据库,Redis、zookeepr。Redis就是其中之一。
- 如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
- 社交网络
- 点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
- 最新列表
- Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
- 消息系统
- 消息队列主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。但Redis不是一个专业的消息队列。建议使用其他消息队列:Kafka、RocketMQ、RabbitMQ等。
15.分布式锁的实现条件?
- 互斥性,和单体应用一样,要保证任意时刻,只能有一个客户端持有锁
- 可靠性,要保证系统的稳定性,不能产生死锁
- 一致性,要保证锁只能由加锁人解锁,不能产生A的加锁被B用户解锁的情况
16.对 Redis 进行性能优化,有些什么建议?
- Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件。
- Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务load 过高,出现短暂服务暂停现象。
- 尽量避免在压力很大的主库上增加过多的从库。
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…。
- Redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
17.Redis6.0为何引入多线程?
Redis支持多线程主要有两个原因:
- 可以充分利用服务器 CPU 资源,单线程模型的主线程只能利用一个cpu;
- 多线程任务可以分摊 Redis 同步 IO 读写的负荷。
18.keys命令存在的问题?
- redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan。
- scan的缺点:在scan的过程中如果有键的变化(增加、删除、修改),遍历过程可能会有以下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键。
19.如何保证缓存与数据库双写时的数据一致性?
- 先删除缓存再更新数据库
- 进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。
- 存在的问题:删除缓存数据之后,更新数据库完成之前,这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。
- 先更新数据库再删除缓存
- 进行更新操作时,先更新MySQL,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。
- 存在的问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。
- 异步更新缓存
- 数据库的更新操作完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。
20、缓存穿透是什么?
缓存穿透是指查询一个不存在的数据,由于缓存是不命中时被动写的,如果从DB查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了。
- 缓存空值,不会查数据库。
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询压力。
布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询Redis和数据库。
21、缓存雪崩是什么?
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重挂掉。
解决方法:在原有的失效时间基础上增加一个随机值,使得过期时间分散一些。
22.缓存击穿是什么?
缓存击穿:大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到数据库。缓存击穿是查询缓存中失效的key,而缓存穿透是查询不存在的 key。
解决方法:加分布式锁,第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。
public String get(String key) {
String value = redis.get(key);
if (value == null) {
//缓存值过期
String unique_key = systemId + ":" + key;
//设置30s的超时
if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) {
//设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(unique_key);
} else {
//其他线程已经到数据库取值并回写到缓存了,可以重试获取缓存值
sleep(50);
//重试
get(key);
}
} else {
return value;
}
}