数据存储和消息队列
Redis
1. Redis 有哪些数据类型
Redis是一个开源的内存中的数据结构存储系统。它可以用作数据库,缓存和消息中间件。
它支持多种类型的数据结构,如字符串String,散列Hashes,列表Lists,集合Sets,有序集合Sorted Sets或者ZSet,Bitmaps,Hyperloglogs和地理空间Geospatial索引半径查询。
最常见的数据结构类型有String,List,Set,Hash,ZSet五种。
1.1 String
Redis的String类型是一个由字节组成的序列。它和其他编程语言或者其他键值对存储提供的字符串操作非常相似。
String是最常用的一种数据类型,普通的key/value存储都可以归为此类。value其实不仅是String,也可以是数字。
1.2 List
Redis的List其实就是链表(Redis使用双端链表实现List)。
使用List结构,可以轻松实现最新消息排行等功能。List的另一个应用就是消息队列。
一个List结构可以有序存储多个字符串,并且是允许元素重复的。
1.3 Set
Redis的集合和列表都可以存储多个字符串,列表可以存储多个相同的字符串,而集合通过使用散列表来保证自己存储的每个字符串都是各不相同的。
Redis的集合使用的是无序的方式存储元素。
应用场景:好友系统;利用唯一性,统计访问网站的所有独立IP。
1.4 Hash散列类型
Redis的散列可以存储多个键值对之间的映射。和字符串一样,散列存储的值既可以是字符串又可以是数字值,并且用户同样可以对散列存储的数字执行自增操作或者自减操作。
一个LIst散列类型的实例,是一个包含两个键值对的散列键。
1.5 有序集合ZSet
有序集合和散列一样,用于存储键值对。有序集合的键被称为成员member,每一个成员都是独一无二的。而有序集合的值被称为分值score,分值必须是浮点数。
有序集合是Redis里面唯一一个既可以根据成员访问元素,又可以根据分值以及分值的排序来访问元素的结构。
一个有序集合类型的实例,zset-key是一个包含两个元素的有序集合键。
参考:
《Redis常见的5种不同的数据类型详解》
2. Redis 内部结构
Redis内部使用一个redisObject对象来表示所有的key和value。redisObject主要的信息包括数据类型type,编码方式encoding,数据指针ptr,虚拟内存vm等。
type表示一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的编码方式。ptr指针指向对象的底层实现数据结构。
dict是一个用于维护key和value映射关系的数据结构。与很多语言中的map或dictionary类似。Redis的一个database中所有key到value的映射,就是使用一个dict来维护的。
dict本质上是为了解决算法中的查找searching问题。一般查找问题的解法分为两大类:一个基于各种平衡树,一个基于哈希表。dict是一个基于哈希表的算法。它最显著的一个特点就在独特的rehashing。它采用了增量式重哈希incremental rehashing的方法。在需要扩展内存时避免一次性对所有key进行重哈希,而是将重哈希操作分散到对于dict的各个增删改查的操作中去。这种方法每次只对一小部分key进行rehash,而每次rehash之间不影响dict的操作。这避免了rehash期间单个请求的响应时间暴增。
Redis数据库是真正存储数据的地方。而数据库本身也是存储在内存中的。数据由dict和expires两个字典构成。其中dict保存键值对,而expires则保存键的过期时间。
参考:
《Redis的内部结构》
《redis内部数据结构深入浅出》
3. Redis 使用场景
Redis读写性能优异,数据类型丰富,而且Redis是单进程单线程工作的,所以存储/删除的操作不必保证原子性。其存储数据还可以自动过期。
3.1 Redis的高性能让其非常适合作为缓存
缓存是Redis最常见的应用场景,因为其读写性能实在过于优异。且Redis内部是支持事务的,在使用时候能有效保证数据的一致性。
3.2 Redis支持多种数据格式,让其应用场景丰富。
string作为最简单的k-v存储,短信验证码,配置信息等都适合用其存储。
hash一般key作为id或者唯一标示,value存储详情。可以用于商品详情,新闻详情,个人信息等。
list是有序的,比较适合存储有序且数据相对固定的数据。例如省市区表,字典等。因为其有序,适合根据写入时间来排序,例如最新的数据,消息队列等。
set提供交集,并集,差集等操作。当其存储一个用户的好友时,可以非常便捷地找出几个用户的共同好友等信息,非常适合用于做推送等应用。
3.3 Redis是单线程的,可作为分布式锁
Redis是单线程,多路复用方式提高处理效率。Redis作为分布式锁,因为其性能的优势,不会成为瓶颈,一般会产生瓶颈的是真正的业务处理内容。
3.4 Redis的自动过期能提高开发效率
Redis的数据都可以设置过期时间,过期的数据清理无需开发者去关注,所以开发效率高。例如短信验证码的过期处理不需要像数据库一下还要查时间对比判断是否数据已经过期了。
参考:
《一起来聊聊最近很火的Redis常见应用场景解析》
4. Redis 持久化机制
将redis内存服务器中的数据持久化到硬盘等介质中使得服务器再重启之后还可以重用以前得数据,也可以防止系统出现故障而导致数据无法恢复。
Redis提供了两种不同方式的持久化方法:快照Snapshotting和之追加文件append-only-file
4.1 快照RDB
快照就是俗称的备份,可以在定期内对数据进行备份,将redis服务器中的数据持久化到硬盘中。
在创建快照之后,用户可以对快照进行备份。同行情况下,为了防止单台服务器出现故障而造成所有数据的丢失,还可以将快照复制到其他服务器,创建具有相同数据的数据副本。快照只适合数据不经常修改或者丢失部分数据影响不大的场景。因为快照只能恢复到最近一次生成快照的数据。如果这个时间间隔过大或者数据的修改非常频繁,会导致快照的恢复能力并不强大。
4.2 只追加文件AOF
AOF在执行写命令的时候,将执行的写命令复制到硬盘里面。后期恢复的时候,只需要重新执行一下这个写命令就可以了。
AOF的持久化会将被执行的写命令写到AOF文件的末尾,以此来记录数据繁盛的变化。这样,在恢复时只需要从头到尾执行一下AOF文件即可恢复数据。
参考:
《使用快照和AOF将Redis数据持久化到硬盘中》
5. Redis 集群方案与实现
这题完全超过我的能力范围了。只给出具体链接,有人对此有深刻理解的话欢迎补充此题。
参考:
《Redis集群方案应该怎么做?》
《
redis集群主流架构方案分析》
《这可能是最全的 Redis 集群方案介绍了》
6. Redis 为什么是单线程的?
Redis采用的是基于内存的单进程单线程模型的k-v数据库,由C语言编写。官方提供的数据是可以达到10万+的每秒内查询次数。
Redis的速度有部分得益于它采用的是单线程。这避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,也就不可能因为可能出现的死锁而导致的不必要的性能消耗,
而Redis为什么会采用单线程,这点官方在FAQ上已经给出来了答案:
因为Redis是基于内存的操作,CPU并不是Redis的瓶颈,Redis的瓶颈最有可能是及其内存的大小或者网络带宽。既然单线程容易实现,而且CPU又不会造成瓶颈,那么就自然而然使用单线程来避免采用多线程会碰到的各种麻烦。
但是相对的,使用单线程的方式是无法发挥多核CPU的性能的,不过可以通过在单机开启多个Redis实例来解决此问题。
参考:
《为什么说Redis是单线程的以及Redis为什么这么快!
》
7. 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
7.1 缓存雪崩
由于原有缓存失效,新缓存未到期间,所有原本应该访问缓存的请求都去直接查询数据库了,从而对数据库CPU和内存瞬间造成巨大压力,严重的甚至会造成数据库宕机。这一系列连锁反应甚至可以造成整个系统崩溃。这就是缓存雪崩。
解决方法:当并发量不高时,可以采用加锁排队来防止瞬间查询压力过大,然而此方法在生产环境中很少使用。生产环境中一般通过设置二级缓存,为key设置不同的缓存失效时间等方式来解决。
7.2 缓存穿透
缓存穿透指用户所要查询的数据在数据库中并不存在,所以这些数据自然不可能出现在缓存。而缓存中无法找到这类数据,从而导致每次都要去数据库进行查询,然后返回空。等于进行了两次无用的查询,这也是经常提到的缓存命中率问题。
解决方案:采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据系统的查询压力。也可以当一个查询返回的数据为空时,仍然将这个空结果缓存,但注意设置它的过期时间设置为较短时间。
7.3 缓存预热
缓存预热就是在系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免用户请求时先查数据库再将数据缓存。
7.4 缓存更新
除了Redis自带的六种缓存失效策略外,还可以根据具体的业务需求进行自定义的缓存淘汰。
可以定时去清理过期的缓存。或当有用户请求过来时,再判断这个请求所用到的缓存是否过期。如果过期再去底层系统得到新数据并更新缓存。
7.5 缓存降级
当访问量剧增,服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。降级的最终目的是保证核心服务可用,即使是有损的。但请注意,有些服务无法被降级:例如购物车,结算等敏感操作。
降级操作可以让系统根据一些关键数据进行自动降级,也可以配置开关实现人工降级。在进行降级之前要对系统进行梳理,看看系统是不是可以弃车保帅,从而梳理出哪些必须保护,而哪些则需要降级。
参考:
《Redis系列十:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级》
8. 使用缓存的合理性问题
- 热点数据,缓存才有价值。
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。而针对热点数据进行缓存,缓存的命中率理论上会非常高,从而会提高很大的性能。 - 频繁修改的数据,看情况考虑使用缓存。
数据更新前至少读取两次,缓存才有意义。如果缓存还没有起作用就失效了,那就没有太大的价值了。
但当读取接口对数据库压力很大,却又是个热点数据,这是就可以考虑使用缓存手段来减少数据库的压力,哪怕它的修改频率很高。 - 数据不一致性
一般会对缓存设置失效时间,一旦超过失效时间,就要从数据库重新加载。因此应用可能需要容忍一定时间的数据不一致。 - 缓存更新机制
针对数据不一致和脏读现象,可以通过缓存更新机制解决:采用缓存双淘汰机制,在更新数据库的时候淘汰缓存。此外,在超过了缓存失效时间也会淘汰掉缓存。 - 缓存可用性
缓存是提高数据读取西能的,换粗数据丢失和缓存不可用不会影响应用程序的处理。因此,一般的操作手段是,如果Redis出现异常,则手动捕获记录日志后,去数据库查询数据返回给用户。 - 采用缓存服务降级
- 采用缓存预热
- 缓存穿透的解决
参考:
《Redis实战(一) 使用缓存合理性》
9. Redis常见的回收策略
Redis内置了六种回收策略供我们使用
- volatile-lru
从已设置过期时间的数据集server.db[i].expires中挑选最近最少使用LRU的数据淘汰 - volatile-ttl
从已设置过期时间的数据集server.db[i].expires中挑选将要过期的数据淘汰 - volatile-random
从已设置过期时间的数据集server.db[i].expires中任意选择数据淘汰 - allkeys-lru
从数据集server.db[i].dict中挑选最近最少使用的数据淘汰 - alleys-random
从数据集server.db[i].dict中任意选择数据淘汰 - no-enviction
禁止驱逐数据
参考:
《redis的回收策略》
10. Redis的pipeline有什么用处?
Redis本身是基于Request/Response协议的,正常情况下,客户端发送一个命令,等待Redis应答,Redis在接收到命令,处理后应答。在这种情况下,如果同时需要执行大量的命令,那就是等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁的调用系统IO,发送网络请求。
为了提升效率,Pipeline出现了,它允许客户端可以一次发送多条命令,而不等待上一条命令执行的结果。它不仅减少了RTT,同时也减少了IO调用次数(IO调用涉及到用户态到内核态之间的切换)。
客户端这边首先将执行的命令写入到缓冲中,最后再一次性发送Redis。但是有一种情况就是,缓冲区的大小是有限制的,比如Jedis,限制为8192,超过了,则刷缓存,发送到Redis,但是不去处理Redis的应答
参考:
《Redis Pipeline原理分析》
11. Redis过期策略是怎么实现的呢?
Redis有三种过期策略:
- 定时删除
在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。
它可以保证内存被尽快释放,然而删除key会占用cpu时间,且定时器的创建耗时,大量创建定时器将会严重影响性能。 - 懒汉式删除
key过期的时候不删除,每次通过key获取值的时候去检查是否过期,若过期,则删除,返回null。
删除操作只在发生key取值的时候发生,对cpu占用较小,但若大量key在超时后都没有被获取过,可能会发生内存泄漏:已经过期的无用数据占用了大量内存。 - 定期删除
每隔一段时间执行一次删除过期key的操作
它是上面两种策略的折中版。
memcached使用的是懒汉式策略,而redis同时使用了懒汉式策略与定期删除两种策略
参考:
《Redis过期策略 实现原理》