ES
简介
- 一个高扩展、开源的全文检索和分析引擎,它可以准实时地快速存储、搜索、分析海量的数据。
- 全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索搜索引擎数据库中的数据
ES为什么比mysql快
Mysql只有term dictionary这一层,是以b-tree排序的方式存储在磁盘上的。检索一个term需要若干次的random access的磁盘操作。而Lucene在term dictionary的基础上添加了term index来加速检索,term index以树的形式缓存在内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘的random access次数。另外:term index在内存中是以FST(finite state transducers)的形式保存的,其特点是非常节省内存。Term dictionary在磁盘上是以分block的方式保存的,一个block内部利用公共前缀压缩,比如都是Ab开头的单词就可以把Ab省去。这样term dictionary可以比b-tree更节约磁盘空间。
同步数据库
我们采取MySQL的数据存储,利用MySQL的事务特性维护数据一致性,使用ElasticSearch进行数据汇集和查询,此时es与数据库的同步方案就尤为重要。
流程
首先添加商品入数据库,添加商品成功后,商品入ES,若入ES失败,将失败的商品ID放入redis的缓存队列,且失败的商品ID入log文件(若出现redis挂掉,可从日志中取异常商品ID然后再入ES),task任务每秒刷新一下redis缓存队列,若是从缓存队列中取到商品ID,则根据商品ID从数据库中获取商品数据然后入ES。
使用
logstash-input-jdbc插件同步数据库,安装,配置:创建一个 .conf文件,配置了要同步的数据库和.sql用于执行的sql语句,最后把一个jdbc驱动放到这个文件夹下,用来连接mysql数据库
可能遇到的问题
elasticsearch数据重复以及增量同步
在默认配置下,tracking_column这个值是@timestamp,存在elasticsearch就是_id值,是logstash存入elasticsearch的时间,这个值的主要作用类似mysql的主键,是唯一的,但是我们的时间戳其实是一直在变的,所以我们每次使用select语句查询的数据都会存入elasticsearch中,导致数据重复。
解决方法
在要查询的表中,找主键或者自增值的字段,将它设置为_id的值,因为_id值是唯一的,所以,当有重复的_id的时候,数据就不会重复
数据同步频繁,影响mysql数据库性能
我们写入jdbc.sql文件的mysql语句是写死的,所以每次查询的数据库有很多是已经不需要去查询的,尤其是每次select * from table;的时候,对mysql数据库造成了非常大的压力
解决:
(1)根据业务需求,可以适当修改定时同步时间,我这里对实时性相对要求较高,因此设置了10分钟
schedule => "*/10 * * * *"
(2)设置mysql查询范围,防止大量的查询拖死数据库,在sql语句这里设置select * from WHERE autoid > :sql_last_value;
elasticsearch存储容量不断上升
elasticsearch为了数据安全,接收到数据后,先将数据写入内存和translog,然后再建立索引写入到磁盘,这样即使突然断电,重启后,还可以通过translog恢复,不过这里由于我们每次查询都有很多重复的数据,而这些重复的数据又没有写入到elasticsearch的索引中,所以就囤积了下来,导致elasticsearch容量就不断上升
解决:
查询官网说会定期refresh,会自动清理掉老的日志,因此可不做处理
增量同步和mysql范围查询导致mysql数据库有修改时无法同步到以前的数据。
解决了mysql每次都小范围查询,解决了数据库压力的问题,不过却导致无法同步老数据的修改问题
解决:
可根据业务状态来做,如果你数据库是修改频繁类型,那只能做全量更新了,但是高频率大范围扫描数据库来做的索引还不如不做索引了(因为建立索引也是有成本的),我们做索引主要是针对一些数据量大,不常修改,很消耗数据库性能的情况。我这里是数据修改较少,而且修改也一般是近期数据,因为同步时,我在mysql范围上面稍微调整一下
php使用ES
- php composer安装composer require elasticsearch/elasticsearch
- 引入es文件autoload.php文件,设置IP地址
- 创建index,index 对应关系型数据(以下简称MySQL)里面的数据库,而不是对应MySQL里面的索引
- 有了数据库还不行,还需要建立表,ES也是一样的,ES中的type对应MySQL里面的表。type不是单独定义的,而是和字段一起定义,字段定义在body中;当然可以在body字段中也能使用ik分词;
- 使用EsClient->search()实现搜索;
同义词和近义词的使用
- 配置分词器:配置IK
- 下载es的ik版本包
- 在es目录下的plugins在创建
ik
目录,把下载ik的zip包所有文件解压进去。- 进去es的config目录,编辑
elasticsearch.yml
,在空白地方加上index.analysis.analyzer.default.type : "ik"
即可。- 拼音分词器配置:使用已经编译好的:elasticsearch-analysis-pinyin-1.3.0
- 在elasticsearch的plugins目录下,新建analysis-pinyin文件夹,解压压缩包,将里面的jar包放到analysis-pinyin文件夹。
- 在elasticsearch.yml里面配置拼音分词器的过滤器
- 同义词分词器配置
- 在elasticsearch.yml里面配置好同义词分词器的过滤器
- 配置同义词词库,在elasticsearch的config目录下新建sysnonym.txt。
- 配置ik+pinying+同义词的分词器,主要有分词器的名称,类型,分割词元的组件,对分割的次元做处理:这里使用的是拼音和同义词
ES 关键字高亮显示
ES 通过在查询的时候可以在查询之后的字段数据加上html 标签字段,使文档在在web 界面上显示的时候是由颜色或者字体格式的,是在highlight修饰高亮字段, 这个部分包含了 name 属性匹配的文本片段,并以 HTML 标签 封装
ES查询分页
Elasticsearch中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回。
一般查询流程为
1)客户端请求发给某个节点
2)节点转发给个个分片,查询每个分片上的前10条
3)结果返回给节点,整合数据,提取前10条
4)返回给请求客户端
当我们查询第10条到第20条的数据时,有两种方式,包括深度分页(from-size)和快照分页(scroll);
深度分页(from-size)
from定义了目标数据的偏移值,size定义当前返回的事件数目。默认from为0,size为10,也就是说所有的查询默认仅仅返回前10条数据。查询前20条数据,然后截断前10条,只返回10-20的数据。浪费了前10条的查询。越往后的分页,执行的效率越低。分页的偏移值越大,执行分页查询时间就会越长
快照分页(scroll)
相对于from和size的分页来说,使用scroll可以模拟一个传统数据的游标,记录当前读取的文档信息位置。这个分页的用法,不是为了实时查询数据,而是为了一次性查询大量的数据(甚至是全部的数据)。因为这个scroll相当于维护了一份当前索引段的快照信息,这个快照信息是你执行这个scroll查询时的快照。在这个查询后的任何新索引进来的数据,都不会在这个快照中查询到。但是它相对于from和size,不是查询所有数据然后剔除不要的部分,而是记录一个读取的位置,保证下一次快速继续读取。
流程:
调用: `index/type/_search?pretty&scroll=2m`,返回一个scroll值
直接用scroll_id进行查询。
清除scroll,我们在设置开启scroll时,设置了一个scroll的存活时间,但是如果能够在使用完顺手关闭,可以提早释放资源,降低ES的负担
redis
简介
- 它支持多种类型的数据结构,如字符串(String),散列(Hash),列表(List),集合 (Set),有序集合(Sorted Set或者是ZSet)与范围查询,Bitmaps,Hyperloglogs 和 地理空间(Geospatial)索引半径查询。其中常见的数据结构类型有:String、List、Set、 Hash、ZSet这5种。
Redis 的对象系统中包括字符串对象(String)、列表对象(List)、哈希对象(Hash)、集合对象(Set)和有序集合对象(ZSet)- Redis 内置了复制(Replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(Transactions) 和不同级别的磁盘持久化(Persistence),并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。
- Redis也提供了持久化的选项,这些选项可以让用户将自己的数据保存到磁盘上面进行存 储。根据实际情况,可以每隔一定时间将数据集导出到磁盘(快照),或者追加到命令日志 中(AOF只追加文件),他会在执行写命令时,将被执行的写命令复制到硬盘里面。您也可 以关闭持久化功能,将Redis作为一个高效的网络的缓存数据功能使用。
- Redis不使用表,他的数据库不会预定义或者强制去要求用户对Redis存储的不同数据进行 关联。
数据库的工作模式按存储方式可分为:硬盘数据库和内存数据库。Redis 将数据储存在内存 里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度极快。
redis,memcache
- 数据结构:Memcache仅能支持简单的K-V形式,Redis支持的数据更多
- 多线程:Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的IO复用模型。
- 持久化:Redis支持持久化,Memcache不支持持久化
- 分布式:Redis做主从结构,而Memcache服务器需要通过hash一致化来支撑主从结构
- 虚拟内存:Redis当物理内存使用完时,会将一些很久没有用的内存交换到磁盘,而Memcache采取的LUR策略,将一部分数据刷新叼
- 事务:redis支持事务
保持 redis和mysql 数据库的数据一致性
MySQL持久化数据,Redis只读数据
redis在启动之后,从数据库加载数据。
读请求:
不要求强一致性的读请求,走redis,要求强一致性的直接从mysql读取
写请求:
数据首先都写到数据库,之后更新redis(先写redis再写mysql,如果写入失败事务回滚会造成redis中存在脏数据)
MySQL和Redis处理不同的数据类型
- MySQL处理实时性数据,例如金融数据、交易数据
- Redis处理实时性要求不高的数据,例如网站最热贴排行榜,好友列表等
- 在并发不高的情况下,读操作优先读取redis,不存在的话就去访问MySQL,并把读到的数据写回Redis中;写操作的话,直接写MySQL,成功后再写入Redis(可以在MySQL端定义CRUD触发器,在触发CRUD操作后写数据到Redis,也可以在Redis端解析binlog,再做相应的操作)
- 在并发高的情况下,读操作和上面一样,写操作是异步写,写入Redis后直接返回,然后定期写入MySQL
例子
当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。
解决方法
这种情况应该是先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。
问题
在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况
解决方法
- 可以用队列的去解决这个问,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。
- 这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。
为什么Redis是单线程的
- 官方FAQ表示, 因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的 大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单 线程的方案
- 我们已经可以很清楚的解释了为什么Redis这么快,并且正是由于在单 线程模式的情况下已经很快了,就没有必要在使用多线程了! 但是,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个 Redis 实例来完善!
注意:
- 这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来 处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,例如Redis进行持久化的时候会以子进程或者子线程的方式执
- 我们知道Redis是用”单线程-多路复用IO模型”来实现高性能的内存数据服务的,这种 机制避免了使用锁,但是同时这种机制在进行sunion之类的比较耗时的命令时会使redis的 并发下降。因为是单一线程,所以同一时刻只有一个操作在进行,所以,耗时的命令会导致 并发的下降,不只是读并发,写并发也会下降。而单一线程也只能用到一个CPU核心,所以 可以在同一个多核的服务器中,可以启动多个实例,组成master-master或者master-slave 的形式,耗时的读命令可以完全在slave进行。
- “我们不能任由操作系统负载均衡,因为我们自己更了解自己的程序,所以,我们可以 手动地为其分配CPU核,而不会过多地占用CPU,或是让我们关键进程和一堆别的进程挤在 一起。CPU 是一个重要的影响因素,由于是单线程模型,Redis 更喜欢大缓存快速 CPU, 而不是 多核 在多核 CPU 服务器上面,Redis 的性能还依赖NUMA 配置和处理器绑定位置。最明显的影 响是 redis-benchmark 会随机使用CPU内核。为了获得精准的结果,需要使用固定处理器 工具(在 Linux 上可以使用 taskset)。最有效的办法是将客户端和服务端分离到两个不同 的 CPU 来高校使用三级缓存。
redis为什么快
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库, 由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据 不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差!
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致 的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出 现死锁而导致的性能消耗;
- 使用多路I/O复用模型,非阻塞IO;
- 多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在 空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤 醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只 依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
- 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用 技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈, 主要由以上几点造就了 Redis 具有很高的吞吐量。
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样, Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去 移动和请求;
多路 I/O 复用模型
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在 空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤 醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只 依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用 技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈, 主要由以上几点造就了 Redis 具有很高的吞吐量。
数据结构
简单动态字符串
- Redis 使用动态字符串 SDS 来表示字符串值。
- SDS 的结构可以减少修改字符串时带来的内存重分配的次数,这依赖于内存预分配和惰性空间释放两大机制。
- 当 SDS 需要被修改,并且要对 SDS 进行空间扩展时,Redis 不仅会为 SDS 分配修改所必须要的空间,还会为 SDS 分配额外的未使用的空间。
- 如果修改后, SDS 的长度(也就是len属性的值)将小于 1MB ,那么 Redis 预分配和 len 属性相同大小的未使用空间。
- 如果修改后, SDS 的长度将大于 1MB ,那么 Redis 会分配 1MB 的未使用空间。
- 比如说,进行修改后 SDS 的 len 长度为20字节,小于 1MB,那么 Redis 会预先再分配 20 字节的空间, SDS 的 buf数组的实际长度(除去最后一字节)变为 20 + 20 = 40 字节。当 SDS的 len 长度大于 1MB时,则只会再多分配 1MB的空间。
- 类似的,当 SDS 缩短其保存的字符串长度时,并不会立即释放多出来的字节,而是等待之后使用。
链表
- 链表在 Redis 中的应用非常广泛,比如列表对象的底层实现之一就是链表。除了链表对象外,发布和订阅、慢查询、监视器等功能也用到了链表。
- 链表在 Redis 中的应用非常广泛,比如列表对象的底层实现之一就是链表。除了链表对象外,发布和订阅、慢查询、监视器等功能也用到了链表。
- Redis 的链表结构的dup 、 free 和 match 成员属性是用于实现多态链表所需的类型特定函数:
- dup 函数用于复制链表节点所保存的值,用于深度拷贝。
- free 函数用于释放链表节点所保存的值。
- match 函数则用于对比链表节点所保存的值和另一个输入值是否相等。
字典
- 字典被广泛用于实现 Redis 的各种功能,包括键空间和哈希对象
- Redis 使用 MurmurHash2 算法来计算键的哈希值,并且使用链地址法来解决键冲突,被分配到同一个索引的多个键值对会连接成一个单向链表。
跳跃表
Redis 使用跳跃表作为有序集合对象的底层实现之一。它以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美 —— 查找、删除、添加等操作都可以在对数期望时间下完成, 并且比起平衡树来说, 跳跃表的实现要简单直观得多。
整数集合
整数集合 intset 是集合对象的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合对象的底层实现。
压缩列表
- 压缩队列 ziplist 是列表对象和哈希对象的底层实现之一。当满足一定条件时,列表对象和哈希对象都会以压缩队列为底层实现。
- 压缩队列是 Redis 为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。它的属性值有:
- zlbytes : 长度为 4 字节,记录整个压缩数组的内存字节数。
- zltail : 长度为 4 字节,记录压缩队列表尾节点距离压缩队列的起始地址有多少字节,通过该属性可以直接确定尾节点的地址。
- zllen : 长度为 2 字节,包含的节点数。当属性值小于 INT16_MAX时,该值就是节点总数,否则需要遍历整个队列才能确定总数。
- zlend : 长度为 1 字节,特殊值,用于标记压缩队列的末端。
- 中间每个节点 entry 由三部分组成:
- previousentrylength : 压缩列表中前一个节点的长度,和当前的地址进行指针运算,计算出前一个节点的起始地址。
- encoding: 节点保存数据的类型和长度
- content :节点值,可以为一个字节数组或者整数。
对象
字符串对象
- 如果一个字符串对象保存的是一个字符串值,并且长度大于32字节,那么该字符串对象将使用 SDS 进行保存,并将对象的编码设置为 raw。如果字符串的长度小于32字节,那么字符串对象将使用embstr 编码方式来保存。
- embstr 编码是专门用于保存短字符串的一种优化编码方式,这个编码的组成和 raw 编码一致,都使用 redisObject 结构和 sdshdr 结构来保存字符串。
- 但是 raw 编码会调用两次内存分配来分别创建上述两个结构,而embstr则通过一次内存分配来分配一块连续的空间,空间中一次包含两个结构。
- embstr 只需一次内存分配,而且在同一块连续的内存中,更好的利用缓存带来的优势,但是 embstr 是只读的,不能进行修改,当一个 embstr 编码的字符串对象进行 append 操作时, redis 会现将其转变为 raw 编码再进行操作。
列表对象
- 列表对象的编码可以是 ziplist 或 linkedlist。
- 当列表对象可以同时满足以下两个条件时,列表对象使用 ziplist 编码:
- 列表对象保存的所有字符串元素的长度都小于 64 字节。
- 列表对象保存的元素数量数量小于 512 个。
- 不能满足这两个条件的列表对象需要使用 linkedlist 编码或者转换为 linkedlist 编码。
哈希对象
- 哈希对象的编码可以使用 ziplist 或 dict
- 当哈希对象使用压缩队列作为底层实现时,程序将键值对紧挨着插入到压缩队列中,保存键的节点在前,保存值的节点在后。
- 当哈希对象可以同时满足以下两个条件时,哈希对象使用 ziplist 编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
- 哈希对象保存的键值对数量小于512个。
- 不能满足这两个条件的哈希对象需要使用 dict 编码或者转换为 dict 编码。
集合对象
- 集合对象的编码可以使用 intset 或者 dict。
- intset 编码的集合对象使用整数集合最为底层实现,所有元素都被保存在整数集合里边。
- 而使用 dict 进行编码时,字典的每一个键都是一个字符串对象,每个字符串对象就是一个集合元素,而字典的值全部都被设置为NULL。
- 当集合对象可以同时满足以下两个条件时,对象使用 intset 编码:
- 集合对象保存的所有元素都是整数值。
- 集合对象保存的元素数量不超过512个。
- 否则使用 dict 进行编码。
有序集合对象
- 有序集合的编码可以为 ziplist 或者 skiplist。
- 有序集合使用 ziplist 编码时,每个集合元素使用两个紧挨在一起的压缩列表节点表示,前一个节点是元素的值,第二个节点是元素的分值,也就是排序比较的数值。
- 压缩列表内的集合元素按照分值从小到大进行排序
- 有序集合使用 skiplist 编码时使用 zset 结构作为底层实现,一个 zet 结构同时包含一个字典和一个跳跃表。
- 其中,跳跃表按照分值从小到大保存所有元素,每个跳跃表节点保存一个元素,其score值是元素的分值。而字典则创建一个一个从成员到分值的映射,字典的键是集合成员的值,字典的值是集合成员的分值。通过字典可以在O(1)复杂度查找给定成员的分值。
- 跳跃表和字典中的集合元素值对象都是共享的,所以不会额外消耗内存。
- 当有序集合对象可以同时满足以下两个条件时,对象使用 ziplist 编码:
- 有序集合保存的元素数量少于128个;
- 有序集合保存的所有元素的长度都小于64字节。
- 否则使用 skiplist 编码。
数据库键空间
- Redis 服务器都有多个 Redis 数据库,每个Redis 数据都有自己独立的键值空间。每个 Redis 数据库使用 dict 保存数据库中所有的键值对。
- 键空间的键也就是数据库的键,每个键都是一个字符串对象,而值对象可能为字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的一种对象。
- 除了键空间,Redis 也使用 dict 结构来保存键的过期时间,其键是键空间中的键值,而值是过期时间。
- 通过过期字典,Redis 可以直接判断一个键是否过期,首先查看该键是否存在于过期字典,如果存在,则比较该键的过期时间和当前服务器时间戳,如果大于,则该键过期,否则未过期。
五种数据类型
string
这个其实没啥好说的,常规的set/get操作,value可以是String也可以是数 字。一般做一些复杂的计数功能的缓存。
hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主 在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为 key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以 利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用 一个场景,很合适---取行情信息。就也是个生产者和消费者的场景。LIST可以 很好的完成排队,先进先出的原则。
set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自 己独有的喜好等功能。
sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以 做排行榜应用,取TOP N操作。
redis事务
众多其它数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。在Redis中,MULTI/EXEC/DISCARD/WATCH这四个命令是我们实现事务的基石。相信对有关系型数据库开发经验的开发者而言
Redis中事务的实现特征:
1). 在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。
2). 和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。
3). 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为"BEGIN TRANSACTION"语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句。
4). 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。
5). 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。
Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。
redis主从
通过配置两台(或多台)数据库的主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。redis 支持 master-slave(主从)模式,redisserver 可以设置为另一个 redis server 的主机(从机),从机定期从主机拿数据。特殊的,一个从机同样可以设置为一个 redis server 的主机,master以写为主,slaver以读为主。
流程
* 准备好设置主从的redis服务器
* 修改配置文件
* 修改redis-master的配置文件 redis.windows.confport 6379
bind 127.0.0.1 2.2
* 修改redis-slave1 和redis-slave2的配置文件
* redis-slave1的配置文件 port 6380
bind 127.0.0.1
slaveof 127.0.0.1 6379
* redis-slave2的配置文件 port 6381
bind 127.0.0.1
slaveof 127.0.0.1 6379
* 检查
redis 哨兵
有了主从复制的实现以后,如果我们想对主从服务器进行监控,那么在redis2.6以后提供了一个"哨兵"的机制。在2.6版本中的哨兵为1.0版本,并不稳定,会出现各种各样的问题,在2.8版本以后的哨兵功能才稳定起来。顾名思义,哨兵的含义就是监控Redis系统的运行状态。可以启动多个哨兵,去监控Redis数据库的运行状态。
其主要功能有两点
- 监控主数据库和从数据库是否正常运行。
- 主数据库出现故障时,可以自动将从数据库转换为主数据库,实现自动切换。
配置
1. port :当前Sentinel服务运行的端口
2.sentinel monitor mymaster 127.0.0.1 6379 2:Sentinel去监视一个名为mymaster的 主redis实例,这个主实例的IP地址为本机地址127.0.0.1,端口号为6379,而将这个主实例 判断为失效至少需要2个 Sentinel进程的同意,只要同意Sentinel的数量不达标,自动 failover就不会执行
3.sentinel down-after-milliseconds mymaster 5000:指定了Sentinel认为Redis实例已 经失效所需的毫秒数。当 实例超过该时间没有返回PING,或者直接返回错误,那么 Sentinel将这个实例标记为主观下线。只有一个 Sentinel进程将实例标记为主观下线并不一 定会引起实例的自动故障迁移:只有在足够数量的Sentinel都将一个实例标记为主观下线之 后,实例才会被标记为客观下线,这时自动故障迁移才会执行
4.sentinel parallel-syncs mymaster 1:指定了在执行故障转移时,最多可以有多少个 从Redis实例在同步新的主实例,在从Redis实例较多的情况下这个数字越小,同步的时间 越长,完成故障转移所需的时间就越长
5.sentinel failover-timeout mymaster 15000:如果在该时间(ms)内未能完成 failover操作,则认为该failover失败
作用
1.当启动哨兵模式之后,如果你的master服务器宕机之后,哨兵自动会在从redis服务器里面 投票选举一个master主服务器出来;这个主服务器也可以进行读写操作!
2.如果之前宕机的主服务器已经修好,可以正式运行了。那么这个服务器只能进行读的操作,会自动跟随由哨兵选举出来的新服务器!
特点
- master可以有多台slave
- 除了多个slave连到相同master外,slave也可以连接到其它slave形成图状结构
- 主从复制不会阻塞master,也就是说当一个或多个slave与master连接进行复制时,master可以继续处理客户端发来的请求
- 主从复制可以用来提高系统的伸缩性,我们可以用多个slave专门负责客户端的读请求,可以做数据冗余
redis持久化
redis持久化的几种方式
1、快照(snapshots)
缺省情况情况下,Redis把数据快照存放在磁盘上的二进制文件中,文件名为dump.rdb。你可以配置Redis的持久化策略,例如数据集中每N秒钟有超过M次更新,就将数据写入磁盘;或者你可以手工调用命令SAVE或BGSAVE。
工作原理
- Redis forks.
- 子进程开始将数据写到临时RDB文件中。
- 当子进程完成写RDB文件,用新文件替换老文件。
- 这种方式可以使Redis使用copy-on-write技术。
2、AOF
- 快照模式并不十分健壮,当系统停止,或者无意中Redis被kill掉,最后写入Redis的数据就会丢失。这对某些应用也许不是大问题,但对于要求高可靠性的应用来说,
- Redis就不是一个合适的选择。
- Append-only文件模式是另一种选择。
- 你可以在配置文件中打开AOF模式
3、虚拟内存方式
- 当你的key很小而value很大时,使用VM的效果会比较好.因为这样节约的内存比较大.
- 当你的key不小时,可以考虑使用一些非常方法将很大的key变成很大的value,比如你可以考虑将key,value组合成一个新的value.
- vm-max-threads这个参数,可以设置访问swap文件的线程数,设置最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的.可能会造成比较长时间的延迟,但是对数据完整性有很好的保证.
RDB机制
RDB机制的优势和劣势
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。 也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。可以通过配置设置自动做快照持久化的方式
RDB文件保存过程
- redis调用fork,现在有了子进程和父进程。
- 父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数 据是fork时刻整个数据库的一个快照。
- 当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出。
- client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求。所以不推荐使用。
- 另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不 是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。
优势
- 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这样非常方便进行备份。比如你可能打算没1天归档一些数据。
- 方便备份,我们可以很容易的将一个一个RDB文件移动到其他的存储介质
- RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
劣势
- 如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
- 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。
AOF机制
redis会将每一个收到的写命令都通过write函数追加到文件中(默认是 appendonly.aof)。
当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要 通过fsync函数强制os写入到磁盘的时机。
aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。
为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下
- redis调用fork ,现在有父子两个进程
- 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
- 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
- 当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
- 现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。
需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
优势
- 使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。
- AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
劣势
对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
- 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
- AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。
对比
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。其余情况我个人喜好选择AOF
Sentinel
Sentinel的作用:
- Master 状态监测
- 如果Master 异常,则会进行Master-slave 转换,将其中一个Slave作为Master,将之前的Master作为Slave
- Master-Slave切换后,master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,即master_redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换
2、Sentinel的工作方式:
1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。 3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。
5):在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 。
6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 。
7):若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
Redis集群方案
集群过程
1:当一个从数据库启动时,会向主数据库发送sync命令,
2:主数据库接收到sync命令后会开始在后台保存快照(执行rdb操作),并将保存期间接收到的命令缓存起来
3:当快照完成后,redis会将快照文件和所有缓存的命令发送给从数据库。
4:从数据库收到后,会载入快照文件并执行收到的缓存的命令
集群方案应该怎么做?都有哪些方案?
Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接
1、所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
2、节点的fail是通过集群中超过半数的节点检测失效时才生效。
3、客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
4、redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。
5、Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
Redis集群方案什么情况下会导致整个集群不可用?
有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用
Redis集群最大节点个数是多少?
一个redis集群的搭建,最少需要6个节点,构成3组服务节点;每组服务节点包括两个节点(Master-Slave)
redis的过期策略以及内存淘汰机制
假如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这 个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占 用率还是比较高,有思考过原因么?
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放, 但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不 是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
- 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需 要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行 检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用 定期删除策略,会导致很多key到时间没有删除。
- 于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一 下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性 删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机 制。
在redis.conf中有一行配置# maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除近少 使用的key。推荐使用,目前项目在用这种。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除 某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间 中,移除近少使用的key。这种情况一般是把redis既当缓存,又做持久化存 储的时候才用。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的 键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间 中,有更早过期时间的key优先移除。
如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
redis和数据库双写一致性问题
一致性问题是分布式常见问题,还可以再分为终一致性和强一致性。数 据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。 就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证终 一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概 率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除 缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
并发竞争key问题
同时有多个子系统去set一个key。因为我们的生产环 境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个 key操作的时候,这多个key不一定都存储在同一个redis-server上。所以, redis的事务机制,并不好。
(1)如果对这个key操作,不要求顺序,这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较 简单。
(2)如果对这个key操作,要求顺序,假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为 valueB,系统C需要将key1设置为valueC.。期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时 候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下
系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢 到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。
其他方法,比如利用队列,将set方法变成串行访问也可以。
Redis内存满了的几种解决方法
1,增加内存;
2,使用内存淘汰策略。
3,Redis集群。
缓存穿透,雪崩,预热,更新,降级
缓存雪崩
由于原有缓存失效,新缓存未到期间(例如:我们设置缓 存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存 的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕 机。从而形成一系列连锁反应,造成整个系统崩溃。
缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列 的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发 请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件。
加锁排队
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建 期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这 是个治标不治本的方法!
注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程 还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!
缓存标记
缓存标记记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际 key的缓存,给每一个缓存数据增加相应的缓存标记,记录缓存的 是否失效,如果缓存标记失效,则更新数据缓存
缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际 key的缓存;
缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据 缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用 端,直到另外的线程在后台更新完成后,才会返回新缓存。
做二级缓存
做二级缓存,或者双缓存策略: A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期
缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询 的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次 无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
布隆过滤器
将所有可能存 在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉, 从而避免了对底层存储系统的查询压力。
对空结果缓存
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还 是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分 钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继 续访问数据库,这种办法最简单粗暴!把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的 值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行 预先校验,然后再放行给后面的正常缓存处理逻辑。
缓存预热
缓存 预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求 的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可 以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
- 定时去清理过期的缓存;
- 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系 统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次 用户请求过来都要判断缓存失效,逻辑相对比较复杂!根据应用场景来权衡。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性 能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自 动降级,也可以配置开关实现人工降级。
目的
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入 购物车、结算)。在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓 死保护,哪些可降级;比如可以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级 或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系 统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
过期键删除策略
- 如果一个键过期了,那么他什么时候会被删除
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来 临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间获取键时,都检测获取得的键是否过 期,如果过期的话,就删除该键,如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。
- 至于要 删除多少过期键,以及要检查多少数据库,由程序算法决定。 这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
Redis的过期键删除策略
Redis服务器实际使用的是惰 性删除和定期删除两种策略,通过配合使用者两种策略,服务器可以很好的合理的使用CPU 和避免浪费内存空间之间取得平衡。
惰性删除策略的实现
- 过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有的读写数据库的Redis命 令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
- 如果输入键已经过期,那么expireIfNeeded函数将做输入键从数据库中删除。
- 如果键未过期,那么expiredNeeded函数不做任何操作。
AOF,RDB和复制功能对过期键的处理
生成RDB文件
- 在执行Save命令或者bgsave命令创建一个新的RDB文件时,程序会对数据库中的键进行检 查,已过期的键不会被保存到新创建的RDB文件中。
- 例如:如果数据库中包含三个键,k1,k2,k3.并且k2已经过期,那么当执行Save命令或者 bgsave命令时,程序只将k1,k3的数据保存到RDB文件中,而K2则会被忽略。
- 因此,数据库中包含过期键不会对生成新的RDB文件造成影响。
载入RDB文件
- 在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器对RDB文件进行载入 如果服务器以主服务器模式运行,那么载入RDB文件时,程序会对文件中保存的键进行检 查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入RDB文件的 主服务器不会造成影响。
- 如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,不乱是否 过期,都会被载入到数据库中,不过因为主从服务器在同步数据的时候,从服务器的数据库 就会被清空。所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
AOF文件写入
- 当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删 除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。
- 当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显式地记 录该键已被删除。
- 和生成RDB文件类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过 期的键不会被保存到重写的AOF文件中。
网友评论