# 笔记：Redis核心原理与实战分析（上）

## Redis是如何执行的

### 命令执行流程

一条命令的执行过程有很多细节，但大体可分为：客户端先将用户输入的命令，转化为 Redis 相关的通讯协议（RESP），再用 socket 连接的方式将内容发送给服务器端，服务器端在接收到相关内容之后，先将内容转化为具体的执行命令，再判断用户授权信息和其他相关信息，当验证通过之后会执行最终命令，命令执行完之后，会进行相关的信息记录和数据统计，然后再把执行结果发送给客户端，这样一条命令的执行流程就结束了。如果是集群模式的话，主节点还会将命令同步至子节点。

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2Fd5566fd2d3f2084e8bb022375cd4040eb8b2c15c.png?generation=1593771153248870\&alt=media)

### socket

每个 socket 被创建后，会分配两个缓冲区，输入缓冲区和输出缓冲区。 写入函数并不会立即向网络中传输数据，而是先将数据写入缓冲区中，再由 TCP 协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区，函数就可以成功返回，不管它们有没有到达目标机器，也不管它们何时被发送到网络，这些都是 TCP 协议负责的事情。 注意：数据有可能刚被写入缓冲区就发送到网络，也可能在缓冲区中不断积压，多次写入的数据被一次性发送到网络，这取决于当时的网络情况、当前线程是否空闲等诸多因素，不由程序员控制。 读取函数也是如此，它也是从输入缓冲区中读取数据，而不是直接从网络中读取。

### IO多路复用

Redis 使用的是 I/O 多路复用功能来监听多 socket 链接的，这样就可以使用一个线程链接来处理多个请求，减少线程切换带来的开销，同时也避免了 I/O 阻塞操作，从而大大提高了 Redis 的运行效率。

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2Fda2fadcf82f19ef926b9e8f88000150a794a7165.png?generation=1593771156493565\&alt=media)

“多路”指的是多个网络连接，“复用”指的是复用同一个线程，采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求，这样就减少了创建和销毁线程所带来的时间消耗，从而到达高效处理大量并发请求的目的

## Redis持久化

<https://juejin.cn/post/6844903939339452430>

### RDB

RDB持久化是通过**快照**的方式，即在指定的时间间隔内将内存中的数据集快照写入磁盘。在创建快照之后，用户可以备份该快照，可以将快照复制到其他服务器以创建相同数据的服务器副本，或者在重启服务器后恢复数据。**RDB是Redis默认的持久化方式**

### AOF

除了RDB持久化，Redis还提供了AOF（Append Only File）持久化功能，AOF持久化会把被执行的写命令写到AOF文件的末尾，记录数据的变化。默认情况下，Redis是没有开启AOF持久化的，开启后，每执行一条更改Redis数据的命令，都会把该命令追加到AOF文件中，这是会降低Redis的性能，但大部分情况下这个影响是能够接受的，另外使用较快的硬盘可以提高AOF的性能

## 数据结构（常用五种）

### 字符串

redis字符串基于SDS（redis内部实现的一种数据结构）实现，SDS对象包含三种不同的数据类型：int、embstr、raw

#### int

当我们设置的值为整数类型时，在redis中以int类型存储；

#### embstr

当我们设置的值为字符串类型时，在redis中以embstr类型存储；

#### raw

当我们设置的字符串长度大于44字节时，在redis中以raw类型存储；

（1字节=8位）按编码类型字与字节的关系如下：

1. 英文

   字节数 : 1;编码：GB2312 字节数 : 1;编码：GBK 字节数 : 1;编码：GB18030 字节数 : 1;编码：ISO-8859-1 字节数 : 1;编码：UTF-8 字节数 : 4;编码：UTF-16 字节数 : 2;编码：UTF-16BE 字节数 : 2;编码：UTF-16LE

   1. 中文

   字节数 : 2;编码：GB2312 字节数 : 2;编码：GBK 字节数 : 2;编码：GB18030 字节数 : 1;编码：ISO-8859-1 字节数 : 3;编码：UTF-8 字节数 : 4;编码：UTF-16 字节数 : 2;编码：UTF-16BE 字节数 : 2;编码：UTF-16LE

**为什么是44字节**：在redis中，如果SDS的存储值大于64字节时，Redis的内存分配器会认为此对象为大字符串，并使用raw类型来处理，当数据小64字节时，会使用embstr类型存储。既然内存分配器的判断标准是64字节，那为什么embstr类型和raw类型的存储判断值是44字节；因为Redis在存储对象时，会创建此对象的关联信息，redisObject对象头和SDS自身属性信息，这些信息都会占用一定的存储空间，在redis的源码中：

redisObject信息占用16字节

```c
typedef struct redisObject {
    unsigned type:4; // 4 bits (0.5字节)
    unsigned encoding:4; // 4 bits  (0.5字节)
    unsigned lru:LRU_BITS; // 3 个字节
    int refcount; // 4 个字节
    void *ptr; // 8 个字节
} robj;
// type 对象的数据类型，例如：string、list、hash 等，占用 4 bits 也就是半个字符的大小；
// encoding 对象数据编码，占用4bits
// lru 记录对象的LRU信息(Least Recently Used 的缩写，即最近最少使用)信息，内存回收时会用到此属性，占用 24 bits(3 字节)；
// refcount：引用计数器，占用 32 bits(4 字节)；
// *ptr：对象指针用于指向具体的内容，占用 64 bits(8 字节)。
```

SDS信息占用7字节，SDS 的存储类型一共有 5 种：SDSTYPE5、SDSTYPE8、SDSTYPE16、SDSTYPE32、SDSTYPE64，在这些类型中最小的存储类型为 SDSTYPE５，但 SDSTYPE５ 类型会默认转成 SDSTYPE8（如图）

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F1631c364eece4440c8ece26fd91cab9538d461b2.png?generation=1593771157180453\&alt=media)

```c
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 1 byte
    uint8_t alloc; // 1 byte
    unsigned char flags; // 1 byte
    char buf[];
};
```

**可以看出除了内容数组(buf)之外，其他三个属性分别占用了 1 个字节，最终分隔字符等于 64 字节，减去 redisObject 的 16 个字节，再减去 SDS 自身的 3 个字节，再减去结束符 `\0` 结束符占用 1 个字节，最终的结果是 44 字节(64-16-3-1=44)**

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F89e5c09230628c05cdde14254ac1831659da4ae0.png?generation=1593771154298079\&alt=media)

### 字典

字典又称散列类型或哈希表。使用redis的字典类型来存储信息（比如go map）不需要手动进行序列化和反序列化（如果使用字符串存则需要手动序列化成json字符串然后取出使用时进行反序列化）

#### 哈希表

在redis中使用数组（桶）加链表（拉链法解决冲突）实现哈希表

```c
typedef struct dictEntry { // dict.h
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 下一个 entry
} dictEntry;
```

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F20ec21af1d3d7444c483219db3aac1d005cc949d.png?generation=1593771167380544\&alt=media)

#### 哈希冲突

字典类型的存储流程是先将键值进行 Hash 计算，得到存储键值对应的数组索引，再根据数组索引进行数据存储，但在小概率事件下可能会出完全不相同的键值进行 Hash 计算之后，得到相同的 Hash 值，这种情况我们称之为**哈希冲突**

#### 查询流程

* 通过计算（Hash，取余等）操作获得数组的索引值，根据索引值找到对应元素
* 判断元素和查找值是否相等，相等则成功返回数据（说明没有出现冲突），否则需要查看next指针是否还有对应其他元素，如果没有则返回null（说明不存在），如果存在则重复此步骤

#### 渐进式reHash

redis为了保证应用的高性能运行，提供了一个重要的机制——渐进式reHash。在字典进行扩容或者缩容时采用的都是渐进式reHash机制

### 列表

redis列表类型

在3.2之前基于字节数组的ziplist实现，3.2之后是基于双向链表+ziplist实现的quicklist（本质上是双端队列）

### 集合

当集合元素都是int类型且数量在一定范围内（默认是512个，可以通过配置修改）时，集合类型为整数集合；其他类型则是基于hashtab实现的，key为集合元素值，value为null

### 有序集合

有序集合在元素小于128个且所有元素都小于64字节时（默认是64字节，可以通过配置修改），基于ziplist存储；反之以dict + skiplist（跳表，对普通链表进行改进，插入数据时保持整体有序，在查找时通过二分进行查找）来存储

## 事务

**事务**指的是提供一种将多个命令打包，一次性按顺序地执行的机制，并且保证服务只有在执行完食物中的所有命令后，才会继续处理此客户端的其他命令

redis中的事务从开始到结束也是要经历三个阶段：开启事务，命令入列，执行事务/放弃事务

### 事务错误

#### 执行时错误

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2Fa5f9de1041154b168063cf24fbcca19118b89041.png?generation=1593771155385117\&alt=media)

即使事务队列中的某个命令在执行期间发生了错误，事务也会继续执行，直到事务队列中的所有命令执行完成

#### 入列时错误，但不终止整个事务

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F0e16231f699acf0696b30b08add43eb05642ed7f.png?generation=1593771156763769\&alt=media)

重复执行multi会导致入列错误，但不会终止事务，最终查询的结果事务是执行成功的

#### 入列时错误，终止整个事务

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F9f125bc6bb4fc747c4d31fc1348a44aa197db755.png?generation=1593771156534845\&alt=media)

### 不支持运行时错误的事务回滚

* redis作者认为 Redis 事务的执行时，错误通常都是编程错误造成的，这种错误通常只会出现在开发环境中，而很少会在实际的生产环境中出现，所以他认为没有必要为 Redis 开发事务回滚功能；
* 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合

### 监控

watch命令用于客户端在并发情况下，为事务提供一个乐观锁（CAS，check and set），也就是可以用watch命令来监控一个或多个变量，如果在事务的过程中，某个监控项被修改了，那么整个事务就会终止执行

**watch 命令只能在客户端开启事务之前执行，在事务中执行 watch 命令会引发错误，但不会造成整个事务失败**

## 过期

### 过期策略

常见的过期策略有：定时删除（每隔一段时间进行删除）；惰性删除（软删除）；定期删除（每个时间段的同一个时间点进行删除）

#### **定时删除**

在设置键值过期时间时，创建一个定时事件，当过期时间到达时，由事件处理器自动执行键的删除操作。

* **优点：**&#x4FDD;证内存可以被尽快地释放。
* **缺点：**&#x5728; Redis 高负载的情况下或有大量过期键需要同时处理时，会造成 Redis 服务器卡顿，影响主业务执行。

#### **惰性删除**

不主动删除过期键，每次从数据库获取键值时判断是否过期，如果过期则删除键值，并返回 null。

* **优点：**&#x56E0;为每次访问时，才会判断过期键，所以此策略只会使用很少的系统资源。
* **缺点：**&#x7CFB;统占用空间删除不及时，导致空间利用率降低，造成了一定的空间浪费。

#### **定期删除**

每隔一段时间检查一次数据库，随机删除一些过期键。

Redis 默认每秒进行 10 次过期扫描，此配置可通过 Redis 的配置文件 redis.conf 进行配置，配置键为 hz 它的默认值是 `hz 10`。

需要注意的是：Redis 每次扫描并不是遍历过期字典中的所有键，而是采用随机抽取判断并删除过期键的形式执行的。

**Redis 使用的是惰性删除加定期删除的过期策略**

## 管道（pipeline）

管道技术是一种批处理技术，用于一次处理多个redis命令，从而提高整个交互的性能

管道技术主要解决了多个命令集中请求造成网络资源浪费的问题

### 需要注意的事项

1. 发送的命令数量不会被限制，但输入缓存区（也就是命令的最大存储体积）最大为1GB，当发送的命令超过此限制时，命令不会被执行，并且会被redis服务器断开链接
2. 如果管道的数据过多可能会导致客户端的等待时间过长，导致网络阻塞
3. 部分客户端自己本身也有缓存区大小的设置，如果管道命令没有没执行或者是执行不完整，可以排查此情况或发送较少的管道内的命令重新尝试执行

## 游标迭代器 Scan

在redis中keys的时间复杂度是O(n)，所以数据量越大查询时间就越长，如果数据足够大时执行keys命令会导致Redis假死不可用，导致出现故障

在redis 2.8以后可以通过Scan命令实现keys的匹配功能；Scan时通过游标进行查询的不会造成假死；Scan提供了count参数，可以规定遍历的数量；Scan会把下标返回给客户端，用户客户端继续遍历查询；Scan返回的结果可能会有重复数据；单次返回值不为0则说明遍历还没有结束；Scan可以保证开始检索之前，被删除的元素一定不会被查询出来；在迭代过程中如果有元素被修改，Scan不保证能查询出相关的元素。

## 优秀的基数统计算法 HyperLogLog

1. 能够使用极少的内存来统计巨量的数据，它只需要12K的空间就能统计2\*\*64的数据；
2. 统计存在一定的误差，误差率整体较低，标准误差为0.81/100
3. 误差通过设置辅助计算因此进行降低

操作：添加一个元素`pfadd key element [element ...]`；统计不重复的元素`pfcount key [key ...]`；合并一个或多个HLL至新结构`pfmerge destkey sourcekey [sourcekey ...]`

## 内存淘汰机制与算法

在redis中，过期策略和内存淘汰策略是两个完全不同的概念；首先redis过期策略指的是redis使用哪种策略来删除已经过期的键值对，而redis内存淘汰机制指的是当运行内存已经超过redis设置的最大内存之后，将采用什么策略删除键值对

### 最大允许内存

配置项为`maxmemory`，只有在redis的运行内存达到了某个阈值，才会触发内存淘汰机制。如果maxmemory配置值不大于0则不会出发内存淘汰机制

### 查询最大运行内存

使用`config get maxmemory`来查看设置的最大运行内存（在64位操作系统中maxmemory默认值为0，在32位操作系统中默认值为3GB）

### 内存淘汰策略

#### 查看内存淘汰策略

使用命令`config get maxmemory-policy`来查看当前的内存淘汰策略

#### 内存淘汰策略分类

早期版本的 Redis 有以下 6 种淘汰策略：

1. **noeviction**：不淘汰任何数据，当内存不足时，新增操作会报错，Redis 默认内存淘汰策略；
2. **allkeys-lru**：淘汰整个键值中最久未使用的键值；
3. **allkeys-random**：随机淘汰任意键值;
4. **volatile-lru**：淘汰所有设置了过期时间的键值中最久未使用的键值；
5. **volatile-random**：随机淘汰设置了过期时间的任意键值；
6. **volatile-ttl**：优先淘汰更早过期的键值。

在 Redis 4.0 版本中又新增了 2 种淘汰策略：

1. **volatile-lfu**：淘汰所有设置了过期时间的键值中，最少使用的键值；
2. **allkeys-lfu**：淘汰整个键值中最少使用的键值。

其中 allkeys-xxx 表示从所有的键值中淘汰数据，而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。

#### 修改redis内存淘汰策略

* 方式一：通过“config set maxmemory-policy 策略”命令设置。它的优点是设置之后立即生效，不需要重启 Redis 服务，缺点是重启 Redis 之后，设置就会失效。
* 方式二：通过修改 Redis 配置文件修改，设置“maxmemory-policy 策略”，它的优点是重启 Redis 服务后配置不会丢失，缺点是必须重启 Redis 服务，设置才能生效。

### 内存淘汰算法

#### LRU算法

LRU（Least Recently Used）最近最少使用的，是一种常用的页面置换算法，选择最近最久未使用的页面予以淘汰

1. 实现：基于链表实现，链表中的元素按照操作顺序从前往后排列，最新操作的键会被移动到表头，需要内存淘汰时，只需要删除链表尾部的元素。
2. 近LRU算法：redis使用的是一种近似LRU算法，目的是为了更好的节约内存，它的实现方式是给现有的数据结构添加一个额外的字段，用于记录此键值的最后一次访问时间，redis内存淘汰时，会以随机采样的方式来淘汰数据（默认配置未5个），然后淘汰最久没有使用的那个。

#### LFU算法

LFU（Least Frequently Used）最不常用的，LFU算法是根据总访问次数来淘汰数据的，它认为“如果数据过去被访问多次，那么将来被访问的频率也更高”

## 消息队列

### 发布订阅模式

发布订阅模式的三个命令：

* subscribe channel 普通订阅
* publish channel message 消息推送
* psubscribe pattern 主题订阅

#### 普通发布与订阅

发布者生产消息

订阅者消费消息

订阅者需要关注该发布者

#### 主题订阅

发布者生产了该主题类型消息

订阅者收到该主题类型消息

订阅者订阅该主题

#### 在redis5.0之前发布订阅模式存在以下两个缺点

1. 无法持久化保存消息，如果 Redis 服务器宕机或重启，那么所有的消息将会丢失；
2. 发布订阅模式是“发后既忘”的工作模式，如果有订阅者离线重连之后不能消费之前的历史消息。(在该模式下，如果所有消费者都离线，redis生产者生产的消息并不会保留等待消费者节点来消费)

**在5.0之后添加了 Stream 类型解决上述两个缺点**

### 基于Redis List的消息队列

lpush向队列中推送消息（生产消息），brpop(阻塞读，当队列中没有数据时就会进入休眠状态，避免一直循环执行消耗系统资源)从队列中拉取消息（消费消息）

#### 优缺点分析

List 优点：

* 消息可以被持久化，借助 Redis 本身的持久化（AOF、RDB 或者是混合持久化），可以有效的保存数据；
* 消费者可以积压消息，不会因为客户端的消息过多而被强行断开。

List 缺点：

* 消息不能被重复消费，一个消息消费完就会被删除；
* 没有主题订阅的功能

### 基于Redis zset的消息队列

zadd向队列中推送消息（生产消息），zrangebyscore从队列中拉取消息（消费消息）

#### 优缺点分析

ZSet 优点：

* 支持消息持久化；
* 相比于 List 查询更方便，ZSet 可以利用 score 属性很方便的完成检索，而 List 则需要遍历整个元素才能检索到某个值。

ZSet 缺点：

* ZSet 不能存储相同元素的值，也就是如果有消息是重复的，那么只能插入一条信息在有序集合中；
* ZSet 是根据 score 值排序的，不能像 List 一样，按照插入顺序来排序；
* ZSet 没有向 List 的 brpop 那样的阻塞弹出的功能。&#x20;

### 终极解决方案 Stream

在redis5.0之前消息队列的实现方式各有缺点：

1. 发布订阅模式PubSub，不能持久化也就无法可靠的保存消息，并且对于离线重连的客户端不能读取历史消息
2. 列表实现的消息队列，消息不能重复消费，一个消息消费完就会被删除
3. 有序集合消息队列，不能存储相同value的消息，并且不能阻塞读消息

并且以上方式实现的消息队列只能存储单value值，也就是如果你要存储一个对象则需要先序列化成json字符串，消费时还需要进行反序列化。

基于以上问题redis提供了Stream类型（支持消息持久化、消息轨迹、支持ack确认消息），相关操作：

* xadd 添加消息；
* xlen 查询消息长度；
* xdel 根据消息 ID 删除消息；
* del 删除整个 Stream；
* xrange 读取区间消息
* xread 读取某个消息之后的消息。

基于stream实现简易版的消息队列：生产消息（xadd），消费消息（xread）

基于stream消息分组实现的发布订阅模式消息队列：同一个分组内的多个 consumer 会读取到不同消息，不同的 consumer 不会读取到分组内的同一条消息

**Redis Stream 适用于小型、廉价的应用程序**

## 分布式锁详解

### 什么是锁（程序级别）

**锁是一种常用的并发控制机制，用于保证一项资源在任何时候只能被一个线程使用**，如果其他线程也要使用同样的资源，必须排队等待上一个线程使用完（释放锁）

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2Fe236f740bda00f84a076a7526d11c2dc6a629490.png?generation=1593771155996513\&alt=media)

### 什么是分布式锁

**分布式锁是用于分布式环境下并发控制的一种机制，用于控制某个资源在同意时刻只被一个应用所使用**

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F42b3c4c04cd5c054dc2b7b6f447998bc3db0bc36.png?generation=1593771163013233\&alt=media)

常见的分布式锁实现方式：

1. 基于Memcached实现：使用add命令，添加成功表示创建分布式锁成功
2. 基于Zookeeper实现：使用ZooKeeper顺序临时节点来实现分布式锁
3. 基于Redis实现分布式锁：使用setnx(set if not exists)，创建成功表示创建分布式锁成功

### 基于Redis实现分布式锁

使用`setnx lock true`命令创建锁

使用`del lock`命令删除锁（释放锁）

如果锁未被释放而使用`setnx lock true`尝试创建锁则会返回"0"表示创建失败

#### 使用setnx存在的问题

如果一个程序创建锁之后异常退出，那么这个锁将永远得不到释放，就造成了**死锁**问题

我们可以使用setnx创建锁之后，使用`expire lock 过期时长`来设置超时时间，但是如果在执行完创建锁之后出现断电或redis异常退出则会导致设置过期时间命令未执行，依然会造成死锁

#### 使用带参数的set

使用`set lock true ex 30 nx`创建过期时间为30秒的锁（保证了创建过期锁的原子性），这样可以解决基于setnx命令实现分布式锁可能会造成死锁的问题

### 分布式锁执行超时问题

如果我们设置锁的最大超时时间为30秒，但业务处理使用了35秒，这就导致原有业务还未执行完成，锁就被释放了，新的程序和旧的程序一起操作就会带来线程安全问题

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F76b3267b4d95307e8371ab88146a6fc62887b276.png?generation=1593771152287920\&alt=media)

因此：1. 把执行比较耗时的任务不要放到加锁的方法呢，锁内的方法尽量控制执行时长；2. 适当延长最大超时时间

#### 锁被误删问题

假设锁的最大超时时间为30秒，应用1执行了35秒，而应用2在30秒时（由于应用1创建的锁已过期）创建锁，然后在35秒后，应用1执行完之后，就会把应用2创建的锁给删除（释放），从而出现**锁被误删**问题

![img](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F74fd266fbb470c3b290131799882496bdfca5439.png?generation=1593771165685522\&alt=media)

解决方法：在使用set命令创建锁时，给value值设置一个归属人标识用于区分锁是否属于该线程，只有属于该线程时才能操作锁。

**需要注意的是在代码中的判断逻辑和删除逻辑可能不具备原子性，我们需要使用Lua脚本来执行判断和删除操作**

## 布隆过滤器

更多原理见**数据结构的布隆过滤器**

### 开启布隆过滤器

在redis中不能直接使用布隆过滤器，在4.0版本之后提供的modules(扩展模块)的方式引入

#### 编译方式开启

```
git clone https://github.com/RedisLabsModules/redisbloom.git
cd redisbloom
make # 编译redisbloom，编译成功后会在根目录生成一个redisbloom.so文件

# 启动redis服务器
./src/redis-server redis.conf --loadmodule ./src/modules/RedisBloom-master/redisbloom.so
# 其中 --loadmodule 为加载扩展模块的意思，后面跟的是 redisbloom.so 文件的目录
```

#### docker方式开启

```
# docker命令方式
docker pull redislabs/rebloom  # 拉取镜像
docker run -p6379:6379 redislabs/rebloom  # 运行容器

# docker-compose方式
# docker-compose.yml文件的版本
version: "2"
# 管理的服务
services:
  redis:
    # 指定镜像
    image: redislabs/rebloom:latest
    ports:
    # 端口映射
    - 6380:6379
    volumes:
    # 目录映射
    - "${REDIS_DIR}/conf:/usr/local/etc/redisbloom"
    - "${REDIS_DIR}/data:/data"
```

### 操作布隆过滤器

布隆过滤器的命令不是很多，主要包含以下几个：

1. bf.add：添加元素
2. bf.exists：判断某个元素是否存在
3. bf.madd：添加多个元素
4. bf.mexists：判断多个元素是否存在
5. bf.reserve：**设置布隆过滤器的准确率**

```
127.0.0.1:6379> bf.reserve user 0.01 200
(error) ERR item exists #已经存的 key 设置会报错
127.0.0.1:6379> bf.reserve userlist 0.9 10
OK
```

可以看出此命令必须在元素刚开始执行，否则会报错，它有三个参数：key、error\_rate 和 initial\_size。

* error\_rate：允许布隆过滤器的错误率，这个值越低过滤器占用空间也就越大，以为此值决定了位数组的大小，位数组是用来存储结果的，它的空间占用的越大（存储的信息越多），错误率就越低，它的默认值是 0.01。
* initial\_size：布隆过滤器存储的元素大小，实际存储的值大于此值，准确率就会降低，它的默认值是 100。

### 原理

Redis 布隆过滤器的实现，依靠的是它数据结构中的一个位数组，每次存储键值的时候，不是直接把数据存储在数据结构中，因为这样太占空间了，它是利用几个不同的无偏哈希函数，把此元素的 hash 值均匀的存储在位数组中，也就是说，每次添加时会通过几个无偏哈希函数算出它的位置，把这些位置设置成 1 就完成了添加操作。

当进行元素判断时，查询此元素的几个哈希位置上的值是否为 1，如果全部为 1，则表示此值存在，如果有一个值为 0，则表示不存在。因为此位置是通过 hash 计算得来的，所以即使这个位置是 1，并不能确定是那个元素把它标识为 1 的，因此**布隆过滤器查询此值存在时，此值不一定存在，但查询此值不存在时，此值一定不存在**。

并且当位数组存储值比较稀疏的时候，查询的准确率越高，而当位数组存储的值越来越多时，误差也会增大。

位数组和 key 之间的关系，如下图所示：

![image.png](https://122767808-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MBDSdFU2kWrJocOCeSH%2Fsync%2F85262ed8973405ee79361d9c2b483e4451c59780.png?generation=1593771155895227\&alt=media)

### 布隆过滤器使用场景

它的经典使用场景包括以下几个：

* 垃圾邮件过滤
* 爬虫里的 URL 去重
* 判断一个元素在亿级数据中是否存在

布隆过滤器在数据库领域的使用也比较广泛，例如：HBase、Cassandra、LevelDB、RocksDB 内部都有使用布隆过滤器。

## 延迟队列

延时队列是指把当前要做的事情往后推迟一段时间再做

使用场景有：

1. 超过30分钟未支付的订单，将会被取消
2. 外卖商家超过5分钟未接单的订单，将会被取消
3. 在平台注册但30天内未登录的用户，发短信提醒

### 基于MQ实现延迟队列

RabbitMQ 本身并不支持延迟队列，但可以通过添加插件 rabbitmq-delayed-message-exchange 来实现延迟队列。

**优点**

1. 支持分布式
2. 支持持久化

**缺点**

框架比较重，需要搭建和配置 MQ。

### 基于Redis实现延迟队列

Redis 是通过有序集合（ZSet）的方式来实现延迟消息队列的，ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

**优点**

1. 灵活方便，Redis 是互联网公司的标配，无序额外搭建相关环境；
2. 可进行消息持久化，大大提高了延迟队列的可靠性；
3. 支持分布式
4. 高可用性，利用 Redis 本身高可用方案，增加了系统健壮性。

**缺点**

需要使用无限循环的方式来执行任务检查，会消耗少量的系统资源。

## 定时任务

**定时任务指的是指定一个时间来执行某个任务**

### 开启键空间通知

redis服务端默认是不开启键空间通知的，需要我们手动开启。

配置 notify-keyspace-events Ex （该配置表示开启键事件通知里面的 key 过期事件）

更多配置项说明：

* K：键空间通知，所有通知以 `__keyspace@<db>__` 为前缀
* E：键事件通知，所有通知以 `__keyevent@<db>__` 为前缀
* g：DEL、EXPIRE、RENAME 等类型无关的通用命令的通知
* $：字符串命令的通知
* l：列表命令的通知
* s：集合命令的通知
* h：哈希命令的通知
* z：有序集合命令的通知
* x：过期事件，每当有过期键被删除时发送
* e：驱逐（evict）事件，每当有键因为 maxmemory 政策而被删除时发送
* A：参数 g$lshzxe 的别名

### 订阅发布

使用订阅者订阅元素的过期事件，然后再执行固定的任务

## RedisSearch高性能的全文搜索引擎

RedisSearch是一个高性能的全文搜索引擎，它可以作为一个Redis Module(扩展模块)运行在Redis服务器上

### 特性

* 基于文档的多个字段全文索引
* 高性能增量索引
* 文档排序（由用户在索引时手动提供）
* 在子查询之间使用 AND 或 NOT 操作符的复杂布尔查询
* 可选的查询子句
* 基于前缀的搜索
* 支持字段权重设置
* 自动完成建议（带有模糊前缀建议）
* 精确的短语搜索
* 在许多语言中基于词干分析的查询扩展
* 支持用于查询扩展和评分的自定义函数
* 将搜索限制到特定的文档字段
* 数字过滤器和范围
* 使用 Redis 自己的地理命令进行地理过滤
* Unicode 支持（需要 UTF-8 字符集）
* 检索完整的文档内容或只是 ID 的检索
* 支持文档删除和更新与索引垃圾收集
* 支持部分更新和条件文档更新

### 安装

推荐使用docker方式安装

### 查询

如果要查询中文内容的话，需要在添加数据时设置中文编码，并且在查询时也要设置中文编码，指定“language "chinese"”

## 慢查询

Redis慢查询作用和MySQL慢查询作用类似，都是为我们查询出不合理的执行命令，然后让开发人员和运维人员一起来规避这些耗时的命令，从而让服务器更加高效和健康的运行。对于单线程的Redis来说，不合理的使用更是致命的，因此掌握Redis慢查询技能对我们来说非常的关键。

### 相关配置

* slowlog-log-slower-than：用于设置慢查询的评定时间，也就是说超过此配置项的命令，将会被当成慢操作记录在慢查询日志中，它执行单位是微秒（默认10000微秒，注：1 秒等于 1000000 微秒）
* slowlog-max-len：用来配置慢查询日志的最大记录数（默认保存128条）。

### 查询慢日志

命令`slowlog show`

(当慢查询日志超过设定的最大存储条数之后，会把最早的执行命令依次舍弃)
