【7】Redis面试总结

219 阅读18分钟

说一下对Redis的理解?

  • Redis是一个用C语言开发的的高性能key-value非关系型缓存数据库。
  • 它提供了五种数据类型来存储值:string(字符串)、list(链表)、set(集合)、zset(有序集合)、hash(哈希类型)。
  • 它是一种 NoSQL 数据存储。

Redis能做什么?

  • 缓存使用,减轻DB压力
  • DB使用,用于临时存储数据(字典表,购买记录)
  • 冷热数据交换
  • 解决分布式场景下Session分离问题(登录信息)
  • 做分布式锁、乐观锁(高性能、高响应(秒杀)采用乐观锁)

Session⼀致性怎么解决?

  • Nginx的IP_Hash策略(可以使用)
    • 同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器,
  • Session复制(不推荐)
    • 多个tomcat之间通过修改配置⽂件,达到Session之间的复制
  • 基于redis来实现session分离

基于redis来实现session分离

传统的session是由tomcat自己进行维护和管理。集群或分布式环境,不同的tomcat管理各自的session。只能在各个tomcat之间,通过网络和Io进行session的复制,极大的影响了系统的性能。

可以将登录成功后的Session信息,存放在Redis中,这样多个服务器(Tomcat)可以共享Session信息。 利用spring-session-data-redis(SpringSession),可以实现基于redis来实现的session分离。

只需要引入Jar包,然后在启动类加上 @EnableRedisHttpSesstion

分布式锁怎么实现?

关键命令:setnx(当key不存在时才能赋值,key在在再次赋值会失败)

setnx用于分布式锁,当value不存在时采用赋值,可用于实现分布式锁。

利用Watch和incr实现Redis乐观锁

Watch: watch命令用于监视任意数量的数据库键,并在EXEC命令执行时,检测被监视的键是否被修改,如果被修改了,服务器将拒绝执行事务,并向客户端返回空回复。

乐观锁: 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,乐观锁适用于多读的应用类型,这样可以提高吞吐量。

incr: 递增数字,可用于实现乐观锁 watch(事务)

不具有互斥性,不会产生锁等待而消 耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。

Redis乐观锁实现流程:

  • 1、 利用redis的watch功能,监控这个redisKey的状态值
  • 2、获取redisKey的值
  • 3、创建redis事务
  • 4、给这个key的值+1
  • 5、然后去执行这个事务,执行后返回空说明key的值已经被修改过了,回滚事务,key不加1

setnx实现Redis分布式锁

setnx: (set if not exist)

添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉

实现原理: 利用Redis的单线程特性对共享资源进行串行化处理

使用Redisson实现分布式锁

使用redisson的API

  • RLock mylock = redisson.getLock(key);
  • mylock.unlock()

说一下对缓存的理解?

什么是缓存?

在互联网技术中,缓存是系统快速响应的关键技术之一

以空间换时间的一种技术(艺术)

缓存的分类如下:

客户端缓存

  • 传统互联网:页面缓存和浏览器缓存Cookie、WebStorage(SessionStorage和LocalStorage)、WebSql等

  • 移动互联网:APP缓存,原生APP中把数据缓存在内存、文件或本地数据库(SQLite)中。比如图片文件。

网络端缓存

  • Web代理缓存:Nginx,缓存原生服务器的静态资源,比如样式、图片等。
  • 边缘缓存: CDN

服务端缓存

服务器端缓存是整个缓存体系的核心。包括数据库级缓存、平台级缓存和应用级缓存。

数据库级缓存

服务器本地缓存

GuavaCache

分布式缓存

具有缓存功能的中间件:Redis等

利用集群支持高可用、高性能、高并发、高扩展

总结:

对于后端开发我们需要设计的缓存的:

JVM缓存、文件缓存和Redis缓存

JVM缓存: JVM缓存就是本地缓存,设计在应用服务器中(tomcat)。 通常可以采用Ehcache和Guava Cache,在互联网应用中,由于要处理高并发,通常选择GuavaCache。

文件缓存: 这里的文件缓存是基于http协议的文件缓存,一般放在nginx中。 因为静态文件(比如css,js, 图片)中,很多都是不经常更新的。nginx使用proxy_cache将用户的请 求缓存到本地一个目录。下一个相同请求可以直接调取缓存文件,就不用去请求服务器了。

Redis缓存: 作为Mybatis的二级缓存使用

过期策略(如何保证redis中的数据都是热点数据)?

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

maxmemory

  • 在redis.conf中 maxmemory 默认为0 不限制 无最大内存限制
  • maxmemory-policy 默认为noeviction (禁止驱逐) 不淘汰
  • 设置maxmemory maxmemory-policy 要配置

不设置maxmemory的场景

  • Redis的key是固定的,不会增加
  • Redis作为DB使用,保证数据的完整性,不能淘汰 , 可以做集群,横向扩展

设置maxmemory的场景

  • Redis是作为缓存使用,不断增加Key
  • 设置多少与业务有关,1个Redis实例,保证系统运行 1 G ,剩下的就都可以设置Redis,约物理内存的3/4
  • 命令: 获得maxmemory数
CONFIG GET maxmemory
  • 设置maxmemory后,当趋近maxmemory时,通过缓存淘汰策略,从内存中删除对象

Redis缓存穿透,缓存击穿,缓存雪崩?

  • 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从⽽可能压垮数据源。⽐如⽤⼀个不存在的⽤户id获取⽤户信息,不论缓存还是数据库都没有,若⿊客利⽤此漏洞进⾏攻击可能压垮数据库。

  • 缓存击穿:key对应的数据存在,但在redis中过期,此时若有⼤量并发请求过来,这些请求发现缓存过期⼀般都会从后端DB加载数据并回设到缓存,这个时候⼤并发的请求可能会瞬间把后端DB压垮。

  • 缓存雪崩:当缓存服务器重启或者⼤量缓存集中在某⼀个时间段失效,这样在失效的时候,也会给后端系统(比DB)带来很大压力。

缓存穿透,缓存击穿,缓存雪崩解决方案

缓存穿透解决方案

  • 布隆过滤器,将所有可能存在的数据哈希到⼀个⾜够⼤的bitmap中,⼀个⼀定不存在的数据会被 这个bitmap拦截掉,从⽽避免了对底层存储系统的查询压⼒
  • 果⼀个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进⾏缓存,但它的过期时间会很短,最⻓不超过五分钟。

缓存击穿解决方案

setnx实现Redis分布式锁

在缓存失效的时候(判断拿出来的值为空), 不是⽴即去load db,⽽是先使⽤缓存⼯具的某些带成功操作返回值的操作(⽐如Redis的SETNX或者 Memcache的ADD)去set⼀个mutex key,当操作返回成功时,再进⾏load db的操作并回设缓存;否 则,就重试整个get缓存的⽅法。

// 伪代码
public String get(key) {
    String value = redis.get(key);
    if (value == null) { //代表缓存值过期
        //设置3min的超时,防⽌del操作失败的时候,下次缓存过期⼀直不能load db
        if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del(key_mutex);
        } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
            sleep(50);
            get(key); //重试
        }
    } else {
         return value; 
       }
}

缓存雪崩解决方案

1.保持缓存层的高可用性

使用Redis 哨兵模式或者Redis 集群部署方式

2.使用限流降级组件

常见的限流降级组件如 Hystrix、Sentinel等

3.缓存不过期

Redis 中保存的 key 永不失效,这样就不会出现大量缓存同时失效的问题,但是随之而来的就是Redis 需要更多的存储空间。

4.优化缓存过期时间

设计缓存时,为每一个 key 选择合适的过期时间,避免大量的 key 在同一时刻同时失效,造成缓存雪 崩。

5.使用互斥锁重建缓存

setnx实现Redis分布式锁

6.异步重建缓存

从线程池中获取线程来异步构建缓存,从而不会让所有的请求直接到达存储层

该方案中每个Redis key 维护逻辑超时时间,当逻辑超时时间小于当前时间时,则说明当前缓存已经失效,应当进行缓存更新,否则说明当前缓存未失效,直接返回缓存中的 value 值。

如在Redis 中将 key 的过期时间设置为 60 min,在对应的 value 中设置逻辑过期时间为 30 min。这样当key 到了 30 min 的逻辑过期时间,就可以异步更新这个key的缓存,但是在更新缓存的这段时间内,旧的缓存依然可用。

这种异步重建缓存的方式可以有效避免大量的 key 同时失效。

Redis缓存和MySQL数据⼀致性的解决方案?

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不⼀致的 情况。举⼀个例⼦:

1.如果删除了缓存Redis,还没有来得及写库MySQL,另⼀个线程就来读取,发现缓存为空,则去数据库 中读取数据写⼊缓存,此时缓存中为脏数据。

2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不⼀致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不⼀致的问题。

延时双删策略

在写库前后都进⾏redis.del(key)操作,并且设定合理的超时时间。

伪代码如下

public void write(String key,Object data){
 redis.delKey(key);
 db.updateData(data);
 Thread.sleep(500);
 redis.delKey(key);
}

1.具体的步骤

  • 1)先删除缓存
  • 2)再写数据库
  • 3)休眠500毫秒(这里的时间设定主要是保证读请求结束,写请求可以删除读请求遭成的缓存脏数据,需要自行评估确定。)
  • 4)再次删除缓存

2.设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终⼀致性的解决⽅案。

所有的写操作以数据库为准,只要到达缓存过期时间,则后⾯的读请求⾃然会从数据库中读取新值然后回填缓存。

3.弊端:

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不⼀致,⽽且⼜增加了写请求 的耗时。

异步更新缓存(基于订阅binlog的同步机制)

1.技术整体思路:

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

  • 1)读Redis:热数据基本都在Redis
  • 2)写MySQL:增删改都是操作MySQL
  • 3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

2.Redis更新

1)数据操作主要分为两⼤块:

⼀个是全量(将全部数据⼀次写⼊到redis)

⼀个是增量(实时更新)

这⾥说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利⽤消息队列,推送更新各台的redis缓存数据。 这样⼀旦MySQL中产⽣了新的写⼊、更新、删除等操作,就可以把binlog相关的消息推送⾄Redis, Redis再根据binlog中的记录,对Redis进⾏更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据⼀致 性。

这⾥可以结合使⽤canal(阿⾥的⼀款开源框架),通过该框架可以对MySQL的binlog进⾏订阅,⽽canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这⾥的消息推送⼯具你也可以采⽤别的第三⽅:kafka、rabbitMQ等来实现推送更新Redis。

Binlog:

是记录所有数据库表结构变更以及表数据修改的二进制日志,不会记录SELECT和SHOW这类操作。

开启Binlog日志有以下两个最重要的使用场景。

  • 主从复制:在主库中开启Binlog功能,这样主库就可以把Binlog传递给从库,从库拿到Binlog后实现数据恢复达到主从数据一致性。
  • 数据恢复:通过mysqlbinlog工具来恢复数据。

Redis并发竞争key的解决方案详解?

多客户端同时并发写一个key,一个key的值是1,本来按顺序修改为2,3,4,最后是4,但是由于并发设置的原因,最后顺序变成了4,3,2,最后变成的key值成了2。

分布式锁

同上 缓存击穿解决方案 setnx实现Redis分布式锁

准备一个分布式锁,大家去抢锁,抢到锁就做set操作。

消息队列

在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。

把Redis.set操作放在队列中使其串行化,必须的一个一个执行。

这种方式在一些高并发的场景中算是一种通用的解决方案。

如何保存redis的高可用性?

1.主从复制

Redis支持主从复制功能,可以通过执行slaveof(Redis5以后改成replicaof)或者在配置文件中设置 slaveof(Redis5以后改成replicaof)来开启复制功能。

主从配置

主Redis:无需特殊配置

从Redis:修改从服务器上的 redis.conf 文件:

replicaof 127.0.0.1 6379

作用:

读写分离

一主多从,主从同步

主负责写,从负责读

提升Redis的性能和吞吐量

数据复制原理(执行步骤)

  • ①从数据库向主数据库发送sync(数据同步)命令。
  • ②主数据库接收同步命令后,会保存快照,创建一个RDB文件。
  • ③当主数据库执行完保持快照后,会向从数据库发送RDB文件,而从数据库会接收并载入该文件。
  • ④主数据库将缓冲区的所有写命令发给从服务器执行。
  • ⑤以上处理完之后,之后主数据库每执行一个写命令,都会将被执行的写命令发送给从数据库。

2.哨兵模式

哨兵(sentinel)是Redis的高可用性(High Availability)的解决方案:

由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。

当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服 务,从而保证redis的高可用性。

需要安装Redis-Sentinel组件然后修改其配置文件

Redis哨兵主要功能

  • (1)集群监控:负责监控Redis master和slave进程是否正常工作
  • (2)消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
  • (3)故障转移:如果master node挂掉了,会自动转移到slave node上
  • (4)配置中心:如果故障转移发生了,通知client客户端新的master地址

当主节点出现故障时,由Redis Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。

3.集群与分区

分区是将数据分布在多个Redis实例(Redis主机)上,以至于每个实例只包含一部分数据。

单机Redis的网络I/O能力和计算资源是有限的,将请求分散到多台机器,充分利用多台机器的计算能力 可网络带宽,有助于提高Redis总体的服务能力。

Redis为什么是单线程、及高并发快的原因?

  • 1.redis是基于内存的,内存的读写速度⾮常快;
  • 2.redis是单线程的,省去了很多上下⽂切换线程的时间;
  • 3.redis使⽤多路复⽤技术,可以处理并发的连接。

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的⼤⼩或者⽹络 带宽。既然单线程容易实现,⽽且CPU不会成为瓶颈,那就顺理成章地采⽤单线程的⽅案了。

在单线程的情况下,就不⽤去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死 锁⽽导致的性能消耗。

采⽤单线程,避免了不必要的上下⽂切换和竞争条件,也不存在多进程或者多线程导致的切换⽽消耗 CPU。

单线程弊端:⽆法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;

Redis 持久化?

两种级别的持久化方式:

  • RDB持久化方式:可以在指定的时间间隔能对数据进行快照存储.
  • AOF持久化方式:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据

RDB(Redis DataBase)

  • 是redis默认的存储方式,RDB方式是通过快照完成的。
  • 在指定的时间间隔内,将内存中的数据集的快照写入磁盘
  • 默认保存在/usr/local/bin中,文件名dump.rdb;
  • 在redis.conf中配置:save

优缺点:

  • RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)
  • 不保证数据完整性,会丢失最后一次快照以后更改的所有数据。

AOF(append only file)

  • Redis默认情况下是不开启的。开启AOF持久化后Redis将所有对数据库进行过写入的命令(及其参数)记录到AOF文件
  • 当Redis重启后只要按顺序回放这些命令就会恢复到原始状态了。
  • AOF会记录过程,RDB只管结果。

RDB与AOF对比

  • 1、RDB存某个时刻的数据快照,采用二进制压缩存储,AOF存操作命令,采用文本存储(混合)
  • 2、RDB性能高、AOF性能较低
  • 3、RDB在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF设置为每秒保存一次,则最多丢2秒的数据
  • 4、Redis以主服务器模式运行,RDB不会保存过期键值对数据,Redis以从服务器模式运行,RDB会保存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。
  • 5、AOF写入文件时,对过期的key会追加一条del命令,当执行AOF重写时,会忽略过期key和del命令。

RDB与AOF应用场景

  • 内存数据库 rdb+aof 数据不容易丢
  • 有原始数据源: 每次启动时都从原始数据源中初始化 ,则 不用开启持久化 (数据量较小)
  • 缓存服务器 rdb 一般 性能高

在数据还原时

  • 有rdb+aof 则还原aof,因为RDB会造成文件的丢失,AOF相对数据要完整。
  • 只有rdb,则还原rdb