Java面试题16 Redis
自己尝试通过打字来回答一些网上常见的面试题,答案仅代表我自己的观点
179. redis 是什么?都有哪些使用场景?
Redis是一个开源的基于键值对的内存NoSQL数据库。
应用场景:
- 缓存
- 秒杀之类的高并发数据读写
180. redis 有哪些功能?
- 基于内存,所以有非常快的读写速度,但又可以通过配置将数据持久化到硬盘
- 支持主从复制,可以很方便的建立集群
- 分布式锁
- 支持消息队列
- 支持事务
181. redis 和 memecache 有什么区别?
redis和memcache都是键值对存储的东西,多被用在缓存,但redis
的功能不只局限于缓存,完备的持久化能力还可以使它作为一个数据存储服务器,不过既然问的是redis和memcache,那就从缓存角度说。
它们最大的区别,就是redis支持五种数据类型,并且对于每种类型都提供了大量的操作方法,而memcache只有字符串,听起来简单,但是使用起来就会发现redis的灵活。
如果你仅仅是将所有待缓存数据序列化成字符串然后缓存到cache server中,那这俩其实没啥区别,但如果你想让存储的形式更加丰富,比如你想缓存一个列表,你想缓存一个对象并稍后可能只取其中的部分属性...像这种需求,memcache是无法实现的。
带来的直接好处就是,网络IO占用的带宽减少了,比如你缓存了一个1MB大小的集合,想取其中一个数据,使用Redis,网络只需要承担这一个数据的流量,该数据在Redis中被计算好并返回,应用端也不再需要进行编解码、解析的操作,而memcache就要把这1MB的集合完整返回,同时在应用侧还要做自己的编解码与解析。
182. redis 为什么是单线程的?
首先Redis并不是单线程的,从6.x开始就不能说它是单线程了,因为Redis引入了一批IO Threads,后面会聊到,但是,我们可以说Redis的工作线程,也就是用来处理你的请求任务的Worker Thread是单线程的。
下面说下我自己对该问题的看法,不一定正确
Redis是基于内存存储的,所以我们可以发现Redis的存储完全是CPU密集任务,单线程完全可以让CPU的一个核心处于忙碌状态了,并且一般情况下这种处理能力就已经比较强了,如果嫌弱,你可以横向扩展成集群嘛。而引入多个线程会发生什么?Redis需要通过使用锁或者其它线程协同机制保证多线程对数据并发访问下的安全性,也就是说它要让其它线程在另一个线程操作数据时等待,而等待就需要挂起线程,这又是一个系统调用,这个挂起操作所花费的额外时间我想都可能要比另一个线程操作内存数据还要长了。对于全是读的情况也是如此,我想表达的是,耗费在线程调度间的额外消耗可能都快赶上读取数据本身消耗的时间了。
那Redis有没有IO操作?有!从网络中读取你发来的数据,返回数据给网络,这都是IO操作。最初时,Redis使用Work Thread来处理这些内容,虽然它使用了同步非阻塞IO技术,但总归,数据在程序和内核间的拷贝还是要自己做的。所以Redis6.x中提供了IO Threads的概念,当同步非阻塞IO发现有数据到达,就启用IO Thread进行数据的拷贝,并不占用Work Thread所在CPU的时间片,让它专注于处理用户数据,而当Work Thread处理完成后,IO Thread也负责将返回数据拷贝出去。
183. 什么是缓存穿透?怎么解决?
指对不存在的数据进行查询时,每次都不会走缓存,都实际进行数据库查询。
解决办法就是将不存在状态也写入到缓存中。
184. redis 支持的数据类型有哪些?
string、list、set、zset、hash
185. redis 支持的 java 客户端都有哪些?
Jedis、Redission、Lettuce
186. jedis 和 redisson 有哪些区别?
没用过Redission
188. redis 持久化有几种方式?
- rdb方式:保存数据库的快照,可以设置成多长时间内有多少改动就保存快照。
- append-only-file方式:追加文件方式,当数据库发生更改时记录更改的语句写入到aof文件中。
187. 怎么保证缓存和数据库数据的一致性?
合理设置缓存的过期时间。
新增、更改、删除数据库操作时同步更新 Redis,可以使用事物机制来保证数据的一致性。
- redis 持久化有几种方式?
Redis 的持久化有两种方式,或者说有两种策略:
RDB(Redis Database):指定的时间间隔能对你的数据进行快照存储。
AOF(Append Only File):每一个收到的写命令都通过write函数追加到文件中。
189. redis 怎么实现分布式锁?
首先分布式锁是用来解决在分布式系统情况下,对一个共享资源的互斥访问问题,比如多个微服务中的线程去共同访问一个数据库中的商品信息,你需要在这个情况下保持商品数据的一致性,那么就要对这个商品资源加分布式锁。
所以如果以这个视角来看,你只要能够保证当一个资源访问方进行访问时,它能顺利的设下一个标志,并且其它访问方发现已经有这个标志了,就会设置失败,这样就可以实现分布式锁。所以,你可以用redis、zookeeper甚至你用一个FTP服务器的文件夹都行(当然这个比较扯了)。
在redis中,恰好提供了具有这样功能的命令——setnx
,它可以在一个key不存在时设置这个key,如果这个key存在,这个命令就会返回设置失败,而redis的单worker线程的工作模式又能保证这个setnx
操作是原子的,所以就能正确的实现分布式锁。
当一个访问方使用setnx
设置成功,它就拿到了这把锁,其它任何访问方的setnx
都无法再成功了,它在这一段时间内可以对锁住的资源进行任何操作,并在结束后删除掉这个key,相当于释放锁,其它访问方又可以开始抢占锁了。
这又引出了两个问题:
- 如果持有锁的访问方宕机,或者由于其他原因没有删除这个key,那么这个锁将永远不会被释放,死锁发生了
- 加锁失败的访问方应该如何处理失败
第一个问题,可以给这个锁的key设置超时时间,使用expire命令,这样,死锁将转化为活锁。不过问题还没有解决,expire
命令可能执行失败,或者访问者在expire
执行之前就宕机了,这时还是会出现问题,我们必须有一个设置keyvalue和超时时间的原子命令,其实set
命令本身就包含了超时参数EX
以及NX参数,就可以解决这个问题。当然,我们使用第三方Java框架时可以不用考虑这些问题。
第二个问题,加锁失败之后,可以考虑直接放弃或者自旋后重新请求锁。这里zookeeper相比redis的一个优点就体现了,zookeeper的发布订阅机制可以在锁被删除时通知到所有加锁失败的访问方。
又引出一个问题:
- 如何确定锁的超时时间?万一在超时时间内访问方没有处理完怎么办?
假设访问方A加锁,并设置超时时间为1秒,1秒后,不管A是否处理完数据,其它访问方都认为锁已经被释放了,并可以请求加锁,假设这时B又加了锁,而A在1.5秒的时候处理完了,删除了锁,那它删除的实际是B加的锁,它加的已经在被超时机制给处理了。
此时可以考虑,将锁的value也用起来,用value标识加锁方,如果删除时发现加锁方不是自己,就不删这个锁。
而问题又来了,有完没完啊??
- 发现加锁方是不是自己需要一个
get
,删除需要一个del
,这不是原子操作,有可能get
和del
的中间,锁又被改动了
这时可以使用eval/evalsha
来用lua脚本原子的执行多条命令,或者使用MULTI/EXEC
事务机制,或者干脆直接用第三方框架!!!比如Redisson!
使用redis做锁的时候不要做主从、主备这种高可用集群,因为这种不一致性让锁的意义丢失,当然,分片集群是可以的。
190. redis 分布式锁有什么缺陷?
- 性能优秀但实现复杂,见上面一个问题
- zookeeper实现锁,不需要设计抢不到锁时的自旋重试,不需要考虑锁永远不会被释放的问题,事实上redis构建分布式锁的问题都是基于锁不被释放这个问题引出的
- 其实redis也有发布订阅模型,只是我们不常用,如果使用Redisson封装好的API,这些问题都将得到解决
- redis 如何做内存优化?
- redis 淘汰策略有哪些?
- redis 常见的性能问题有哪些?该如何解决?