Redis原理及实战

James 2019年12月23日 175次浏览

初识Redis

Redis可以看成是NoSQL数据库,也可以看成是缓存中间件,Redis缺省有16(分别是0-15)个库,每个库中包含多个key,每个key对应的数据类型有五种,分别是String、List、Hash、Set、SortSet

安装

  • 下载 注意:第二位数字为偶数则代表稳定版,奇数为非稳定版,一般安装到/usr/local/redis目录下
  • 解压:tar -zxvf redis-5.0.7.tar.gz
  • 进入Redis目录:cd redis-5.0.7
  • 编译:make 注意:这里或许会报错,根据提示安装依赖的库即可解决,例如gcc,如果出现如下错误zmalloc.h:50:31: 致命错误:jemalloc/jemalloc.h:没有那个文件或目录,执行make MALLOC=libc即可。
  • 测试编译:make test,注意:这里可能会报错You need tcl 8.5 or newer in order to run the Redis test,直接安装tcl即可yum install -y tcl
  • 安装:cd src && make install {PREFIX=/path}
  • 将命令copy到/usr/local/bin下面
[root@MiWiFi-R3L-srv ~]# cp /usr/local/redis/bin/redis-server /usr/local/bin/
[root@MiWiFi-R3L-srv ~]# cp /usr/local/redis/bin/redis-cli /usr/local/bin/
//将配置文件copy到/etc目录下
[root@MiWiFi-R3L-srv ~]# cp /env/downloads/redis-5.0.5/redis.conf /etc/

如果安装过程中出现编译错误,一定是少了依赖造成的,可以尝试如下指令进行升级,参考链接

yum -y install centos-release-scl

yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils

scl enable devtoolset-9 bash

Redis数据类型

Redis支持五种数据类型,分别是String、List、Hash、Set、SortSet,五种类型的特性如下:

String(字符串)

字符串是Redis中最基本的数据类型,它能存储任何字符数据,例如JSON,Base64编码的图片等,String最大支持存储512M的字符数据。

内部数据结构

String支持三种数据类型,分别是int、浮点数据和字符数据,int数据类型使用int存储,浮点数据和字符数据使用SDS(Simple Dynamic String)存储,SDS是在C的标准字符串结构上作了封装,Redis3.2
有五种sdshdr类型,目的是根据存储的字符串长度选择不同的sdshdr,不同的sdshdr占用的内存大小各有不同,这样就达到了节省内存开销的目的。

List(列表)

List列表是一个有序的字符串列表,由于List底层采用的是双向链表的数据结构,所以不管List列表中的数据有多大,向列表的两端存取数据都是很快的,常用操作也是向列表的两端存取数据。

内部数据结构

在3.2之前,List中元素个数较少或者单个元素长度较小的时候,采用ZipList数据接口存储数据,当List中元素个数或者单个元素长度较大的时候,就会采用LinkedList存储。这两种数据结构各有
优缺点,LinkedList在两端数据的存储复杂度较低,但是内存开销比较大;ZipList内存开销比较小,但是插入和删除需要频繁申请内存。
在3.2之后,Redis在数据存储结构上做了优化,采用QuickList数据结构,QuickList其实是LinkedList和ZipList数据结构的整合,QuickList任然是一个LinkedList,只是每个元素都是一个ZipList,
每个元素都能存储多个数据元素;即QuickList是多个ZipList组成的LinkedList

Hash(可以认为是Java中的Map)

Hash可以看成是Java中的Map,由一个Sting类型的key和多个String类型的field和value组成。适合存储对象。

内部数据结构

Hash底层数据结构可以使用ZipList和HashTable,当Hash中field和value的字符串长度都小于64字节,一个Hash的field和value的个数小于512个时,使用ZipList数据结构存储

Set(集合)

Set存储一个无序不能重复的元素集合,最多可以存储232-1个元素,集合和列表的最大区别就是唯一性和有序性。

内部数据结构

Set底层数据结构有IntSet和HashTable,当所有元素是int类型时,这使用IntSet,否则使用HashTable(只用Key,Value为null)

SortSet(有序集合)

SortSet和Set的区别就是增加了排序功能,在集合的基础上,有序集合为集合中的每个元素绑定了一个score(分数)。有序集合中的元素和集合一样是唯一的,但是元素的score是可以重复的。
我们可以通过score进行排序,查找,删除等操作。

内部数据结构

SortSet采用ZipList或者SkipList+HashTable数据结构存储数据。

Redis过期时间

Redis中可以为一个key设置一个过期时间,当设置了过期时间的key到期过后会被删除。
在Redis中,为某个key设置过期时间有三种方式:

  1. EXPIRE key seconds:为key设置过期时间,单位为妙,返回值1表示设置成功,0表示失败(例如:键不存在)。
  2. PEXPIRE key millis:为key设置过期时间,单位为毫秒
  3. setex key seconds value:该方式为字符串独有,设置key的过期时间,单位为秒
    查看一个key的有效期使用TTL key或者PTTL key,两种方式分别对应EXPIREPEXPIRE两种方式设置的过期时间。如果TTL或者PTTL返回-2,则表示键不存在,-1则表示没有设置过期时间,其他数字
    则表示过期剩余时间。
    如果想让某个设置了过期时间的key恢复成持久的key,可以使用PERSIST key,成功返回1,失败返回0.

过期删除原理

在Redis中,对于已经过期的key的删除有两种方式,如下:

  1. 积极方式:采用随机抽取算法,周期性的对已经设置了过期时间的key随机抽取一批key,将已经过期的key进行删除,该方式有一个缺陷,并不能确保所有过期的key被删除。具体流程如下:
    1. 随机抽取20个带有timeout的key
    2. 将已经过期的key进行删除
    3. 如果被删除(已过期)的key超过抽取总数的25%(5个),则重复执行该操作
  2. 消极方式:当key被访问的时候判断是否过期,如果过期则删除它,该方式有一个缺陷,对于没有被查询到的已经过期的key,会常住内存。
    Redis采用以上两种过期删除方式来互补,达到过期key的删除逻辑。

发布/订阅(publish/subscribe)

Redis提供发布/订阅的功能,可以在多个进程之间进行消息通信。PUBLISH channel.1 message表示向channel.1发送了一条消息,内容为message,该命令返回一个数值,表示订阅了当前channel
的订阅者数量,当返回0的时候表示该channel没有订阅者;订阅者使用SUBSCRIBE channel.1 channel.2 ...订阅channel.1,一个channel可以有多个订阅者,一个消费者也可以订阅多个消息,当发送者发送一条
详细到一个channel,该channel中的所有订阅者都会受到该条消息。需要注意的是:发送到channel中的消息不会持久化,也就是说,订阅者只能收到订阅过后的消息,订阅之前该channel所产生的消息不能收到。
channel可以分为两类,普通的channel和Pattern Channel(规则匹配),例如:现在有两个channel,分别是普通channel abc和Pattern Channel *bc,发送者向abc中发送一条消息PUBLISH abc hello,
首先abc这个channel能收到一条消息hello,*bc也能匹配到abc这个channel,所以*bc也能收到这条消息hello。

Redis数据的持久化

在Redis中,数据的持久化有两种方式

  • RDB:Fork一个子进程根据配置的规则定时的将内存中的数据写入到磁盘中。
  • AOF(Append Only File):每次执行过后将命令追加到AOF文件中,类似于MySQL的binlog。

RDB

当符合RDB持久化条件时,Redis会Fork和主进程一样的子进程,先将内存中的所有数据写入到一个临时文件中,当内存中的所有数据都写入完毕过后,再将之前的备份文件替换。该方式的缺点是最后一次持久化
过后的数据有可能会丢失,也就是说,两次数据的持久化间隔产生的数据有可能丢失。
什么叫符合RDB持久化条件呢?

  • 当满足配置文件的规则时:在redis.conf文件中配置save 900 1,save 300 10,save 60 10000,以上配置,save后面的第一个参数表示时间(单位秒),第二个表示键的个数,并且满足以上任意
    一个配置都会执行,以上配置表示的意思就是:当900秒内有一个键发送变动或者300秒内有10个键发生变动或者60秒内有10000个键发生变动都会触发RDB快照。
  • 客户端发送了SAVE或者BGSAVE命令:当我们需要对Redis服务进行重启的时候,我们可以操作SAVE或者BGSAVE命令手动执行RDB快照,SAVE和BGSAVE命令的区别在于,一个是同步执行,一个是异步执行,
    同步执行会阻塞其他客户端的请求,而BGSAVE则不会阻塞。我们还可以通过LASTSAVE命令来查看最后一次执行RDB快照的时间。
  • 客户端发送了FLUSHALL命令:该操作依赖配置规则,如果没有配置RDB的执行规则,该命令也不会触发RDB快照的执行。
  • 执行复制(Replication):该操作一般指在主从模式下,Redis会在复制初始时执行RDB快照。

AOF

当我们的业务需求对Redis的使用不限于缓存时,可能会使用Redis存储一些比较重要的数据,这个时候我们可以开启AOF来降低RDB持久化方式对内存数据的丢失,当然,开启AOF对Redis对外提供服务的性能
是有一定的影响的,但是这种影响一般能接受,解决办法可以使用一些写入性能较高的磁盘。默认情况下,Redis并没有开启AOF持久化方式,我们可以在配置文件中配置AOF是否开启。appendonly yes
appendonly属性值改为yes,则表示开启AOF持久化。还可以指定AOF持久化到磁盘的文件名称appendfilename "appendonly.aof"
查看appendonly.aof文件,我们可以发现,里面保存了客户端操作Redis的所有事务操作(增删改)命令,但其实有的时候我们对Redis的操作是针对同一个key的,也就是说,其实真正有用的数据是最新
存在于内存中的数据,而AOF持久化文件则保存了各个key的变动轨迹,有很多命令轨迹是没有用的,所以这个时候需要对这样一个问题进行优化。
Redis也考虑到了这一点,我们可以通过配置的方式来解决这一问题,在redis.conf配置文件中配置auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb

  • auto-aof-rewrite-percentage 100:表示当前AOF文件的大小超过上一次重写AOF文件大小的百分之多少时会再次执行重写,如果之前没有重写过,则以启动时AOF文件的大小为准。
  • auto-aof-rewrite-min-size 64mb:表示限制允许重写最小AOF文件的大小。
    当然我们也可以通过手动执行BGREWRITEAOF命令的方式让AOF文件重写。
    AOF方式的数据恢复会一个一个将AOF文件中的命令在Redis服务器上执行,性能上会比RDB方式慢。

AOF重写原理

同样的,为了不影响对外提供服务,AOF重写时主进程会Fork一个子进程来执行,该操作并不是和之前AOF追加的方式,而是类似于RDB的方式,将内存中的数据遍历出来,然后解析成set命令保存到AOF文件中,
在这期间,由于Redis还持续对外提供服务,那么在期间客户端发送的操作执行该如何保证数据同步呢,Redis的解决方案是在执行AOF重写的过程中,主进程接受到的所有客户端的事务操作会缓存到
aof_rewrite_buf缓存(单独开辟一块内存空间来存储执行AOF重写期间收到的客户端命令)中,当重写操作执行完成过后,在将aof_rewrite_buf缓存中将所有命令追加到重写过后的文件中,
当然这个文件也是一个临时文件,当以上操作都执行完毕过后,Redis会把之前旧的AOF文件替换,这样做的好处在于,就算在AOF重写时失败了,也不会影响之前已经持久化的AOF文件。

Redis的内存回收策略

内存是有限且昂贵的,Redis作为一个内存缓存中间件,必须要考虑如何合理有效的使用内存空间。例如:当内存不足时,如何保证Redis程序的正常运行。Redis提供多种内存回收策略,当内存不足时,Redis
为了保证持续的对外提供服务,根据不同的策略淘汰一些对象,来达到Redis的可靠性。那么Redis有哪些淘汰策略?

  • allkeys-lru:从数据集中挑选最近最少使用的key淘汰,该方式适用于缓冲中的数据都是热点数据
  • allkeys-random:随机选择一些key进行淘汰,该方式适用于如果我们应用对缓冲key访问的概率相等
  • volatile-lru:从已经设置了过期时间的数据集中挑选最近最少使用的key进行淘汰
  • volatile-random:从已经设置了过期时间的数据集中随机挑选一些key进行淘汰
  • volatile-ttl:从已经设置了过期时间的数据集中选择快要过期的key进行淘汰
    注意:Redis中的LRU算法并不是真正意义上的LRU算法,是采用抽样的LRU算法,在一定程度上接近真正LRU算法。

单线程的Redis在性能上为什么这么突出

首先来说为什么使用单线程,我们知道多线程主要是对CPU资源的最大化使用,而Redis的性能瓶颈并不在CPU,而是在于内存和网络。为了在单线程模式下性能达到更高,Redis采用IO多路复用的解决方案
来解决Redis在单线程下的性能问题。

多路复用

I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

引自:Redis I/O 多路复用

在Redis中使用Lua脚本

在客户端使用Redis会面临很多问题,例如:原子性问题、性能问题等

  • 原子性问题:Redis作为数据服务器,多个客户端连接到Redis上,这个时候多个客户端发送的命令可能会因为网络或者其他元素导致Redis服务器收到的命令顺序会被打乱,这样就造成了客户端发送的一批
    命令没有顺序性的执行,导致数据错乱。
  • 性能问题:客户端执行一段逻辑可能需要多次访问Redis服务器,这期间的多次网络请求就会成为Redis性能的瓶颈。
    为了解决以上问题,Redis内嵌了对Lua脚本的支持,客户端可以通过Lua脚本发送一批Redis命令到Redis服务器,这一操作,既解决了Redis的原子性问题又解决了性能问题。使用Lua脚本的好处:
  1. 减少网络开销,可以吧多个Redis命令放到一个Lua脚本中执行
  2. 原子操作:Redis会叫这个Lua脚本作为一个整体执行,中间不会接受客户端的其他请求。
  3. 复用性:Redis可以把一个Lua脚本保存到服务器中,其他客户端都可以使用该脚本。

在Lua脚本中调用Redis命令

redis.call("set","key","value")
local val = redis.call("get","key")

以上脚本是在Lua脚本中调用Redis命令,并且返回结果到Lua脚本中,那么在开发场景中,Lua脚本其实也可以看成是我们封装的一个带有逻辑性的Redis命令,那么是Redis命令就一定会有返回值,应该任何
从Lua脚本中获取返回值呢,也就是执行完Lua脚本过后,得到执行结果,其实可以通过在Lua脚本中通过return关键字将结果返回,如果没有return,则默认返回nil。
PS:定义一个lua脚本用于设置一个字符串类型的key

# demo.lua
return redis.call("set",KEYS[1],ARGS[1])

执行lua脚本,使用EVAL demo.lua 1 key value,其中1表示一个key,也就是1后面的多少个参数作为key会被放到KEYS变量中,其余的参数都会放到ARGS中。
使用Lua脚本可以将一系列的Redis命令封装在脚本中,减少了多次请求网络的开销和原子性问题,但其实当客户端发送一次Lua脚本的时候,Lua脚本本身比较大,对网络开销也很大,所以我们可以将
Lua脚本缓存到Redis中并生成SHA1摘要,客户端只需要发送摘要就可以代替对应的Lua脚本。操作命令如下:

script load demo.lua
eavlsha sha摘要 0

Redis集群

在日常开发过程中,我们使用任何中间件都一定会考虑其单点问题,都会使用集群的方式来达到中间件本身的高可用。

主从复制

Redis支持一主多从的高可用方案,就是一个主节点对应多个从节点,主节点能处理客户端的读写操作,而从节点则只能接受读操作,当主节点出现宕机不可用等情况时,从节点可以升级成为主节点持续
对外提供服务。那么主节点和从节点就一定会有数据同步的过程,这个过程是在当主节点中数据发生变更时会触发,该操作是异步的,即数据同步过程中不会影响主节点对外提供服务。
实现Redis主从复制也是很简单的,只需要在从节点的redis.conf配置文件中增加配置slaveof masterIp masterPort并且允许所有IP访问(注释掉bindip)即可,主节点不需要修改任何配置。
启动主从节点过后,可以通过info replication查看集群状态。

原理

  • 全量复制:全量复制一般发生在初始化过程中,步骤如下:

    1. 从节点启动连接到主节点过后,向主节点发送SYNC命令
    2. 主节点收到SYNC命令过后,执行BGSAVE命令进行RDB快照,并且将从现在开始收到的客户端的增删改操作命令保存到缓冲区中
    3. 主节点执行完BGSAVE命令过后将生成的RDB发送给从节点,发送期间继续保存主节点执行的命令
    4. 从节点收到RDB文件过后,丢弃旧的数据,从RDB文件中恢复数据
    5. 主节点在发送完快照文件过后向从节点发送缓冲区的操作命令
    6. 从节点收到主节点的操作命令过后执行
      当完成以上操作过后Redis主从的初始化就完成了,从节点这个时候就可以接受客户端发送的读请求了。
      Redis中主从复制其实是利用RDB快照的方式,然而使用该方式会存在一些问题,例如:
      1. 当Master为开启RDB快照时,主从复制的初始化任然会执行RDB快照,生成一个文件到响应目录中,当Master下次启动时,会根据这个RDB文件进行数据恢复,由于快照生成的时间可以是任何时间点的
        所有就会造成数据问题。
      2. 当硬盘性能本身很低时,可能会造成主节点的性能瓶颈
        为了解决以上问题,Redis2.8.18版本以后,提供了无硬盘复制,也就是说不会生成RDB文件,直接发送数据。我们可以通过repl-diskless-sync yes配置来开启该功能。
  • 增量复制:增量复制是指当主节点数据发生变更时,主节点将接收到的客户端发送的命令原封不动的发送给从节点,从节点收到主节点发送的命令过后执行来达到数据的同步。

哨兵机制

前面说到了当Redis集群中的主节点宕机或者不可用时,需要从其他从节点中选举一个作为主节点继续对外提供服务,那么谁去选举Master呢,在Redis中提供了一个角色来专门做Master故障切换和选举。
这就是哨兵。在Redis中,哨兵主要干两件事情,一是监控Master和Slave节点是否正常运行,二是当Master出现宕机不可用时,从从节点中选举一个节点作为Master节点。哨兵是一个独立的进程,监控着
集群中的Master节点,通过Master节点,哨兵可以找到该集群中的其他从节点,进而监控着整个集群中的所有主从节点。

虽然这一架构解决了Redis集群中故障切换的问题,但是有引发了另外一个问题,就是哨兵作为集群中故障切换的关键角色,哨兵的单点问题也需要解决。所以,这个时候的架构应该改进为,主从集群和哨兵集群,
来达到整个Redis服务集群的高可用。哨兵集群中的所有哨兵节点是相互感知的,原理大概是:所有哨兵节点都监控同一个Master节点,并且订阅同一个channel(名为 channel:sentinel:hello),
新加入的哨兵节点会向该channel中发送一条消息(包含自身信息),新加入的哨兵节点和其他哨兵节点建立一个长连接。

哨兵节点会定期向Master发送心跳包来判断Master节点是否存货,一旦发现Master没有正确相应,哨兵会将该Master节点状态设置为“主观不可用”,然后把这个状态发送给其他哨兵节点进行确认,
如果确认的节点超过配置的“quorum”值时,则会认为Master是客观不可用。接着就会进入Master选举流程。这个时候又会出现一个问题,哨兵是一个集群,具体的由哪一个哨兵来执行该操作呢,
这里就涉及到了领头哨兵的选举,这里其实使用是Raft算法,具体流程如下:

  1. 发现主库客观下线的哨兵节点(这里称为A)向每个哨兵节点发送命令要求对方选举自己为领头哨兵(leader)
  2. 如果目标哨兵没有选举过其他人,则同意将A选举为领头哨兵
  3. 如果A发现有超过半数且超过quorum参数值的哨兵节点同意选自己成为领头哨兵,则A哨兵成功选举为领头哨兵
  4. 当有多个哨兵节点同时参与领头哨兵选举时,出现没有任何节点当选可能,此时每个参选节点等待一个随机时间进行下一轮选举,直到选出领头哨兵

哨兵的配置实现

在运行哨兵节点的服务器上新建一个sentinel.conf文件,添加以下属性:

port 6040
sentinel monitor mymaster 192.168.1.1 6379 1            # name为自定义master名称,1表示最少多少个哨兵节点同意才能执行后面的操作
sentinel down-after-milliseconds mymaster 5000          # 表示如果5s内mymaster没响应,就认为SDOWN
sentinel failover-timeout mymaster 15000                # 表示如果15秒后,mysater仍没活过来,则启动failover,从剩下的slave中选一个升级为master

启动哨兵的两种方式./redis-server.sh /path/sentinel.conf --sentinel/./redis-sentinel /path/sentinel.conf

Redis-Cluster

Redis Cluster集群一般推荐使用三主三从的架构方案。三主主要是用于数据的分片,从而达到高吞吐,三从主要是防止主节点宕机过后进行顶替,从而防止服务器不可用而造成损失,也就是达到了高可用的目的。下面就着手搭建一个三主三从的redis集群。

  1. 首先是每个节点的配置(redis.conf),这里六个节点的配置都一样,配置如下:
cluster-enabled yes # 开启cluster模式
cluster-config-file node-6379.conf # 各个节点自己的一些集群信息
cluster-node-timeout 5000 # 超时时间,超时则认为master宕机,进行主备的切换
appendonly yes # 保证aof持久化模式开启状态
  1. 将数据目录下的appendonly.aof,dump.rdb两个文件删除
  2. 构建集群,在旧版本的redis中使用redis cluster我们需要通过ruby进行构建(在redis解压目录中的src目录中有个redis-trib.rb文件),但是在新版本中是可以直接使用redis-cli进行构建cluster集群。redis-cli -a redisPwd --cluster create ip:port [ip:port...] --cluster-replicas 1
    参数说明:-a表示redis节点间建立连接使用的认证密码;--cluster-replicas表示主从节点的比值,当前使用三主三从的集群方案,实际上每一个主节点是有一个从节点是与之对应的,所以这里填写值为1。
  3. 到这里,redis cluster就算是搭建成功了,可以使用redis-cli -a redisPwd --cluster check ip:port进行查看集群信息。

Redis缓存及数据一致性的问题

Redis缓存和数据库在事务上是不能达到统一的,那么我们如何保证最终一致性。

  • 先操作缓存还是先操作数据库?
    答:如果我们使用缓存失效这种方式来代替缓存数据的更新,那么应该先更新数据库再失效缓存。如果使用更新缓存的方式,我们需要根据业务场景来权衡。
  • 更新缓存还是让缓存失效?
    答:如果更新缓存的代价较小,可以更新缓存,如果更新缓存的代价较大,我们可以直接将缓存失效,下一次访问时缓存未命中,则会自动从数据库中获取数据并且将其缓存

缓存雪崩

当大规模的缓存数据同时失效或者说缓存集群不可用时,大量的客户端请求直接性的对DB层造成了重大的冲击,最终导致整个系统瘫痪,这种现象称之为缓存雪崩。那么面对这一现象应该考虑的解决方式如下:

  1. 当缓存中未命中客户端想要的数据,则通过加锁形成排队的方式访问数据库,避免同时并发的访问底层存储系统带来的重大冲击。
  2. 避免大批量缓存数据同时失效,将缓存过期时间点分散。
  3. 保证缓存服务的高可用。

缓存穿透

当客户端查询一个不存在的数据,缓存和数据库都不会命中,又由于数据库中没有数据,所以不会被缓存。由于底层存储系统往往不具备高并发性,频繁并发的穿透可能会导致存储系统宕机。对于这一现象
的解决思路如下:

  1. 将不存在的key也缓存到缓存中
  2. 将key按照规则命名,将不符合规则的key过滤掉
  3. 采用布隆过滤器的方式来判断当前查询的key是否存在于缓存当中,如果不存在,则过滤掉。

布隆过滤器

主要作用是判断一个元素是否存在于集合中,因为它是一个概率算法,所以会存在误差。它的优点在于空间效率和查询时间都比其他算法快。
例如:当传入一个元素,结果表示其存在但有可能不存在,但是绝对不会出现结果表示不存在的但实际存在。也就是说,布隆过滤器判断一个元素不存在则绝对不存在,判断一个元素存在则会出现误差。