目录:
- 写在前面
- 什么是 redis ?
- 为什么要用Redis(缓存)?
- 为什么要用Redis 而不用 map/guava 做缓存?
- Redis与 memcached 的区别
- Redis的应用场景
- Redis为什么这么快?
- Redis有哪些数据类型?各自的使用场景?
- Redis 的持久化机制是什么?
- RDB持久化、AOF持久化的区别
- 如何保证缓存与数据库双写时的数据一致性?
- 什么是 缓存 穿透?怎么解决
写在前面
什么是Redis?
Redis是现在最受欢迎的 NoSQL 数据库之一,Redis是一个使用 ANSI C 编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库,其具备如下特性:
- 基于内存运行,性能高效( 每秒可以处理超过 10万次读写操作 )
- 支持分布式,理论上可以无限扩展
- Key-Value 存储系统(key是字符串,键有 字符串 、列表、集合、散列表、有序集合等 )
- 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
为什么要用Redis(缓存)?
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能: 假如⽤户第⼀次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
直接操作缓存能够承受的请求是远远⼤于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
为什么要用Redis 而不用 map/guava 做缓存?
缓存分为本地缓存和 分布式缓存
以 Java 为例,使⽤⾃带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,而命周期随着 jvm 的销毁⽽结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
Redis与Memcached的区别
两者都是非关系型内存键值数据库,现在公司⼀般都是用 Redis来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:
(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
(2) redis的速度比memcached快很多
(3) redis可以持久化其数据
Redis的应用场景
计数器
可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
缓存
将热点数据放到内存中,设置内存的最⼤使⽤量以及淘汰策略来保证缓存的命中率。
会话缓存
可以使用 Redis 来统一存储多台应⽤服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。以 Magento 为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对 WordPress 的⽤户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
查找表
例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,⽽缓存的内容可以失效,因为缓存不作为可靠的数据来源。
消息队列(发布/订阅功能)
List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka 、 RabbitMQ 等消息中间件。
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用Redis 自带的SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
其它
Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。
Redis为什么这么快?
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap ,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU ,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现 死锁 而导致的性能消耗;
4、使用多路 I/O 复用模型,非阻塞 IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制,因为⼀般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis有哪些数据类型?各自的使用场景?
Redis主要有5种数据类型,包括String,List,Set,Zset, Hash ,满足大部分的使用要求。但Redis还为我们提供了几种高级数据结构,bitmaps,HyperLogLong、Geo,其中bitmaps,HyperLogLong底层是基于String,Geo则是基于zset。
string
介绍:
字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如 JSON 、XML))、数字(整数、 浮点数 ),甚至是 二进制 (图片、音频、视频),但是值最大不能超过512MB。
使用场景:
字符串类型的使⽤场景很广泛:
缓存功能
Redis 作为缓存层,MySQL作为存储层,绝⼤部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
计数
使⽤Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。
共享Session
一个分布式Web服务将用户的Session信息(例如⽤户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
为了解决这个问题,可以使用Redis将用户的 Session 进行集中管理,,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
限速
比如,很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接⼝不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。一些网站限制一个IP地址不能在一秒钟之内访问超过n次也可以采用类似的思路。
list
介绍:
list 即是 链表
列表( list)类型是用来存储多个有序的字符串,a、b、c、d、e五个元素从左到右组成了一个有序列表,列表中的每个字符串称为元素(element)
列表类型有两个特点:第一、列表中的元素是有序的,这就意味着可以通过 索引 下标获取某个元素或者某个范围内的元素列表。第二、列表中的元素可以是重复的。
使用场景:
消息队列,Redis 的 lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使⽤brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
实现其他数据结构
lpush+lpop = Stack(栈)
lpush +rpop = Queue(队列)
lpsh+ ltrim = Capped Collection(有限集合)
lpush+brpop =Message Queue(消息队列)
hash
介绍:hash 类似于 JDK 1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。
使用场景:
哈希类型比较适宜存放对象类型的数据,我们可以比较下,如果数据库中表记录为:
使用String类型:
set user:1:name king;
set user:1:age 18;
set user:1:sex boy;
优点:简单直观,每个键对应一个值
缺点:键数过多,占用内存多,用户信息过于分散,不用于生产环境
将对象序列化存入redis
set user:1 serialize(userInfo);
优点:编程简单,若使用序列化合理内存使用率高
缺点: 序列化 与反序列化有一定开销,更新属性时需要把userInfo全取出来进行反序列化,更新后再序列化到redis
使用hash类型:
hmset user:1 name King age 18 sex boy
优点:简单直观,使用合理可减少内存空间消耗
缺点:要控制内部编码格式,不恰当的格式会消耗更多内存
set
介绍:set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元
素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择
使用场景:
set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set轻易实现交集、并集、 差集 的操作。
集合类型比较典型的使⽤场景是标签( tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。
例如一个电子商务的⽹站会对不同标签的用户做不同类型的推荐,比如对 数码产品 比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。除此之外,集合还可以通过生成随机数进行比如抽奖活动,以及社交图谱等等。
zset
介绍:
有序集合相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数( score)作为排序的依据。
有序集合中的元素不能重复,但是score可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。
有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
使用场景:
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
bitmap
介绍:
现代计算机用二进制(位)作为信息的基础单位,1个字节等于8位,例如“big”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示,“big”分别对应的ASCII码分别是98、105、103,对应的二进制分别是01100010、01101001和 01100111。
Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作。
Bitmaps单独提供了⼀套命令,所以在Redis中使用Bitmaps和使⽤字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在 Bitmaps 中叫做偏移量。
使用场景:适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
Redis 的持久化机制是什么?
Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。
Redis 的持久化机制有两种,第一种是RDB快照,第二种是 AOF日志。
RDB持久化
RDB持久化是将某个时间点上Redis中的数据保存到一个RDB文件中,如下所示
该⽂件是⼀个经过压缩的 二进制文件 ,通过该文件可以还原生成RDB文件时Redis中的数据,如下所示:
Redis提供了2个命令来创建RDB文件,一个是SAVE,另一个是BGSAVE。
因为 BGSAVE命令 可以在不阻塞服务器进程的情况下执行,所以推荐使用BGSAVE命令。
载入RDB文件
载入 RDB文件 的目的是为了在Redis服务器进程重新启动之后还原之前存储在Redis中的数据。然后,Redis载入RDB文件并没有专门的命令,而是在Redis服务器启动时自动执行的。而且,Redis服务器启动时是否会载入RDB文件还取决于服务器是否启用了AOF持久化功能,具体判断逻辑为:
- 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据
- 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据。
AOF持久化
AOF 持久化 是通过保存Redis服务器所执行的写命令来记录数据库数据的,如下图所示
默认情况下,AOF持久化功能是关闭的,如果想要打开,可以修改配置。
载入AOF文件
因为AOF文件包含了重建数据库所需的所有写命令,所以Redis服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原Redis服务器关闭之前的数据。
Redis读取AOF文件并还原数据库的详细步骤如下:
1. 创建一个不带⽹络连接的伪客户端
(因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有⽹络连接的伪客户端来执行AOF文件保存的写命令。伪客户端执行命令的效果和带网络连接的客户端执⾏命令的效果完全一样)
2. 从AOF文件中分析并读取出一条写命令。
3. 使用伪客户端执行被读取出的写命令。
4. 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被执行完毕。
以上步骤如下图所示:
RDB持久化、AOF持久化的区别
实现方式
RDB持久化是通过将某个时间点Redis服务器存储的数据保存到RDB文件中来实现持久化的。AOF持久化是通过将Redis服务器执行的所有写命令保存到AOF文件中来实现持久化的。
文件体积
由上述实现方式可知,RDB持久化记录的是结果,AOF持久化记录的是过程,所以AOF持久化生成的AOF文件会有体积越来越大的问题,Redis提供了AOF重写功能来减小AOF文件体积。
安全性
AOF持久化的安全性要⽐RDB持久化的安全性高,即如果发生机器故障,AOF持久化要比RDB持久化丢失的数据要少。
因为RDB持久化会丢失上次RDB持久化后写入的数据,而AOF持久化最多丢失1s之内写入的数据(使用默认everysec配置的话)。
优先级
由于上述的安全性问题,如果Redis服务器开启了AOF持久化功能,Redis服务器在启动时会使用AOF文件来还原数据,如果Redis服务器没有开启AOF持久化功能,Redis服务器在启动时会使用RDB文件来还原数据,所以AOF文件的优先级比RDB文件的优先级高。
如何保证缓存与数据库双写时的数据一致性?
什么是数据一致性问题?
只要使用到缓存,无论是本地内存做缓存还是使用 redis 做缓存,那么就会存在数据同步的问题。
更新缓存方案
1、先更新缓存,再更新DB
这个方案我们一般不考虑。原因是更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在
2 . 先更新DB,再更新缓存
这个方案也我们一般不考虑,原因跟第一个一样,数据库更新成功了,缓存更新失败,同样会出现数据不一致问题。
这种更新缓存的方式还有并发问题。
同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
删除缓存方案
3 、先删除缓存,后更新DB
该方案也会出问题,具体出现的原因如下。
此时来了两个请求,请求 A(更新操作)和请求 B(查询操作)
请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作;
此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中;
但是此时请求 A 并没有更新成功,或者事务还未提交,请求B去数据库查询得到旧值;
那么这时候就会产生数据库和 Redis 数据不一致的问题。如何解决呢?其实最简单的解决办法就是延时双删的策略。就是
(1)先淘汰缓存
(2)再写数据库
(3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
4 、先更新DB,后删除缓存
这种方式,被称为Cache Aside Pattern,读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。
这种情况不存在并发问题么?
依然存在。假设这会有两个请求,⼀个请求A做查询操作,⼀个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写⼊数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写⼊缓存
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离⼲嘛,做 读写分离 的意义就是因为读操作比较快,耗资源少),因此步(3)耗时比步骤(2)更短,这一情形很难出现。一定要解决怎么办?如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。
其次,采用异步延时删除策略。
但是,更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功怎么办?这个问题,在删除缓存类的方案都是存在的,那么此时再读取缓存的时候每次都是错误的数据了。
此时解决方案有两个,一是就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:
1、请求 A 先对数据库进行更新操作
2、在对 Redis 进行删除操作的时候发现报错,删除失败
3、此时将Redis 的 key 作为消息体发送到消息队列中
4、系统接收到消息队列发送的消息后
5、再次对 Redis 进行删除操作
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
订阅binlog程序在mysql中有现成的中间件叫canal,阿里开源的,大家可以自行查阅官网,可以完成订阅binlog日志的功能。
什么是缓存穿透?怎么解决
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
接口层增加校验,如用户 鉴权 校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
布隆过滤器
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力
1970 年布隆提出了一种布隆过滤器的算法,用来判断一个元素是否在一个集合中。
这种算 法由一个二进制数组和一个 Hash 算法组成。
选择多个 Hash 函数 计算多个 Hash 值,这样可以减少误判的几率