目录
基础
Redis有哪些数据结构?
1. String
实现:
内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配
Redis使用C开发,为什么不使用C字符串,而是用SDS?
使用SDS的优点:
获取字符串长度时间复杂度是O(1)
二进制安全
拓展知识: 什么是二进制安全?
通俗地讲,C语言中,用'0'表示字符串的结束,如果字符串本身就有'0'字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。SDS是怎么解决的?
SDS使用len属性的值判断字符串是否结束,所以不会受'\0'的影响。
杜绝缓冲区溢出
C字符串会溢出的原因
字符串的拼接操作是使用十分频繁的,在C语言开发中使用char *strcat(char *dest,const char *src)方法将src字符串中的内容拼接到dest字符串的末尾。
由于C字符串不记录自身的长度,所有strcat方法已经认为用户在执行此函数时已经为dest分配了足够多的内存,足以容纳src字符串中的所有内容,而一旦这个条件不成立就会产生缓冲区溢出,会把其他数据覆盖掉。SDS是怎么解决的?
与C字符串不同,SDS 的自动扩容机制完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查 SDS 的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,
然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出问题。扩容使用的是:自动扩容机制——sdsMakeRoomFor方法
内存重分配次数优化
空间预分配策略: 因为 SDS 的空间预分配策略, SDS 字符串在增长过程中不会频繁的进行空间分配。通过这种分配策略,SDS 将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。
惰性空间释放机制: 用于优化 SDS 字符串缩短时并不立即使用内存重分配来回收缩短后多出来的空间,而仅仅更新 SDS 的len属性,多出来的空间供将来使用。
应用场景:
缓存功能
计数器
共享用户Session,upupor网站实现了这个场景
2. Hash
是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段
3. List
有序列表
通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西
应用场景
基于 Redis 实现简单的高性能分页: 通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询
简单的消息队列: Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据
4. Set
无序集合,会自动去重的那种
应用场景:
基于 Set 玩有交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?对吧
5. SortedSet(也称为zset)
排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序
应用场景:
可以实现延时队列(根据分数来实现延时队列)
排行榜
Redis高级用法
1. HyperLogLog
提供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV
2. Geospatial
可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过用Redis来实现附近的人?或者计算最优地图路径?
3. Pub/Sub
作简单的消息队列
4. Bitmap
位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter)
5. Pipeline
可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答
6. Lua
Redis 支持提交 Lua 脚本来执行一系列的功能,原子性
7. 事务
Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去
Redis怎么做持久化?
1. RDB做镜像全量持久化(周期性的持久化)
原理
fork: fork是指redis通过创建子进程来进行RDB操作cow: cow指的是copy on write
子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来
简单的描述原理
RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;
场景
适合冷备
优缺点
优点:
RDB对Redis的性能影响非常小,因为是fork了一个子线程来做的
数据恢复的时候速度比AOF来的快
缺点:
因为是定时快照,数据丢失的多
如果快照文件很大,客户端会暂停几毫秒或者几秒
2. AOF做增量持久化(append-only模式)
简单的描述原理:
以文本日志的形式记录 Redis 处理的每一个写入或删除操作。
通过一个后台的线程fsync操作
场景:
适合热备
优缺点:
优点
数据完整性比RDB高,最多丢失1s的数据
append-only 追加写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。
缺点
AOF文件比RDB还要大
AOF开启后,Redis支持写的QPS会比RDB支持写的要低(但是不影响Redis的高性能)
AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,
只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。
3. RDB和AOF全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。
如果突然机器掉电会怎样?
取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
Pipeline有什么好处,为什么要用Pipeline?
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。
Redis集群,集群的高可用怎么保证,集群的原理是什么?
Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务
Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
Redis为什么快?
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的
SDS简单动态字符串
字典
跳跃表: 跳跃表是Redis特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
最新的一版已经是多线程了,利用现代计算机多核优势。网络处理是多线程,但是命令执行过程还是单线程处理的
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
合理的线程模型。使用多路I/O复用模型,非阻塞IO。多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
合理的数据编码。Redis 支持多种数据类型,每种基本类型,可能对多种数据结构
Redis双写一致性、并发竞争、线程模型
双写一致性
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应
更新的时候,先更新数据库,然后再删除缓存。
不更新缓存的原因是你更新了可能很长一段时间没有被用到,所以白白浪费了系统资源,所以删掉最合适,等系统下一次用到就会触发计算然后缓存结果,这样可以节省系统资源,特别是对于那种需要经过大量计算才能得到结果的任务。反正无论如何保持一个原则,【用到缓存才去算缓存】,这也是懒加载的一个思想
并发竞争。zk分布式锁
线程模型
文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
最新版的Redis6支持多线程了,要看一下!!只是接收网络请求是多线程了,实际上Redis处理数据还是单线程
高可用(重点)
Redis 支持主从同步,提供 Cluster 集群部署模式,通过 Sentine l哨兵来监控 Redis 主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从 slaveof 到新主
在 Redis 集群中,sentinel 也会进行多实例部署,sentinel 之间通过 Raft 协议来保证自身的高可用
哨兵:哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。
哨兵组件的主要功能:
集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
经典的哨兵集群:
主从数据同步描述。
你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。数据传输的时候断网了或者服务器挂了怎么办啊?
传输过程中有什么网络问题啥的,会自动重连的,并且连接之后会把缺少的数据补上的
选举新的master策略
slave 的 priority 设置的越低,优先级越高
同等情况下,slave 复制的数据越多优先级越高
相同的条件下 runid 越小越容易被选中
Redis Cluster 使用分片机制,在内部分为 16384 个 slot 插槽,分布在所有 master 节点上,每个 master 节点负责一部分 slot。数据操作时按 key 做 CRC16 来计算在哪个 slot,由哪个 master 进行处理。数据的冗余是通过 slave 节点来保障。
Redis内存块满时,会走淘汰策略
背景
不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。
配置: maxmemory
有哪些策略?
FIFO
淘汰最早数据LRU
剔除最近最少使用。(LRU算法)allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
LFU
剔除最近使用频率最低的数据noeviction
返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)allkeys-random
回收随机的键使得新添加的数据有空间存放volatile-random
回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键volatile-ttl
回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
lua脚本
是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作
Key失效机制(定期+惰性+内存淘汰)
Redis 的 key 可以设置过期时间,过期后 Redis 采用主动和被动结合的失效机制,一个是和 Memcache 一样在访问时触发被动删除,另一种是定期的主动删除。
Redis过期策略:
定期删除
定期好理解,默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了惰性删除
见名知意,惰性嘛,我不主动删,我懒,我等你来查询了我看看你过期没,过期就删了还不给你返回,没过期该怎么样就怎么样
Redis
与 Memcache 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库
相比 Memcache,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务
做好保障
事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免* MySQL* 被打死。
事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
为啥redis zset使用跳跃链表而不用红黑树实现
skiplist的复杂度和红黑树一样,而且实现起来更简单。
在并发环境下红黑树在插入和删除时需要rebalance,性能不如跳表。
使用场景
热点数据缓存: 缓存订单不常用数据
轻量级队列: 异步日志
使用缓存可能会出现的问题
缓存雪崩(雪崩就是凡是有波及的很难起来,必须等这一步雪崩过去才能起来)
在失效时间上加上随机时间,不让过多的缓存在同一个时间一起失效 或者 设置永不过期,有更新操作再更新就好了缓存击穿(击:有目的性的)
缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。缓存穿透
避免缓存穿透的利器之BloomFilterBloomFilter实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难
Bloom Filter 原理: 当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率缺点
存在误判
删除困难
一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。可以采用Counting Bloom Filter
场景
监控数据收集: cerberus在收集监控数据的时候, 有的系统的监控项量会很大, 需要检查一个监控项的名字是否已经被记录到db过了, 如果没有的话就需要写入db.
爬虫URL: 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤
垃圾邮件过滤: 如果用哈希表,每存储一亿个 email地址,就需要 1.6GB的内存(用哈希表实现的具体办法是将每一个 email地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB的内存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解决同样的问题。
**总结:** 对准确性不高的需要知道是否存在的场景
缓存更新方式
数据不一致
缓存常见面试题
有哪些缓存类型?
本地缓存
Jvm堆中的缓存,可以使用LRUMap来实现
Ehcache
**本地缓存优缺点**: 本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
分布式缓存
优缺点: 可以解决本地缓存的问题,分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。多级缓存
为了平衡本地缓存和分布式缓存,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中Memcache
特点
Memcache 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
Memcache 功能简单,使用内存存储数据;
Memcache 的内存结构以及钙化问题我就不细说了,大家可以查看官网了解下;
Slab 钙化问题
前置概念:
Slab Allocation
传统的内存分配是通过malloc/free实现,易产生内存碎片,加重操作系统内存管理器的负担。Memcached使用Slab Allocation机制高效管理内存,Slab Allocation机制按照一个特定的增长比例,将分配的内存分割成特定长度的块,完全解决内存碎片问题。
Slab Allocation将内存分割成不同Slab Class,把相同尺寸的块Chunk组成组Slab,如下图所示。Slab Allocation重复使用已分配的内存块,覆盖原有内存块的数据。
Page:操作系统的内存页,分配给Slab的内存空间,默认1MB
Chunk:用于缓存记录的内存空间,不同Slab的Chunk大小通过一个特定增长比例逐渐增大;
Slab:特定大小的Chunk的组,同一个Page分割成相同大小的Chunk,组成一个Slab。
Memcached采用LRU(Least Recent Used淘汰算法,在内存容量满时踢出过期失效和LRU数据,为新数据腾出内存空间。不过该淘汰算法在内存空间不足以分配新的Slab情况下,这时只会在同一类Slab内部踢出数据。即当某个Slab容量满,且不能在内存足够分配新的Slab,只会在相同Slab内部踢出数据,而不会挪用或者踢出其他Slab的数据。这种局部剔除数据的淘汰算法带来一个问题:Slab钙化。
如何解决?
重启Memcached实例,简单粗暴,启动后重新分配Slab class,但是如果是单点可能造成大量请求访问数据库,出现雪崩现象,冲跨数据库。
随机过期:过期淘汰策略也支持淘汰其他slab class的数据,twitter工程师采用随机选择一个Slab,释放该Slab的所有缓存数据,然后重新建立一个合适的Slab。
通过slab_reassign、slab_authmove参数控制。
Memcache 对缓存的数据可以设置失效期,过期后的数据会被清除;
失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。
致命的限制(导致在互联网场景下大家都不会选择Memcache)
key 不能超过 250 个字节;
value 不能超过 1M 字节;
key 的最大失效时间是 30 天;
只支持 K-V 结构,不提供持久化和主从同步功能。
关于Redis锁相关问题及解决方案
死锁:设置过期时间
过期时间评估不好,锁提前过期:守护线程,自动续期
锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放
如何解决主从切换后,锁失效问题?
Redis 的作者提出一种解决方案,就是我们经常听到的 Redlock(红锁)
使用分布式锁,如果锁内业务超时会怎么处理?
使用守护线程自动处理, Redisson框架就实现了