1.什么是Redis?
Redis是一中基于 key-value 对的非关系型数据库(nosql),redis 所有数据都存在内存中,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。
因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。
Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
2.为什么要用 redis/为什么要用缓存?
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
3. Redis好处?
(1) 速度快,因为数据存在内存中。类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
(2) 支持丰富数据类型,支持string(字符串类型),list(列表类型),set(集合类型),sorted set(有序集合类型),hash(散列类型)
(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
(4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
4、为什么要用 redis 而不用 map/guava 做缓存?
缓存分为本地缓存和分布式缓存。以 Java 为例,
使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
3.redis和memcached的区别?
(1) redis支持更丰富的数据类型(支持更复杂的应用场景):Redis支持Srting、list,set,zset,hash等数据结构的存储。memcache只支持字符串String数据类型。
(2)Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用;而Memecache把数据全部存在内存之中。
(3)Redis使用单线程的多路 IO 复用模型;Memcached是多线程、非阻塞IO复用的网络模型。(redis的速度比memcached快很多)
(4)集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但redis 目前是原生支持 cluster 模式的。
4.Redis支持的数据类型?
- String(字符串类型)
- String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。
- 常用命令: set,get,incr,decr,mget 等:set key value 设置值、 get key 获取值、 incr key 加一、 decr key 减一
- hash(哈希)
- Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。
- 常用命令: set,get,decr,incr,mget 等:
- hset key field value 设置值
- hget key field 获取值
- hincrby key field num 设置增数量
- list(列表)
- Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
- Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
- 可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
- 常用命令: lpush,rpush,lpop,rpop,lrange等:
- lpush list a b c d (从list左边添加元素)、 rpush list 1 2 3 4 (从list右边添加元素)
- lrange list 0 -1(从0 到 -1 元素查看:也就表示查看所有)
- lpop list (从list左边取,删除)、 rpop list (从list右边取,删除)
- set(集合)
- Redis的Set是string类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
- 常用命令: sadd,spop,smembers,sunion 等:
- sadd set1 a b c d d (向set1中添加元素) 元素不重复
- smembers set1(查询元素)、 srem set1 a(删除元素)
- sorted set(zset,有序集合)
- 和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
- 例:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
- 常用命令: zadd,zrange,zrem,zcard等:
- zadd zset1 1 a 2 b 3 c (添加元素 zadd key score member,这里添加元素a:1分、元素b:2分、元素c:3分 )
- zrange zset1 0 -1 (查看zset1的所有元素,默认从小到大)
- zrange zset1 0 -1 withscores (查看zset1的所有元素,包括分数score)
- zrevrange zset1 0 -1 (查看zset1的所有元素,从大到小)
- zincrby zset1 5 a (对zset1的a元素增加5分)
5.什么是Redis持久化?Redis有哪几种持久化方式?优缺点是什么?
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
(Redis 数据都放在内存中。如果机器挂掉,内存的数据就不存在。所以需要做持久化,将内存中的数据保存在磁盘,下一次启动的时候就可以恢复数据到内存中。)
Redis 提供了两种持久化方式:RDB(默认) 和AOF 。
- RDB (快照):
Redis可以通过创建快照来 获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
- AOF(只追加文件):
与快照持久化相比,AOF持久化的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
- RDB (快照):快照形式 ,定期将当前时刻的数据保存磁盘中。会产生一个dump.rdb文件
- 特点:性能较好,数据备份。但可能会存在数据丢失。
- AOF(只追加文件) :append only file (所有对redis的操作命令记录在aof文件中),恢复数据,重新执行一遍即可。
- 特点:每秒保存,数据比较完整。但耗费性能。
【注】如果两个都配了优先加载AOF。(同时开启两个持久化方案,则按照 AOF的持久化放案恢复数据。)
6、redis 设置过期时间
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们 set key 的时候,都可以给一个过期时间(expire time),通过过期时间我们可以指定这个 key 可以存活的时间。
如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?
定期删除+惰性删除。
- 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
- 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除。
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? redis 内存淘汰机制(数据淘汰策略)。
7.Redis有哪几种数据淘汰策略?
问题:MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
redis 提供 6种数据淘汰策略:
- volatile-lru::从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
- allkeys-lru: 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
- no-enviction:禁止驱逐数据,永不回收数据
在Redis当中,有生存期的key被称为volatile。在创建缓存时,要为给定的key设置生存期,当key过期的时候(生存期为0),它可能会被删除。
更新生存时间:可以对一个已经带有生存时间的 key 执行EXPIRE命令,新指定的生存时间会取代旧的生存时间
最大缓存配置:在 redis 中,允许用户设置最大使用内存大小 server.maxmemory,默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
8.Redis 有哪些架构模式?讲讲各自的特点
主从模式(redis2.8版本之前的模式)、哨兵sentinel模式(redis2.8及之后的模式)、redis cluster模式(redis3.0版本之后)优点:
- 解决数据备份问题
- 做到读写分离,提高服务器性能
缺点:
- 每个客户端连接redis实例的时候都是指定了ip和端口号的,如果所连接的redis实例因为故障下线了,而主从模式也没有提供一定的手段通知客户端另外可连接的客户端地址,因而需要手动更改客户端配置重新连接。
- 主从模式下,如果主节点由于故障下线了,那么从节点因为没有主节点而同步中断,因而需要人工进行故障转移工作。
- 无法实现动态扩容。
优点:
- 如果主服务器异常,则会进行主从转换,将其中一个从服务器升级为为主服务器,将之前的主服务器作为从服务器。
缺点:
- 如果是从节点下线了,sentinel是不会对其进行故障转移的,连接从节点的客户端也无法获取到新的可用从节点
- 无法实现动态扩容
在这个图中,每一个蓝色的圈都代表着一个redis的服务器节点。它们任何两个节点之间都是相互连通的。客户端可以与任何一个节点相连接,然后就可以访问集群中的任何一个节点。对其进行存取和其他操作。
一般集群建议搭建三主三从架构,三主提供服务,三从提供备份功能。
每一个节点都存有这个集群所有主节点以及从节点的信息。
它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。如果某个节点和所有从节点全部挂掉,我们集群就进入faill状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入faill状态。这就是我们的redis的投票机制,具体原理如下图所示:
优点:
- 有效的解决了redis在分布式方面的需求
- 遇到单机内存,并发和流量瓶颈等问题时,可采用Cluster方案达到负载均衡的目的
- 可实现动态扩容
- P2P模式,无中心化
- 通过Gossip协议同步节点信息
- 自动故障转移、Slot迁移中数据可用
缺点:
- 为了性能提升,客户端需要缓存路由表信息。
- 节点发现、reshard操作不够自动化。
9. 说一下Redis集群原理
Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用一种叫做哈希槽 (hash slot)
的方式来分配数据的。redis cluster 默认分配了 16384 个slot,当需要在 Redis 集群中放置一个 key-value 时,redis先对key使用crc16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在 0-16383之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点。
Redis 集群会把数据存在一个 master节点,然后在这个 master 和其对应的salve之间进行数据同步。当读取数据时,也根据一致性哈希算法到对应的 master 节点获取数据。只有当一个master 挂掉之后,才会启动一个对应的 salve 节点,充当 master 。
需要注意的是:必须要3个或以上
的主节点,否则在创建集群时会失败,并且当存活的主节点数小于总节点数的一半时,整个集群就无法提供服务了。
10.缓存穿透?击穿?雪崩?
缓存穿透:一般缓存系统都是按照key去查询,如果不存在对应的value再去数据库中查找。一些恶意的请求会故意查询不存在的key,请求量很大,会对数据库造成很大的压力。
解决:
1)接口层增加校验,如对id做基础校验,id<=0的直接拦截。
2)从缓存中取不到的数据在数据库中也没有取到,这时可以将key-value对携程key-null,缓存有效时间设置短点。这样可以防止攻击用户反复用同一个id暴力攻击。
缓存击穿:指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时缓存没读到数据,就同时去数据库去取数据,引起数据库压力瞬间增大。
解决:
1)设置热点数据永不过期。
2)加互斥锁。缓存中如果没数据,就首先去获取锁,获取锁成功后(if(relock.tryLock()))后再去数据库取数据,然后更新缓存数据。其他并行进入的线程等待100ms再重新去缓存取数据。
缓存雪崩:指缓存中大批量数据到过期时间,而查询数量巨大,引起数据库压力过大甚至宕机。 与缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存血崩是不同数据。
解决:
1)对查询结果为空的数据也进行缓存(key-value写成key-null),然后将它的缓存时间设置短一点,这样可以防止攻击用户反复用一个id暴力攻击。
2)对key进行过滤。将所有可能存在的数据哈希到一个足够大的bitmap中,一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
3)缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
11. 如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争Key 的问题也就是:多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
12.如何保证Redis缓存与数据库的数据一致性?
采用如下策略:
- 读的时候:先读缓存,读到数据则直接返回;如果没有读到,就读数据库,同时将数据放入缓存,并返回响应。
- 更新的时候:先更新数据库,然后再删除缓存。
为什么不先删除缓存再更新数据库?
如果有两个并发操作:一个更新、一个查询。更新操作删除删除缓存后,查询操作没有从redis中查到数据,就从数据库中取,而此时数据库中还是老数据。查询操作从数据库中取得老数据后写入缓存中,然后更新操作更新了数据库。这样数据库与缓存中数据不一致,缓存中还是老数据。
先更新数据库再删除缓存就没问题吗?
还是有的,例如一个读操作没有从缓存中读到数据就去数据库里读,此时突然来了一个写操作,它先更新了数据库又删除了缓存,之后那个读操作再把老数据放进去,这样这样数据库与缓存中数据不一致,缓存中还是老数据。
解决:可以为缓存设置过期时间。
为什么删除缓存,而不是把更新的数据写入缓存里?
如果不删除缓存而将更新的数据写入缓存,这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。
13. Redis集群方案什么情况下会导致整个集群不可用?
(1)如果集群任意master挂掉,且当前master没有slave,集群进入fail状态。
(2)如果集群超过半数以上master挂掉,无论是否有slave集群,都会进入fail状态.17.redis事务
multi:开启事务
- multi:开启事务
- exec:提交事务
Redis提供了一个 multi 命令开启事务,exec 命令提交事务,在它们之间的命令是在一个事务内的,能保证原子性。Redis在事务没提交之前不会执行事务中的命令,会等到事务提交的那一刻再执行事务中的所有命令。
multi 开始到 exec结束前,中间所有的命令都被加入到一个命令队列中;当执行 exec命令后,将queue中所有的命令执行。
- discard:放弃事务。即该事务内的所有命令都将取消。清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
- Watch:保证原子性。在 multi 命令之前可以使用 watch 命令来“观察”一个或多个key,在事务提交之前Redis会确保被“观察”的key有没有被修改过,没有被修改过才会执行事务中的命令,如果存在key被修改过,那么整个事务中的命令都不会执行。 (乐观锁CAS)
- unwatch:取消WATCH命令对多有key的监控,所有监控锁将会被取消。
事务对异常的处理机制:
Redis执行命令的错误主要分为两种:
- 命令错误:执行命令语法错误,比如说将 set 命令写成 sett
- 运行时错误:命令语法正确,但是执行错误,比如说对 List 集合执行 sadd 命令
18.redis事务是否具有原子性、隔离性、一致性、持久性?
Redis事务总是具有原子性、一致性和隔离性,当 Redis 运行在某种特定的持久化模式下(appendfsync always)时,事务也具有持久性。
- 原子性√:事务是一个原子操作,事务中的命令要么全部执行,要么全部不执行。redis单个操作是原子的,多个操作也支持原子性,通过multi与exec命令包起来。
- 隔离性√:redis事务是一个单独的隔离操作,事务中的所有命令都会按顺序地执行,事务在执行过程中不会被客户端发来的其他命令打断,如果是discard只会放弃全部命令。
- 一致性√:redis拒绝入队错误的事务,指令执行错误的事务不会引起整个事务失败。
- 持久性×:事务的持久性由redis使用的持久化模式决定:
- (1)单纯的内存模式下,事务肯定是不持久的;
- (2)RDB模式下,服务器可能在事务执行之后、RDB文件更新之前这段时间失败,因此也是不持久的;
- (3)在AOF的“总是”持久化方式下(appendfsync always:每次有数据修改发生时都会写入AOF文件)是持久的,因为事务每条命令在执行成功后都会立即调用fsync将是事务数据写入aof文件。
13.为什么redis需要把所有数据放到内存中?
Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。
如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
14.Redis是单进程单线程的。
redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。
参考 redis面试题