1. 什么是Redis
C语言开发的,高性能键值对 内存数据库,是一种非关系型数据库
a. 性能优秀,读写速度非常快。可以10万+的QPS(每秒内的查询次数)。
b. 单进程单线程,是线程安全的。
c. 具有丰富的数据类型支持,包括 字符串,散列,列表,集合,有序集合,另外还有高级数据类型 BitMaps, HyperLogLog, GEO
d. 支持数据持久化
e. 主从复制,哨兵,高可用
f. 可以用作分布式锁
g. 可以作为消息中间件使用,支持发布订阅
2. 简单说下 五种数据类型
a. string 是 redis 最基本的数据类型,一个 key 对应 一个 value,value 不仅可以是 string, 也可以是数字。 string类型是二进制安全的
意思是 redis 的 string 类型可以包含任何数据,比如图片或者序列化的对象。string 类型的值最大能存储 512M.
b. Hash 是一个键值的集合。就像我们 Java 中的 map 类型,特别适合存储对象
常用命令有 hget, hset, hgetall
c. list 是简单的字符串列表,按照插入顺序排序。
常用命令: lpush, lpop, rpush, rpop, lrange
list 就是链表,可以用来当消息队列使用。
redis list 的实现是一个双向链表,可以支持反向查找和遍历,不过带来了额外的内存开销。
d. set 是string 类型的无序集合。集合是通过 hashtable 实现的。无序且没有重复。
常用命令: sdd, spop, smembers, sunion等
redis set对外提供的功能和list一样是一个列表,特殊之处在于set是自动去重的,而且set提供了判断某个成员是否在一个set集合中
e. zset 和 set一样是string类型元素的集合,且不允许重复的元素。
常用命令:zadd、zrange、zrem、zcard等。
sort set 关联了一个 double 类型的权重参数 score ,使得集合中的元素能够按照 score 进行有序排列。
实现方式:Redis sorted set的内部使用HashMap和跳跃表(skipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,
而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
3. 怎么使用 redis
a. 直接通过 RedisTemplate 来使用
b. 使用spring cache集成Redis
4. 缓存和数据库数据一致性问题:
如果项目对缓存的要求是 强一致性的,那就不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,更新数据库后及时更新缓存、缓存失败时增加重试机制。
5. 缓存雪崩
例如: 我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期,所有原本应该访问缓存的请求都去查询数据库了,对数据库造成巨大的压力,严重的会造成宕机。从而形成一系列连锁反应,造成整个系统崩溃。
解决方案:
a. 大多数系统设计者考虑用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
b. 在批量往Redis 存数据的时候,把每个 key 的失效时间都加个随机值,保证数据不会在同一时间大面积失效。
6. 缓存穿透
缓存和数据库中都没有的数据,用户在不断的发起请求。
解决方案:
- 直接在接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接return,比如对 id 做基础校验,id <= 0 的直接拦截。
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 使用布隆过滤器。
布隆过滤器的原理:
利用高效的数据结构和算法快速判断出你这个 key 是否在数据库中存在,不存在就return, 存在就去查 DB 刷新 KV ,再return。
7. 缓存击穿
和缓存雪崩有点像,但又不一样。缓存雪崩是因为大面积缓存失效,打崩了数据库,而缓存击穿是指一个 key 非常热点,在不停的扛着大量的请求,大并发集中对这一个点进行访问,当这个key 失效的瞬间,持续的大并发请求直接落到了数据库上,就在 这个 key 的点上击穿的缓存。
解决方案:
设置热点数据永不过期,或者加上互斥锁。
//互斥锁代码:
public static String getData(String key) throws InterruptedException {
//从Redis查询数据
String result = getDataByKV(key);
//参数校验
if (StringUtils.isBlank(result)) {
try {
//获得锁
if (reenLock.tryLock()) {
//去数据库查询
result = getDataByDB(key);
//校验
if (StringUtils.isNotBlank(result)) {
//插进缓存
setDataToKV(key, result);
}
} else {
//睡一会再拿
Thread.sleep(100L);
result = getData(key);
}
} finally {
//释放锁
reenLock.unlock();
}
}
return result;
}
8. 缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据!
解决方案:
a. 直接写个缓存刷新页面,上线时手动操作一下
b. 数据量不大的话在项目启动的时候自动进行加载。
c. 定时刷新缓存
9. 缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
10. redis 为什么这么快? 它的多线程模型你了解么?
Redis 是单进程单线程的模型,完全基于内存操作,CPU不是redis的瓶颈,redis 的瓶颈最有可能是机器内存的大小或者网络带宽。
为什么这么快?
a. 完全基于内存。
b. 采用单线程,避免了不必要的上下文切换和竞争条件。
c. 使用多路复用IO模型,非阻塞IO.
11. Memcache 与 Redis 的区别有哪些?
- 存储方式: Memcache 把数据全部存储在内存中,断电之后会挂掉。数据不能超过内存大小。而Redis 有部分存在硬盘上,redis 可以持久化数据。
- 数据支持类型:Memcache 所有的值均是简单的字符串而Redis 支持更为丰富的数据类型,提供 list, set, zset, hash 等数据结构的存储
- 使用底层模型不同 它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- value 值的大小不同 Redis 最大可以支持 1GB ,而 Memcache 只有 1MB
12. Redis 过期策略
定期删除 + 惰性删除
- 定期删除: redis 会将设置了过期时间的key放入到一个独立的字典中,以后会定期遍历这个字典来删除过期的key默认扫描时间 100 ms 一次。过期扫描不会遍历过期字典中所有的 key ,而是采用了一种贪心策略。
1. 从过期字典中随机20个key
2. 删除这20个key中已经过期的key
3. 如果过期的key比率超过四分之一,那就重复步骤1- 惰性删除: 这个就是用户在访问这个key的时候,redis 对这个key的过期时间进行了检查,如果过期了就立即删除不会给你返回任何东西。
所以为什么需要淘汰策略就很容易理解了,不管是定期删除还是惰性删除,都不是一种完全精准的删除,还会存在key没有被删掉的场景,所以需要淘汰策略来进行补充。
13. Redis 淘汰策略(6种),Redis4.0加入了LFU(least frequency use)淘汰策略
- noeviction: 当内存使用超过配置的时候会返回错误,不驱逐任何键
- volatile-lru: 加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
- volatile-ttl: 从配置了过期时间的键中驱逐马上就要过期的键
- volatile-lfu: 从所有配置了过期时间的键中驱逐使用频率最少的键
- volatile-random: 加入键的时候如果过限,从过期键的集合中随机驱逐
- allkeys-lru: 加入键的时候如果过限,首先通过 LRU 算法驱逐最久没有使用的键
- allkeys-lfu: 从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-random: 从所有键中驱逐使用频率最少的键
14. Redis的LRU实现
Redis 内部维护了一个24位的时钟,可以简单的理解为当前系统的时间戳,每隔一定时间会更新这个时钟。每个key对象内部也维护了一个同样的时钟,当新增key对象的时候会把系统的时钟赋值到这个内部对象时钟。比如我现在要进行LRU,那么首先拿到当前的全局时钟,再找到内部时钟与全局时钟距离时间最久的进行淘汰。这里值得注意的是全局时钟只有24位,按秒为单位来表示才能存储194天,所以可能会出现key 的时钟大于全局时钟的情况,此时应该相加计算最久的key.
15. LFU
LFU把原来的key对象的内部时钟的24位分成两部分,前16位还代表时钟,后8位代表一个计数器。16位的情况下如果还按照秒为单位就会导致不够用,所以一般这里以时钟为单位。而后8位表示当前key对象的访问频率,8位只能代表255,但是redis并没有采用线性上升的方式,而是通过一个复杂的公式,通过配置如下两个参数来调整数据的递增速度。
- lfu-log-factor : 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
- lfu-decay-time : 是一个以分钟为单位的数值,可以调整counter的减少速度。
16. 持久化 RDB , AOF
redis 为了保证效率,数据缓存在了内存中,但会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。
Redis 默认持久化方式: RDB
RDB: 快照形式,是直接把内存中的数据保存到一个 dump 的文件中,定时保存策略。
AOF: 把所有对Redis服务器进行修改的命令都会存到一个文件里。
当Redis重启的时候,他会优先使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的更完整。
17. RDB
默认 Redis 会以快照"RDB"的形式将数据持久化到磁盘的一个二进制文件 dump.rdb。
- 工作原理:
当Redis需要持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉。- RDB的优点:
适用于备份,非常适合灾难恢复。- RDB的缺点:
无法做到实时持久化,具有较大可能丢失数据。
宕机带来数据丢失风险
基于fork创建子进程,内存产生额外消耗
18. AOF
使用 AOF 做持久化,每一个写命令都通过 write 函数追加到 appendonly.aof 中
- 工作原理:
当 Redis 重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。- AOF的优点:
更好的保护数据不丢失,最多丢失1s
写入性能高,文件不易破损
适合做灾难性的误删除的紧急恢复- AOF的缺点:
恢复速度比较慢。
比RDB 快照文件更大。
19. redis 主从复制过程和原理
主从配置结合哨兵模式能解决单点故障问题,提高 redis 的可用性。从节点只提供读操作,主节点提供写操作。
- 复制过程:
a. 从节点执行 slaveof[masterIp][masterPort],保存主节点信息
b. 从节点中的定时任务发现主节点信息,建立与主节点的 socket 连接
c. 从节点发送 Ping 信号,主节点返回 Pong, 两边可以互相通信
d. 建立连接后,主节点将所有数据发送给从节点进行数据同步,完成复制过程。
f. 主节点持续把写命令发送给从节点,保持主从数据一致性。- 数据同步过程:
概念:
redis 2.8 之前使用 sync[runId][offset] 同步命令
redis 2.8 之后使用 psync[runId][offset] 命令
(sync 命令只支持全量复制,psync支持全量和部分复制)
runId:每个redis节点启动都会生成唯一的uuid,每次redis重启后,runId都会发生变化
offset:主节点和从节点都各自维护自己的主从复制偏移量offset,当主节点有写入命令时,offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的offset,并把自己的offset发送给主节点。这样,主节点同时保存自己的offset和从节点的offset,通过对比offset来判断主从节点数据是否一致。
repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是1MB。
过程:
a. 主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。从节点同步完主节点数据后,主节点将缓冲区中的数据继续发送给从节点,用于部分复制。
b. 主节点响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区,用于复制命令丢失的数据补救。
psync的执行流程:
从节点发送psync[runId][offset]命令,主节点有三种响应:
(1)FULLRESYNC:第一次连接,进行全量复制
(2)CONTINUE:进行部分复制
(3)ERR:不支持psync命令,进行全量复制
20. Redis 高级数据类型
- BitMaps
bitmaps严格来说并不是一种新的数据类型,而是基于字符串位操作的集合,由于字符串是二进制安全的,并且最长可支持512M,所以它们可以用来存储2的32次方(512 * 1024 * 1024 * 8 )不同位的数据。
应用场景:系统记录用户的登录情况、用户在线情况等。- HyperLogLog
用来做基数统计算法(基数就是数据集中不重复元素的数量)
常用命令:pfadd, pfcount, pfmerge
应用场景:实现记录网站每天访问的独立IP数量、系统的有多少用户访问、直播间的观看人数等。- GEO
redis在3.2版本里面添加了一个对地理位置(GEO)的支持。
geoadd:添加地理位置的坐标。
geopos:获取地理位置的坐标。
geodist:计算两个位置之间的距离。
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
geohash:返回一个或多个位置对象的 geohash 值。
21. Redis Sentinel(哨兵)
- 主要功能: 主节点存活检测,主从运行情况检测,自动故障转移,主从切换。
- 最小配置: 一主一从
- 执行任务:
1. 监控主从服务器是否正常运行
2. 当某个redis服务器出现问题时,通过API脚本向管理员或其他应用程序发出通知。
3. 主节点不能工作时,将从节点升为主节点,并将其它从节点指向新的主节点。避免人工干预。
4. 配置提供者:在Redis Sentinel模式下,客户端应用在初始化时连接的是Sentinel节点集合,从中获取主节点的信息。- 工作原理:
1. 每个Sentinel节点都需要定期执行以下任务:
每个Sentinel以每秒一次的频率,向它所知的主服务器、从服务器以及其他的Sentinel实例发送一个PING命令。
2. 如果一个实例距离最后一次有效回复PING命令的时间超过down-after-milliseconds所指定的值,那么这个实例会被Sentinel标记为主观下线。
3. 如果一个主服务器被标记为主观下线,那么正在监视这个服务器的所有Sentinel节点,要以每秒一次的频率确认主服务器的确进入了主观下线状态。
4. 如果一个主服务器被标记为主观下线,并且有足够数量的Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个主服务器被标记为客观下线。
5. 一般情况下,每个Sentinel会以每10秒一次的频率向它已知的所有主服务器和从服务器发送INFO命令,当一个主服务器被标记为客观下线时,Sentinel向下线主服务器的所有从服务器发送INFO命令的频率,会从10秒一次改为每秒一次。
6. Sentinel和其他Sentinel协商客观下线的主节点的状态,如果处于SDOWN状态,则投票自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制。
7. 当没有足够数量的Sentinel同意主服务器下线时,主服务器的客观下线状态就会被移除。当主服务器重新向Sentinel的PING命令返回有效回复时,主服务器的主观下线状态就会被移除。