通过20问来了解redis
前人栽树,后人乘凉。本文中的问题来自公众号:《java专栏》,帮我们总结了了解redis,必须要掌握的20问,当然,很多答案个人按照自己的理解做了解读,主要还是用于自己加深理解;
- redis的特点:
redis本质上是一个key-value类型的
内存
数据库,整个数据库统统加载在内存中进行操作,定期通过异步操作吧数据同步到硬盘上保存起来。由于是纯内存操作,redis的性能非常出色(这里的原因我觉得他没说完整,还有redis的单线程特性,关于这点,要衍生就是一篇...),目前来说redis可以说是最好的key-value数据库。不仅仅由于他的快,还有丰富的数据结构,这样就能用redis来实现许多功能,比如用redis的list来做FIFO的双向链表,实现一个轻量级的消息队列服务器等。同时还能对key设置过期时间(这点用于短信验证码是真的方便)。redis的缺点:由于数据库加载在内存中操作,那么一定会收到内存的限制,不能存储大量的数据;
- 使用redis有哪些好处
速度快: 数据操作在内存中进行,单线程操作避免频繁的线程上下文切换;
数据类型丰富: string list set hash zset;
功能多: 定时过期、发布订阅、键值存储;
支持事务,操作都是原子性;
- redis和memcached比较的优势
redis异步同步数据到硬盘,memcached重启数据就会丢失;
redis支持的数据结构更多;
redis的存储速度更快;
redis的功能更多(发布订阅、过期超时);
redis底层的模型不同,通信应用协议不同,redis实现了自己的vm,减少了调用系统函数,提高了效率;
- redis的适用场景
redis的数据结构丰富使得redis的适用场景也非常的多,这里举几个常用的例子:
高频数据缓存;
队列应用(排行榜、关注列表、评论);
发布订阅(简单的消息传递,不能堆积消息,最好还是用mq);
分布式会话缓存;
分布式锁;
分布式id自增或是计数器;
....
- redis的缓存失效策略和失效机制
redis有给key设置过期时间的功能,可以通过ttl key来查看key的过期时间(-1)是不会过期;
缓存失效那么就要有定时任务定期清理过期的key,如果定时任务过于频繁,就会影响redis的性能,所以redis的key的过期清理是通过定时任务
加上读时判断
。也就是说读取key的时候如果key已过期,那么就会删除key;
在redis中,如果没有设置server.maxmemory
(最大缓存), 那么如果redis已经满了,再有新的数据添加进来,redis就会报错或崩溃;设置了之后redis在容量达到一定程度的时候就会执行key的淘汰策略,redis提供了6种key的淘汰策略:
· volatile-lru: 从设置了过期时间的数据中挑选近期最少适用的数据淘汰;
· volatile-ttl: 从设置了过期时间的key种挑选将要过期的淘汰;
· volatile-random: 从设置了过期时间的key中随机淘汰;
· allkeys-lru: 从所有的key中挑选最少适用的淘汰;
· allkeys-random: 从所有的key中随机淘汰;
· no-enviction: 禁止淘汰数据;
- 为什么redis要将所有的数据方在内存中
首先想到的就是快;redis为了达到最快的读写速度,将操作都在内存中操作使得redis的操作非常的快速(不然不就和mysql一样,磁盘io瓶颈)。
- redis为什么是单线程的
这个问题这里回答的并不好,我看到另一个地方的回答:如果一个线程进行一个操作耗时30us,那么这个线程10次操作耗时300us。如果换成多线程,每次线程上下文切换需要120us,那么执行10次至少上下文切换的时间就是1200us;而redis基于内存操作,每个操作的耗时短,就适合单线程来完成。这里说的只是单核cpu,但是要知道,对于多核cpu来说,单线程肯定不能完全利用多核cpu的特性。那么我们知道,如果操作数据,要考虑操作同一数据的并发问题,是不是就需要加锁,引入了多线程,就引入了锁竞争,也引入了设计的复杂性。所以既然单线程已经这么快了,为什么还要引入这些可能变慢的因素呢?
至于所说的redis并不能良好的应用多核cpu,所以可以在redis启动的时候绑定cpu核心,让固定的核心处理redis,然后再一台机器上部署多个redis来合理利用机器资源;
- redis如何解决并发竞争
redis使用了单线程的模式,所以redis没有锁,使用队列模式将并发访问变成串行访问。但是值得注意的是,尽管redis不存再竞争,但是操作redis的客户端还是要考虑自身并发读写redis的问题,比如并发写reids同一key,可能造成的更新顺序问题,这个问题的本质是客户端,也有一些连接超时、数据转换错误等连接错误也是需要客户端处理。
这里有一个很常见的问题,在实现分布式锁的时候,不能先set key,然后再设置过期时间。可以使用setnx或lua脚本来实现,而redission帮助我们封装了它。
- redis常见的性能问题和解决方案
Master写内存快照,save命令调度rdbSave函数,会阻塞主线程,当快照比较大的时候对性能影响会非常大,甚至间断性暂停服务。所以Master最好不要写内存快照.(redis数据同步到磁盘的策略之一的RDB的save命令的缺陷,也可以使用bgsave命令派生一个子线程来同步,占用系统资源还是在大文件的时候会对性能有影响);
Master AOF持久化, 如果不重写AOF文件,AOF相对来说影响较小,但是AOF文件会不断增大,会影响Master的重启速度。最好的方式是Master不要做任何的持久化工作,特别重要的数据让Slave开启AOF持久化,每秒同步一次。(AOF相对于RDB内存快照,使用的是追加命令的形式,可设置每秒同步一次,那么理论上最多丢失1S的数据,RDB则不那么稳定,但是AOF有一些bug,而且相同数据的文件会比RDB的方式大)
Master Slave 同步数据的问题, 为了主从同步数据的稳定性和速度,最好将redis的主从放置在一个局域网。
- redis存在事务吗
redis帮助我们提供了事务的支持,在redis中
MULTI/EXEC/DISCARD/WATCH
这几个命令帮助我们实现事务。但是相比较mysql的事务,在redis中如果有一条命令执行失败,但是其他的命令还是会执行;我们使用MULTI开启事务,EXEC执行或者是DISCARD回滚事务(他的回滚是取消,并不是真的回滚),所以我个人并不认为这是一种事务...
- redis的watch
reids和zookeeper在许多的时候提供了许多的相同的功能,但是个人经验redis的实现手法是CAP理论中的AP,而zookeeper的实现手法往往是CP;对于锁的实现AP理论往往实现的都是乐观锁,CP往往实现的是悲观锁。watch在redis中可以建通一个key,当这个key开启事务的时候,事务还没有完成的时候,这个key被修改了,那么在事务中继续执行EXEC就会返回一个空值。
- redis的分布式锁
redis分布式锁也是老生常谈了,synchronized帮助java在单个jvm中操作一段代码具有线程的安全性,但是分布式的情况下单个jvm中安全的代码在不同的jvm中还是会有并发的问题,这个时候就需要一个单点来同一管理一个分布式的synchronized。我们可以使用redis,这里也有很多陷阱:
如何加锁,使用setnx(新版本的redisTemplate提供了setAbsent方法)或者是lua脚本(本质上是保证加锁和设置过期时间在一条命令完成保证原子性);
设计可重入,value设置一个唯一id并且向下传递id以便重入, 当然想要不可重入也是可以的;
如何解锁,解锁的时候最好判断一下解锁的对象是不是加锁的对象,同样我们可以根据value来判断;
锁的重试,redis实现的是乐观锁,在特定的场景下可以自行选择是重试还是直接失败;
- 想要获取redis中具有相同前缀的key,如何找出他们
redis提供了基于key的一系列操作,想要找出有相同前缀的key,使用keys xxx就可以了,但是,这往往是个陷阱。接下来就会问,如果在线上系统执行 keys 命令会有什么问题吗?
首先应该能向到的是效率问题,如果redis中的key过多,并且使用这个前缀的key也较多,那么使用这个命令会造成redis短暂的阻塞,直到这条命令执行完毕;
这时候应该就能想到redis是单线程的,需要等待这条指令执行完成才能执行下一条;
那么我们如何避免这个呢?这个时候我们可以使用scan命令来提取想要的key, 这是无阻塞的获取key方式,使用游标进行分页迭代;
- redis如何做异步队列使用
redis提供了多种数据结构,对于队列我们就可以使用list作为队列,rpush产生消息,lpop消费消息(或者使用blpop消费,此种情况当没有消息在队列中的时候会阻塞到消息到来),如果数据保证不重复的话使用set也是可以的。
如果要进行广播消费,那么可以使用redis的发布订阅,此种方式可以在简单场景下替代mq,但是他的缺点是没有堆积能力,在消费者下线的情况下,生产的消息会丢失,所以要堆积消息削峰填谷的时候还是要用类似rocketmq、kafka这样的专业mq;
如果想实现延时队列,可以使用Zset,score使用时间戳,消费者使用rangebyscore来消费消息,然后按时间戳做定时任务处理;
- redis中的key为什么不要大量的设置相同过期时间
如果有大量的key在同一时间过期,一个是redis本身清理这些key可能会有短暂的卡顿,其次,这些key大量的过期,造成业务访问大量直接访问数据库,造成缓存雪崩,严重影响整个系统的性能。
此时我们应该在设置过期时间的时候加一个随机值,让key在不同的时间过期。
- 使用redis如何处理缓存穿透
缓存穿透出现在当业务数据的key没有命中缓存,查询数据库,但是数据库中确实没有数据,就永远不能存入缓存,那么每次这个key的查询都直接查询数据库,对应用系统造成影响。我们应当在redis的key多次未命中的时候将这个key存入缓存,设置一个较短的失效时间,或者在应用层面过滤掉无效的key;
还有一种方式是使用布隆过滤器,redis有现成的布隆过滤器的实现;
- 使用reids如何处理缓存击穿
缓存击穿指的是在系统压力比较大的时候,其中的key到了失效的时间,此时大量的请求都会直接打到数据库(还没来的及缓存到redis),这个时候数据库就会由很大的压力,虽然比较短暂。对于这些可能比较大并发的key,我们可以不设置其过期时间,高并发并不会一直持续,这是互联网的特点,等到并发峰值过了之后,在设置清除策略;或者是key命中达到一定次数的时候,更新key的过期时间;
- 什么是redis的pipleline
简单来说pipleline就是批量操作,这样的好处是可以减少io次数,如果分10次来设置key、value,那么系统应用和redis之间就有10次的io,而将者10次操作使用pipleline做一次操作,可以提高redis的性能。当然,如果本身请求不多的话,这也是不必要的,一般来说在高并发的时候可以使用pipleline;
- redis的同步机制
redis可以使用主从同步,也可以使用从从同步。第一次同步的时候,主节点做一次bgsave,同时将bgsave之后的操作记录到内存buffer中,完成bgsave后将rdb文件同步到复制节点,复制节点加载完rdb文件,再通知主节点将之后的操作记录同步到复制节点,由复制节点一直重放之后的操作。
- redis的集群
redis的sentinal集群方式,由sentinal服务来监控redis的主从节点,进行选主操作,适用于需要提供高可用,数据较小的服务;
redis的cluster集群方式, 将需要存在redis中的数据按slot切分到不同的redis节点中(就是数据分片类似mongodb、es),其中分配到数据的都是主节点,每个主节点再可以有几个从节点,从节点从主节点同步数据,如果一个分片上的所有节点都不可用,则集群不可用,这种方式适用于相对较大的数据分片存储;