【八股文】Redis
1.Redis是什么
2.Redis优缺点
3.Redis为什么是单线程,6版本之后为什么又变成了多线程
4.Redis应用场景
5.Redis和Memcached的区别
6.Redis数据结构与底层实现
7.持久化机制
8.Redis事务
9.Redis过期策略
10.内存淘汰机制
11.Redis常见的部署方式有哪些
12.高并发和高可用方案
13.如何保证缓存与数据库双写一致性
14.Redis的雪崩,穿透和击穿
15.Redis如何实现消息队列
16.Redis如何实现分布式锁
1.Redis是什么
Redis是一个使用C语言编写的,高性能非关系型的键值对数据库。与传统的数据库不同,Redis的数据是存储在内存中的,所以读写速度非常快,被广泛应用在缓存方向。Redis可以将数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。
2.Redis优缺点
优点:
- 基于内存操作,内存读写速度快
- 单线程,避免线程切换开销及多线程的竞争问题。单线程是指网络请求使用一个线程来处理,即一个线程处理所有网络请求,Redis运行时不止有一个线程,比如数据持久化还会另起线程
- 支持多种数据结构
- 支持持久化
- 支持事务。
- 支持主从复制
缺点:
- 对结构化查询的支持比较差
- 数据库容量受到物理内存的限制,不适用作海量数据的高性能读写
- 较难支持在线扩容
3.Redis为什么是单线程,6版本之后为什么又变成了多线程
- 避免过多的上下文切换开销。
- 避免同步机制的开销
- 实现简单,方便维护
引入多线程:
- 可以充分利用服务器CPU资源,单线程模型的主线程只能利用一个CPU
- 多线程任务可以分摊Redis同步IO读写的负荷
4.Redis应用场景
- 缓存
- 数据共享分布式
比如分布式session,由于在集群或分布式环境下,不同的tomcat管理各自的session,而且通过复制的方式会极大的影响效率。
将登录成功后的session信息,存放在Redis中,这样多个服务器(tomcat)可以共享session信息
- 分布式锁
string的set命令增加了一些参数:
EX:设置键的过期时间(秒)
PX:设置键的过期时间(毫秒)
NX:只在键不存在时,才对键进行设置操作。
XX:只在键已经存在时,才对键进行设置操作。
由于这个操作是原子性的,可以简单实现一个分布式锁
set lock_key locked NX EX 1
如果这个操作返回false,说明key的添加不成功,也就是当前有人占用这把锁。而如果返回true,说明获得了这把锁,设置了过期时间也可以释放锁。
5.Redis和Memcached的区别
- 数据类型:Redis数据类型丰富,Memcached只有string
- 持久化:redis数据可以持久化,Memcached不可以
- 线程模型:Redis使用单线程的多路IO复用模型,Memcached使用多线程的非阻塞IO模型
- 内存管理:Redis单线程,Memcached多线程
- 集群:Redis提供主从同步机制和cluster集群部署能力。Memcached没有提供原生集群
6.Redis数据结构与底层实现
1.数据类型
- string:能表达字符串,整数,浮点数
- hash:键值对集合
- list:可以存储有序,可重复的元素
- set:无序去重的集合。set提供了交集,并集等方法,对于实现共同好友,共同关注等功能特别方便
- zset:有序set。内部维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景
- Bitmap:位图,可以认为是一个以位为单位的数组,数组的每个单元只能存0或者1
- Hyperloglog:是用做基数统计的算法
- Geospatial:主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位,附近的人等
2.应用场景
- String
存储mysql中某个字段的值
生成自增id
set nx用作分布式锁
- hash 存储对象
- list
消息队列
列表:用户列表,商品列表
- set
共同好友
- zset
排行榜
3.底层原理
- String:使用了SDS(动态字符串对象),加入了free和len字段,获取字符串长度只需要O(1)
- hash:小于一个阈值的话使用压缩列表作为底层实现,其他的时候使用字典作为底层实现,字典就类似于java中的HashMap
- list:底层是快速列表,快速列表是由压缩列表和双向列表组成的
快速列表里的每一个节点都维护一个ziplist,ziplist是真正存放数据的地方。只有新增一个超过64长度的字符串或者ziplist包含的节点超过512,才会转化成双向链表。
- set:inset和哈希表。inset可以理解为数组,元素个数不小于512并且可以用整型表示时使用
- zset:ziplist和skiplist,有序集合保存的元素数量小于128 && 有序集合保存的所有元素的长度小于64字节时使用ziplist
当ziplist作为zset的底层存储结构时,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值
当skiplist作为zset的底层存储结构时,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系
※ Redis底层结构详细讲解:
我们先看看一个Redis整体的结构:
RedisDB
相当于Redis的数据库
当Redis服务器初始化时,会预先分配16个数据库(编号0-15)
SDS
跳跃表
将有序链表中的部分节点分层,每一层都是一个有序链表
字典
字典dict又称散列表
Redis整个数据库是用字典来存储的,对Redis进行crud操作其实就是对字典中的数据进行crud
Redis字典包括:字典(dict),Hash表(dictht),Hash表节点(dictEntry)
Redis字典除了主数据库的K-V数据存储外,还可以用于:散列表对象,哨兵模式中的主从节点管理等不同的应用中,字典的形态都可能不同,dictType是为了实现各种形态的字典而抽象出来的操作函数(多态)
扩容流程:rehash
1)初次申请默认分配4个dictEntry,非初次申请为当前hash表容量的一倍
2)迁移过程非常慢
压缩列表
ziplist是由一系列特殊编码的连续内存块组成的顺序型数据结构
应用场景:
sorted-set和hash元素个数少且是小整数或短字符串
list用快速链表数据结构存储,而快速链表是双向列表和压缩列表的组合
整数集合
整数集合(intset)是一个有序的,存储整数的连续存储结构
当Redis集合类型的元素都是整数并且都处在64位有符号整数范围内(2的64次方),使用该结构体存储
快速列表
快速列表(quicklist)是Redis底层重要的数据结构,是列表的底层实现
7.持久化机制
持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失
- RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
- AOF:AOF 机制对每条写入命令作为日志,以
append-only
的模式写入一个日志文件中,在 Redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集。
8.Redis事务
1)使用MULTI开启一个事务
2)在开启事务的时候,每次操作的命令都将会被插入到一个队列中,同时这个命令并不会被真的执行
3)EXEC命令提交(不支持回滚)
WATCH
命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行(类似于乐观锁)。执行EXEC
命令之后,就会自动取消监控。
9.Redis过期策略
定期删除 + 惰性删除
定期删除:指的是Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除
惰性删除:在你获取某个key的时候,检查一下是否已经过期,过期了此时就会删除
10.内存淘汰机制
过期策略仍然可能导致有大量过期key堆积在内存中,这个时候我们可以走内存淘汰机制
volatile-lru:LRU(Least Recently Used
),最近使用。利用LRU算法移除设置了过期时间的key
allkeys-lru:当内存不足以容纳新写入数据时,从数据集中移除最近最少使用的key
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
allkeys-random:从数据集中任意选择数据淘汰
no-eviction:禁止删除数据,当内存不足以容纳新写入数据时,新写入操作会报错
11.Redis常见的部署方式有哪些
1)单机版:很少使用。内存容量有限,处理能力有限,无法高可用
2)Redis主从:master挂了之后需要手动指定新的master
3)Redis Sentinel(哨兵):哨兵可以选举新的master。但每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用
4)Redis Cluster:主要针对海量数据+高并发+高可用的场景。
12.高并发和高可用方案
**高并发:**通过主从实现
单机的Redis,能够承载的QPS大概就在上万到几万不等,对于缓存来说,一般是用来支撑读高并发的。因此架构做成主从架构,一主多从,主负责写,并且将数据复制到其他的slave节点,从节点负责读。
主从复制:
1)当启动一个从节点时,它从发送一个PSYNC命令给主节点
2)如果是从节点初次连接到主节点,那么会触发一次全量复制。此时主节点会启动一个后台线程,开始生成一份RDB快照文件。
3)同时还会将从客户端client新收到的所有写命令缓存到内存中。RDB
文件生成完毕后, 主节点会将RDB
文件发送给从节点,从节点会先将RDB
文件写入本地磁盘,然后再从本地磁盘加载到内存中
4)接着主节点会将内存中缓存的写命令发送到从节点,从节点同步这些数据
5)如果从节点跟主节点之间网络出现故障,连接断开了,会自动重连,连接之后主节点仅会将部分缺失的数据同步给从节点
**高可用:**哨兵Sentinel
哨兵是一个独立的进程,可以独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
执行流程
1)启动并初始化Sentinel
Sentinel是一个特殊的Redis服务器,不会进行持久化
Sentinel会创建2个连向主服务器的网络连接:命令连接和订阅连接
2)哨兵检测
检测主观下线状态:我(Sentinel)没有接收到主服务器的回复
3)哨兵确认
检测客观下线状态:监控所有其他Sentinel发送查询命令,确认主服务器宕机
4)哨兵故障转移
选举Leader:通过Raft协议
Redis Cluster:
进一步提升性能
13.如何保证缓存与数据库双写一致性
14.Redis的雪崩,穿透和击穿
缓存雪崩
解决方案:
事前:Redis高可用,主从 + 哨兵,Redis Cluster,避免全盘崩溃
事中:本地ehcache缓存 + hystrix限流&降级,避免mysql被打死
事后:Redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据
缓存穿透
解决方案:将数据库中所有可能的数据哈希映射到布隆过滤器。然后对每个请求进行如下判断:
- 请求数据的key不存在于布隆过滤器中,可以确定数据就一定不会存在于数据库中,系统可以立即返回不存在
- 请求数据的key存在于布隆过滤器中,则继续再向缓存中查询
使用布隆过滤器能够对访问的请求起到了一定的初筛作用,避免了因数据不存在引起的查询压力
缓存击穿
缓存击穿,就是说某个key非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开一个洞。
- 若缓存的数据是基本不会发送更新的,则可尝试将该热点数据设置为永不过期
- 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于Redis,zk等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存
- 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存
15.Redis如何实现消息队列
- List:基于List结构来模拟消息队列
- PubSub:基本的点对点消息模型
- Stream:较完善的消息队列模型
List底层是双向链表,可以利用List的添加取出命令来实现模拟消息队列
lpush和brpop
16.Redis如何实现分布式锁
三个命令:setnx,expire,delete
第一步:setnx key val(setnx就是,若key不存在,则存入键值对,若ket存在,则什么都不做,返回0)
第二步:expire key(为key设置一个过期时间)
第三步:delete key(删除指定key)
#java##八股文##Redis#本专栏是我总结的八股大全