# README

## 说明

**Github地址(欢迎star):** [**RedisBook**](https://github.com/step-by-step-wiki/RedisBook)

**Gitbook在线版:** [**为你自己学Redis**](https://redis.sai.show)

**另外还有一本Gitbook, 是Go的学习,** [**为你自己学Go**](https://go.sai.show)**, 欢迎学习和star🌟**

## About me

**Url**: [**https://sai.show/about**](https://sai.show/about)

## Highlights

代码在[code](https://github.com/step-by-step-wiki/RedisOnDocker/blob/main/code/README.md)目录下

文章按章节如下：

### 第1章 构建Redis开发环境

* [1.1 Redis概述](/di-1-zhang-gou-jian-redis-kai-fa-huan-jing/1.1-redis-gai-shu)
* [1.2 了解必要的Docker技能](/di-1-zhang-gou-jian-redis-kai-fa-huan-jing/1.2-le-jie-bi-yao-de-docker-ji-neng)
* [1.3 安装和配置基于Docker的Redis环境](/di-1-zhang-gou-jian-redis-kai-fa-huan-jing/1.3-an-zhuang-he-pei-zhi-ji-yu-docker-de-redis-huan-jing)

### 第2章 实践Redis的基本数据类型

* [2.1 Redis缓存初体验](/di-2-zhang-shi-jian-redis-de-ji-ben-shu-ju-lei-xing/2.1-redis-huan-cun-chu-ti-yan)
* [2.2 针对字符串的命令](/di-2-zhang-shi-jian-redis-de-ji-ben-shu-ju-lei-xing/2.2-zhen-dui-zi-fu-chuan-de-ming-ling)
* [2.3 针对哈希类型变量的命令](/di-2-zhang-shi-jian-redis-de-ji-ben-shu-ju-lei-xing/2.3-zhen-dui-ha-xi-lei-xing-bian-liang-de-ming-ling)
* [2.4 针对列表类型变量的命令](/di-2-zhang-shi-jian-redis-de-ji-ben-shu-ju-lei-xing/2.4-zhen-dui-lie-biao-lei-xing-bian-liang-de-ming-ling)
* [2.5 针对集合的命令](/di-2-zhang-shi-jian-redis-de-ji-ben-shu-ju-lei-xing/2.5-zhen-dui-ji-he-de-ming-ling)
* [2.6 针对有序集合的命令](/di-2-zhang-shi-jian-redis-de-ji-ben-shu-ju-lei-xing/2.6-zhen-dui-you-xu-ji-he-de-ming-ling)

### 第3章 实践Redis的常用命令

* [3.1 键操作命令](/di-3-zhang-shi-jian-redis-de-chang-yong-ming-ling/3.1-jian-cao-zuo-ming-ling)
* [3.2 HyperLogLog相关命令](/di-3-zhang-shi-jian-redis-de-chang-yong-ming-ling/3.2-hyperloglog-xiang-guan-ming-ling)
* [3.3 lua脚本相关命令](/di-3-zhang-shi-jian-redis-de-chang-yong-ming-ling/3.3-lua-jiao-ben-xiang-guan-ming-ling)
* [3.4 排序相关命令](/di-3-zhang-shi-jian-redis-de-chang-yong-ming-ling/3.4-pai-xu-xiang-guan-ming-ling)

### 第4章 实践Redis服务器和客户端的操作

* [4.1 Redis服务器管理客户端的命令](/di-4-zhang-shi-jian-redis-fu-wu-qi-he-ke-hu-duan-de-cao-zuo/4.1-redis-fu-wu-qi-guan-li-ke-hu-duan-de-ming-ling)
* [4.2 查看Redis服务器的详细信息](/di-4-zhang-shi-jian-redis-fu-wu-qi-he-ke-hu-duan-de-cao-zuo/4.2-cha-kan-redis-fu-wu-qi-de-xiang-xi-xin-xi)
* [4.3 查看并修改服务器的常用配置](/di-4-zhang-shi-jian-redis-fu-wu-qi-he-ke-hu-duan-de-cao-zuo/4.3-cha-kan-bing-xiu-gai-fu-wu-qi-de-chang-yong-pei-zhi)
* [4.4 多个客户端连接远端服务器](/di-4-zhang-shi-jian-redis-fu-wu-qi-he-ke-hu-duan-de-cao-zuo/4.4-duo-ge-ke-hu-duan-lian-jie-yuan-duan-fu-wu-qi)

### 第5章 Redis数据库操作实战

* [5.1 切换数据库操作](/di-5-zhang-redis-shu-ju-ku-cao-zuo-shi-zhan/5.1-qie-huan-shu-ju-ku-cao-zuo)
* [5.2 Redis事务操作](/di-5-zhang-redis-shu-ju-ku-cao-zuo-shi-zhan/5.2-redis-shi-wu-cao-zuo)
* [5.3 地理位置相关操作](/di-5-zhang-redis-shu-ju-ku-cao-zuo-shi-zhan/5.3-di-li-wei-zhi-xiang-guan-cao-zuo)
* [5.4 位图数据类型的应用](/di-5-zhang-redis-shu-ju-ku-cao-zuo-shi-zhan/5.4-wei-tu-shu-ju-lei-xing-de-ying-yong)
* [5.5 慢查询实战分析](/di-5-zhang-redis-shu-ju-ku-cao-zuo-shi-zhan/5.5-man-cha-xun-shi-zhan-fen-xi)

### 第6章 Redis数据持久化操作

* [6.1 Redis持久化机制概述](/di-6-zhang-redis-shu-ju-chi-jiu-hua-cao-zuo/6.1-redis-chi-jiu-hua-ji-zhi-gai-shu)
* [6.2 AOF持久化机制实战](/di-6-zhang-redis-shu-ju-chi-jiu-hua-cao-zuo/6.2-aof-chi-jiu-hua-ji-zhi-shi-zhan)
* [6.3 RDB持久化机制实战](/di-6-zhang-redis-shu-ju-chi-jiu-hua-cao-zuo/6.3-rdb-chi-jiu-hua-ji-zhi-shi-zhan)
* [6.4 如何选用持久化方式](/di-6-zhang-redis-shu-ju-chi-jiu-hua-cao-zuo/6.4-ru-he-xuan-yong-chi-jiu-hua-fang-shi)

### 第7章 搭建Redis集群

* [7.1 搭建基于主从复制模式的集群](/di-7-zhang-da-jian-redis-ji-qun/7.1-da-jian-ji-yu-zhu-cong-fu-zhi-mo-shi-de-ji-qun)
* [7.2 搭建哨兵模式的集群](/di-7-zhang-da-jian-redis-ji-qun/7.2-da-jian-shao-bing-mo-shi-de-ji-qun)
* [7.3 搭建cluster集群](/di-7-zhang-da-jian-redis-ji-qun/7.3-da-jian-cluster-ji-qun)

### 第8章 GO整合MySQL与Redis

* [8.1 GO通过redigo读写Redis](/di-8-zhang-go-zheng-he-mysql-yu-redis/8.1-go-tong-guo-redigo-du-xie-redis)
* [8.2 Go与各种Redis数据类型](/di-8-zhang-go-zheng-he-mysql-yu-redis/8.2-go-yu-ge-zhong-redis-shu-ju-lei-xing)
* [8.3 Redis与MySQL的整合](/di-8-zhang-go-zheng-he-mysql-yu-redis/8.3-redis-yu-mysql-de-zheng-he)
* [8.4 Redis缓存实战分析](/di-8-zhang-go-zheng-he-mysql-yu-redis/8.4-redis-huan-cun-shi-zhan-fen-xi)

### 第9章 Redis应用场景与案例实现

* [9.1 Redis消息队列实战](/di-9-zhang-redis-ying-yong-chang-jing-yu-an-li-shi-xian/9.1-redis-xiao-xi-dui-lie-shi-zhan)
* [9.2 Go实战Redis分布式锁](/di-9-zhang-redis-ying-yong-chang-jing-yu-an-li-shi-xian/9.2-go-shi-zhan-redis-fen-bu-shi-suo)


# 安装


# macos下安装redis

通过二进制的方式安装 Redis 的步骤如下：

1. **下载 Redis 源码**: 你可以从 Redis 官方网站上下载最新的源码包。由于此环境中无法访问外部网站，你可以自行前往 Redis 官方网站或其 GitHub 仓库进行下载。
2. **解压源码包**: 假设你下载了 `redis-6.2.5.tar.gz` (版本号可能会不同)，在终端中进入到下载目录并执行：

   ```bash
   tar xzf redis-6.2.5.tar.gz
   cd redis-6.2.5
   ```
3. **编译 Redis**:

   在 `redis-6.2.5` 目录中，执行以下命令来编译 Redis：

   ```bash
   make
   ```

   编译完成后，`src` 目录下会生成相关的二进制文件，如 `redis-server` 和 `redis-cli`。
4. **测试编译结果**:

   在 `redis-6.2.5` 目录中，执行：

   ```bash
   make test
   ```

   这会运行 Redis 的测试套件，确保编译的版本没有问题。
5. **安装**:

   如果你想将 Redis 的二进制文件安装到系统中，可以执行：

   ```bash
   sudo make install
   ```

   默认情况下，这会将 Redis 安装到 `/usr/local/bin` 目录。
6. **启动 Redis**:

   你可以使用以下命令启动 Redis：

   ```bash
   redis-server
   ```

   如果你希望使用自定义的配置文件启动 Redis，可以这样做：

   ```bash
   redis-server /path/to/your/redis.conf
   ```
7. **测试 Redis**:

   使用 Redis 客户端工具 `redis-cli` 来测试 Redis 服务器：

   ```bash
   redis-cli ping
   ```

   如果 Redis 服务器正在运行，你应该会看到 `PONG` 作为响应。

以上就是通过二进制方式在 macOS 上安装 Redis 的步骤。希望这可以帮到你！


# 第1章 构建Redis开发环境


# 1.1 Redis概述

Redis(**Remote Dictionary Server**)是由Salvatore Sanfilippo开发的**key-value(键值对)存储系统**.Redis属于NoSQL数据库,进一步讲,Redis是**基于键值对存储的NoSQL数据库**.

## 1.1.1 对比传统数据库与NoSQL数据库

NoSQL使用比较简单的数据结构来保存数据,比如Redis用的就是键值对.NoSQL数据库更适用于"数据量小但对性能有一定要求"的场景

## 1.1.2 Redis的特点

优点:

1. 由于数据是存储在内存中的,因此查找数据的速度比较快
2. 支持的数据类型比较多,比如支持字符串、列表和哈希表等
3. **可以支持事务**,同时支持数据的持久化,即能把内存中的数据存入硬盘

缺点:

1. Redis**难以支持在线扩容**.尤其是在集群场景里,当存储容量达到上限后,在线扩容会非常困难
2. Redis是基于内存的,如果短时间内存入大量数据,可能会导致内存问题,比如会出现OOM(内存溢出)异常
3. Redis工作时是**基于单线程**的,所以无法充分利用多核机器里的CPU

基于Redis的优缺点,一般会将它用在缓存、秒杀、计数器和排行榜等**和性能密切相关的场景**里

## 1.1.3 Redis更适合以分布式集群的方式提供服务

**a. 基于主从复制的Redis集群**

![基于主从复制的Redis集群示意图](/files/cEkuQXCaiRariLiAJcAI)

能避免因单节点失效而带来的服务不可用,且能实现读写分离,以提升Redis系统的并发量

**b. 基于Cluster的Redis集群**

![基于Cluster的Redis集群示意图](/files/7FL6SvIpaisGiFUbtLhp)

基于Cluster(槽)的集群能有效扩展Redis缓存的容量,应对高并发场景下的缓存需求


# 1.2 了解必要的Docker技能

略


# 1.3 安装和配置基于Docker的Redis环境

## 1.3.1 用docker pull下载最新Redis镜像

拉取镜像:

```
docker pull redis:latest
latest: Pulling from library/redis
a2abf6c4d29d: Pull complete 
c7a4e4382001: Pull complete 
4044b9ba67c9: Pull complete 
c8388a79482f: Pull complete 
413c8bb60be2: Pull complete 
1abfd3011519: Pull complete 
Digest: sha256:db485f2e245b5b3329fdc7eff4eb00f913e09d8feb9ca720788059fdc2ed8339
```

查看结果:

```
 docker images|grep redis
redis                                                            latest    7614ae9453d1   19 months ago   113MB
```

## 1.3.2 用docker run启动Redis容器

运行Redis容器:

```
docker run -itd --name myFirstRedis -p 6379:6379 redis:latest
db18e24f57c664d85897241a248fa0ebace73fe7df321fd2d34307bb2b0291e1
```

查看结果:

```
docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                    NAMES
db18e24f57c6   redis:latest   "docker-entrypoint.s…"   18 seconds ago   Up 17 seconds   0.0.0.0:6379->6379/tcp   myFirstRedis
```

## 1.3.3 用docker logs观察Redis启动效果

````
docker logs myFirstRedis
1:C 31 Jul 2023 14:28:38.716 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 31 Jul 2023 14:28:38.716 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 31 Jul 2023 14:28:38.717 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
1:M 31 Jul 2023 14:28:38.717 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.2.6 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 1
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

1:M 31 Jul 2023 14:28:38.718 # Server initialized
1:M 31 Jul 2023 14:28:38.718 * Ready to accept connections
````

## 1.3.4 通过docker exec进入Redis容器

进入容器:

```
docker exec -it myFirstRedis /bin/bash
root@db18e24f57c6:/data# 
```

与redis服务器交互:

```
redis-cli
127.0.0.1:6379> set val 1
OK
127.0.0.1:6379> get val
"1"
```

退出redis-cli:

```
127.0.0.1:6379> exit
root@db18e24f57c6:/data# 
```

退出容器:

```
root@db18e24f57c6:/data# exit
exit
```

## 1.3.5 停止、重启和删除Redis容器

停止容器:

```
docker stop myFirstRedis
myFirstRedis
```

查看结果:

```
docker ps -a           
CONTAINER ID   IMAGE                        COMMAND                  CREATED         STATUS                      PORTS     NAMES
db18e24f57c6   redis:latest                 "docker-entrypoint.s…"   5 minutes ago   Exited (0) 28 seconds ago             myFirstRedis
```

再次启动容器:

```
docker start myFirstRedis
myFirstRedis
```

注:`docker restart myFirstRedis`也可以再次启动一个被停止的容器.但与`docker start`的区别在于:`docker start`会挂载容器所关联的文件系统,而`docker restart`则不会

在Redis这个场景下,若更改了Redis启动时所需加载的配置项参数,则在重启时就需要先`docker stop`再`docker start`.直接`docker restart`则不一定会加载更改后的配置项

## 1.3.6 查看Redis的版本

查看Redis服务端版本:

```
root@db18e24f57c6:/data# docker exec -it myFirstRedis /bin/bash
redis-server --version
Redis server v=6.2.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=b61f37314a089f19
```

查看Redis客户端版本:

```
root@db18e24f57c6:/data# redis-cli --version
redis-cli 6.2.6
```

## 1.3.7 Redis服务器和客户端

Redis是基于键值对存储的NoSQL数据库,其中的数据是存储在Redis服务器里的.和传统的MySQL数据库服务器相似,**一个Redis服务器可以同多个客户端创建连接**.

通过客户端停止Redis服务端:

```
root@db18e24f57c6:/data# redis-cli
127.0.0.1:6379> shutdown
```

```
docker ps -a
CONTAINER ID   IMAGE                        COMMAND                  CREATED          STATUS                     PORTS     NAMES
db18e24f57c6   redis:latest                 "docker-entrypoint.s…"   20 minutes ago   Exited (0) 9 seconds ago             myFirstRedis
```

当通过`docker run -itd --name myFirstRedis -p 6379:6379 redis:latest`和`docker start myFirstRedis`这两个命令启动Redis容器后,包含在容器里的Redis服务器会自动启动.


# 第2章 实践Redis的基本数据类型


# 2.1 Redis缓存初体验

作为基于键值对的NoSQL数据库,Redis支持五种数据类型:字符串(string)类型、哈希(hash)类型、列表(list)类型、集合(set)类型和有序集合(sorted set或zset)类型.

## 2.1.1 用redis-cli启动客户端并缓存数据

启动容器:

```
docker exec -it myFirstRedis /bin/bash
root@db18e24f57c6:/data# 
```

连接Redis并使用string类型进行存储:

```
root@db18e24f57c6:/data# redis-cli
127.0.0.1:6379> set CSDN https://www.csdn.net/
OK
127.0.0.1:6379> set baidu www.baidu.com
OK
127.0.0.1:6379> get CSDN
"https://www.csdn.net/"
127.0.0.1:6379> get baidu
"www.baidu.com"
```

注意:这里的对应关系是存储(或者称为缓存)在Redis服务器上的,且本例中使用的是"string"类型来缓存数据.

## 2.1.2 设置数据的生存时间

在命令后使用`ex`或`px`参数来设置该对象的生存周期.其中:

* `ex`: 设置生存周期的单位为秒
* `px`: 设置生存周期的单位为毫秒

例:设置`val`对象的生存周期为5s:

```
127.0.0.1:6379> set val 100 ex 5
OK
```

例:设置`valWithShort`对象的生存周期为100ms:

```
127.0.0.1:6379> set valWithShort 200 px 100
OK
```

过了5秒后尝试使用`get`命令获取`val`对象和`valWithShort`对象的值:

```
127.0.0.1:6379> get val
(nil)
```

```
127.0.0.1:6379> get valWithShort
(nil)
```

可以看到得到的表示`null`的`nil`值


# 2.2 针对字符串的命令

Redis是基于"键值对"的NoSQL.而此处的"string"是指"键值对"中的"值"是以"string"形式存储数据的.

## 2.2.1 读写字符串的set和get命令

设置字符串对象的语法:

```
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
```

其中:

* `key`: 键名.若对应的key中已经有值,那么再次执行`SET`命令时会用新的value替换旧的value
* `value`: **字符串类型的**值
* `EX/PX`: 设置生存周期.其中`EX`的单位为秒;`PX`的单位为毫秒
* `NX`: 当`key`不存在时才进行设置值的操作,若key存在则该命令不执行
* `XX`: 与`NX`相反,表示当key存在时才进行操作
* `KEEPTTL`: 在设置新的键值对时,保持原有的TTL(Time To Live,生存时间)不变.
  * 该选项为Redis 6.0新增选项.当使用`SET`命令对一个已经存在的key设置value时,若该key已经存在生存周期,则新的`SET`命令会移除这个生存周期.此时加上`KEEPTTL`选项,则表示保持该key原有的生存周期不变.

```
127.0.0.1:6379> SET name foo ex 5
OK
127.0.0.1:6379> GET name bar keepttl
OK
127.0.0.1:6379> SET name
"bar"
```

需求:工号为001的员工姓名为Mike,这条数据是存储在Emp表里的,但是每次查询该数据时需要读表,这会影响数据库的性能,所以需要在Redis里缓存该条数据.

设置了错误的员工姓名:

```
127.0.0.1:6379> SET 001 'Mary'
OK
```

注意:此处的`'Mary'`带有`'`,表示设置的值类型为字符串

此处的返回值`OK`表示设置成功

尝试将姓名修改为Mike:

```
127.0.0.1:6379> SET 001 'Mike' NX
(nil)
```

此处的返回值`nil`表示设置不成功

```
127.0.0.1:6379> SETNX 001 'Mike'
(integer) 0
```

此处的`setNX key value`和`set key value NX`是等价的.返回值`(integer) 0`同样表示设置不成功

查看`key`为`001`的值:

```
127.0.0.1:6379> GET 001
"Mary"
```

将`key`为`001`的值替换为Mike:

```
127.0.0.1:6379> SET 001 'Mike'
OK
```

查看结果:

```
127.0.0.1:6379> GET 001
"Mike"
```

例:将工号为002、姓名为Tom的数据写入Redis:

确认key是否存在:

```
127.0.0.1:6379> GET 002
(nil)
```

此处返回值`nil`表示没有找到key为`002`的数据

当key为`002`的数据存在时设置其值为`'Tom'`:

```
127.0.0.1:6379> SET 002 'Tom' XX
(nil)
```

此处由于key为`002`的数据不存在,故不进行操作,该`SET`命令返回值为`nil`表示没有进行操作

设置key为002的value为`'Tom'`,生存周期为10ms:

```
127.0.0.1:6379> SET 002 'Tom' PX 10
OK
```

10ms后获取key为002的value:

```
127.0.0.1:6379> GET 002
(nil)
```

此处返回`nil`表示没有获取到对应的值

设置key为002的value为`'Tom'`,生存周期为12分钟:

```
127.0.0.1:6379> SET 002 'Tom' EX 60*12
(error) ERR value is not an integer or out of range
```

报错原因:生存周期必须为一个数值,不能是一个表达式

```
127.0.0.1:6379> SET 002 'Tom' EX 720
OK
```

可以看到,值设置成功.

查看结果:

```
127.0.0.1:6379> GET 002
"Tom"
```

## 2.2.2 设置和获取多个字符串的命令

* `MSET`:设置多个字符串
  * 语法:`MSET key value [key value]`
* `MGET`:获取多个字符串
  * 语法:`MGET key [key]`

注意:`MSET`、`MGET`命令不包含`NX`、`XX`、`EX`、`PX`等参数

例:

同时对`003`和`004`这2个key设置string类型的值:

```
127.0.0.1:6379> MSET 003 'Peter' 004 Mary EX 10
OK
```

注:此处虽然使用`EX`参数设置了生命周期,但实际上这个生命周期不会生效

注:可以看到,字符串可以用`"`或`'`包含,也可以不包含.效果相同

10秒后使用`MGET`命令获取key为003和004的value:

```
127.0.0.1:6379> MGET 003 004
1) "Peter"
2) "Mary"
```

可以看到,虽然使用`MSET`命令设置了生命周期为10s,但并未生效.10s后依旧能通过key取到value

使用`MSET`指令时同时指定`NX`或`XX`参数:

```
127.0.0.1:6379> MSET 003 "Peter" 006 'JohnSon' NX
(error) ERR wrong number of arguments for MSET
```

```
127.0.0.1:6379> MSET 003 "Peter" 006 'JohnSon' XX
(error) ERR wrong number of arguments for MSET
```

可以看到,`MSET`命令不支持`NX`和`XX`参数

同时对`007`和`008`这2个key设置string类型的值:

```
127.0.0.1:6379> MSET 007 "John" 008 'Tim' PX 10
OK
```

注:此处虽然使用`PX`参数设置了生命周期,但实际上这个生命周期不会生效

10ms后使用`MGET`命令获取key为`007`和`008`的value:

```
127.0.0.1:6379> MGET 007 008
1) "John"
2) "Tim"
```

可以看到,虽然使用`MSET`命令时通过`PX`选项设置了生命周期为10ms,但并未生效.10ms后依旧能通过key取到value.**即:`PX`和`EX`参数不会生效**

## 2.2.3 对值进行增量和减量操作

* `INCR key`:对key所对应的**数字类型值**进行加1操作
* `DECR key`:对key所对应的**数字类型值**进行减1操作
* `INCRBY key increment`:对key对应的值进行加increment的操作
* `DECRBY key decrement`:对key对应的值进行减decrement的操作

```
127.0.0.1:6379> GET visit
(nil)
```

对visit变量进行加1操作:

```
127.0.0.1:6379> INCR visit
(integer) 1
```

对visit进行加10操作:

```
127.0.0.1:6379> INCRBY visit 10
(integer) 11
```

对visit进行减1操作:

```
127.0.0.1:6379> DECR visit
(integer) 10
```

对visit进行减5操作:

```
127.0.0.1:6379> DECRBY visit 5
(integer) 5
```

```
127.0.0.1:6379> GET visit
"5"
```

将`INCR`命令和`DECR`命令作用在字符串类型上:

```
127.0.0.1:6379> SET visitPersion 'Peter'
OK
127.0.0.1:6379> INCR visitPersion
(error) ERR value is not an integer or out of range
127.0.0.1:6379> DECR visitPersion
(error) ERR value is not an integer or out of range
```

可以看到,**将`INCR`命令和`DECR`命令作用在字符串类型上会报错**.

## 2.2.4 通过getset命令设置新值

`GETSET`:若key对应的值存在,则用给定的值覆盖旧的值,同时返回旧的值;若key对应的值不存在,也会设置值,但会返回`nil`.语法:`GETSET key value`

```
127.0.0.1:6379> GETSET 009 'Alex'
(nil)
```

```
127.0.0.1:6379> GET 009
"Alex"
```

```
127.0.0.1:6379> GETSET 009 'Frank'
"Alex"
```

```
127.0.0.1:6379> GET 009
"Frank"
```

## 2.2.5 针对字符串的其他操作

* `GETRANGE`:
  * 语法:`GETRANGE key start end`
  * 功能:获取key的子字符串.返回key对应的值从start位置开始到end位置为止的子字符串.其中位置的计算从0开始.返回的子字符串包含start位置和end位置

例:

```
127.0.0.1:6379> SET tel 021-12345678
OK
```

```
127.0.0.1:6379> GETRANGE tel 4 12
"12345678"
```

* `SETRANGE`:
  * 语法:`SETRANGE key offset value`
  * 功能:从offset位置开始,把值替换为value.该命令的返回值是字符串的长度

例:

```
127.0.0.1:6379> GET tel
"021-12345678"
```

```
127.0.0.1:6379> SETRANGE tel 4 87654321
(integer) 12
```

```
127.0.0.1:6379> GET tel
"021-87654321"
```

注:若offset超出了字符串的长度,则会用空白字符(`\x00`)进行填充,填充至到达指定的偏移量后再进行替换.这个过程可能会导致字符串长度的增加.

```
127.0.0.1:6379> SETRANGE tel 15 -2468
(integer) 20
```

```
127.0.0.1:6379> GET tel
"021-87654321\x00\x00\x00-2468"
```

* `STRLEN`:
  * 语法:`STRLEN key`
  * 功能:返回字符串的长度

```
127.0.0.1:6379> STRLEN tel
(integer) 20
```

* `APPEND`
  * 语法:`APPEND key value`
  * 功能:将value追加到原值的末尾.该命令的返回值是追加后的字符串的长度

```
127.0.0.1:6379> GET tel
"021-87654321\x00\x00\x00-2468"
```

```
127.0.0.1:6379> GET tel
"021-87654321\x00\x00\x00-2468-3579"
```

注:若对一个不存在的key使用`APPEND`指令,则等价于`SET`指令

```
127.0.0.1:6379> GET keym
(nil)
```

```
127.0.0.1:6379> APPEND keym content
(integer) 7
```

```
127.0.0.1:6379> GET keym
"content"
```


# 2.3 针对哈希类型变量的命令

## 2.3.1 设置并获取哈希值

* `HSET`:设置哈希值.语法:`HSET key field value [field value ...]`.其中:
  * `key`:待缓存对象的键名
  * `field value`:以键值对形式描述的对象数据.针对同一个key,可以用多个`field value`对来存储数据.这里的`field`可以理解成对象的属性名,`value`可以理解为对象的属性值
  * 注:若一个key已经被定义为类型A,再使用其他类型的指令操作该key时将会报错
  * 返回值:
    * `1`:表示成功设置了置顶field的值.若field之前不存在,则创建新的field并设置值
    * `0`:表示字段已存在,`HSET`命令将更新字段的值
    * `-1`:表示命令执行出错或参数错误
* `HGET`:读取哈希值.语法:`HGET key field`.其中:
  * `key`:待读取对象的键名
  * 如果存在key和field所对应的数据,则返回该数据,否则返回nil

例:

* 存储一个工号为001的员工信息.其中:
  * 姓名:peter
  * 薪水:10000
  * 部门:dataTeam

```
127.0.0.1:6379> HSET 001 name 'peter' salary 10000 dep dataTeam
(integer) 3
```

可以看到,`HSET`命令的返回值为设置的字段数量

* 查询该员工的姓名、薪水、部门:

```
127.0.0.1:6379> HGET 001 name
"peter"
```

```
127.0.0.1:6379> HGET 001 salary
"10000"
```

```
127.0.0.1:6379> HGET 001 dep
"dataTeam"
```

* 查询一个不存在的key:

```
127.0.0.1:6379> HGET 002 name
(nil)
```

可以看到,返回值为nil

* 查询一个存在的key中不存在的field:

```
127.0.0.1:6379> HGET 001 age
(nil)
```

可以看到,返回值也为nil

* 查询哈希值时只传入key而不传入field:

```
127.0.0.1:6379> HGET 001
(error) ERR wrong number of arguments for 'hget' command
```

## 2.3.2 hsetnx命令

* `HSETNX`
  * 语法:`HSETNX key field value`
  * 功能:只有当key和field所对应的value不存在时才设置value.设置成功返回1,设置失败返回0
* 例:重新设置工号为001的员工姓名

查看当前姓名:

```
127.0.0.1:6379> HGET 001 name
"peter"
```

使用`HSET`命令设置姓名:

```
127.0.0.1:6379> HSET 001 name 'johnson'
(integer) 0
127.0.0.1:6379> HGET 001 name
"johnson"
```

可以看到,使用`HSET`命令可以更新field值

* 例:若工号为002的员工姓名不存在,则设置员工姓名

```
127.0.0.1:6379> HSETNX 002 name 'tom'
(integer) 1
```

可以看到,此时由于key为`002`、field为`name`的值不存在,故设置成功,返回1

```
127.0.0.1:6379> HSETNX 002 name 'tom'
(integer) 0
```

可以看到,此时由于key为`002`、field为`name`的值已存在,故设置失败,返回0

注:**`HSETNX`命令的key后边只能跟一对`field value`**

```
127.0.0.1:6379> HSETNX 002 salary 9000 dep dataTeam
(error) ERR wrong number of arguments for 'hsetnx' command
```

## 2.3.3 针对key的相关操作

* `HKEYS key`
  * 功能:查看key对应的哈希类型数据的所有field
* `HVALS key`:
  * 功能:查看key对应的哈希类型的所有value
* `HGETALL key`:
  * 功能:以field和value对的形式查看key对应的哈希类型数据

例:

* 设置一个哈希类型的数据:

```
127.0.0.1:6379> HSET 010 name 'mary' salary 8000
(integer) 2
```

* 查看key对应的所有field:

```
127.0.0.1:6379> HKEYS 010
1) "name"
2) "salary"
```

* 查看key所对应的所有value:

```
127.0.0.1:6379> HVALS 010
1) "mary"
2) "8000"
```

* 查看key所对应的field-value对:

```
127.0.0.1:6379> HGETALL 010
1) "name"
2) "mary"
3) "salary"
4) "8000"
```

* 对一个不存在的key执行`HKEYS`、`HVALS`、`HGETALL`指令:

```
127.0.0.1:6379> HKEYS 00
(empty array)
127.0.0.1:6379> HVALS 00
(empty array)
127.0.0.1:6379> HGETALL 00
(empty array)
```

可以看到,当无法找到key时,会返回`empty array`

## 2.3.4 用hexists命令判断值是否存在

* `HEXISTS`:
  * 语法:`HEXISTS key field`
  * 功能:判断key和field对应的value是否存在.存在返回1,反之则返回0

例:

```
127.0.0.1:6379> HEXISTS 010 name
(integer) 1
```

```
127.0.0.1:6379> HEXISTS 00 name
(integer) 0
```

```
127.0.0.1:6379> HEXISTS 00
(error) ERR wrong number of arguments for 'hexists' command
```

若调用`HEXISTS`命令时只写了1个参数则会报错

## 2.3.5 对哈希类型数据的删除操作

* `HDEL`:
  * 语法:`HDEL key field [field ...]`
  * 功能:删除key指定的field数据.该命令可以同时删除1个key对应的多个field数据.若想删除key对应的整个哈希类型数据,则需使用`DEL key`命令(该命令可以对所有类型的数据使用,删除成功返回1;删除失败返回0)

例:

删除key为001所对应的哈希类型的name和salary这2个field:

```
127.0.0.1:6379> HDEL 001 name salary
(integer) 2
```

删除key对应的哈希类型中一个不存在的field:

```
127.0.0.1:6379> HDEL 001 gender
(integer) 0
```

调用`HDEL`指令时只填写了1个参数:

```
127.0.0.1:6379> HDEL 001
(error) ERR wrong number of arguments for 'hdel' command
```

可以看到,会报错

删除key对应的整个哈希类型数据:

```
127.0.0.1:6379> DEL 001
(integer) 1
```

查看删除的结果:

```
127.0.0.1:6379> HVALS 001
(empty array)
```


# 2.4 针对列表类型变量的命令

## 2.4.1 读写列表的命令

* `LPUSH`:
  * 语法:`LPUSH key element [element ...]`
  * 功能:将1个或多个值依次插入列表头部.其中key指定待插入的列表,element表示插入到列表的值.返回值为插入操作后的列表长度.
* `LINDEX`:
  * 语法:`LINDEX key index`
  * 功能:读取列表的值.其中key指定待读取的列表,index指定列表值的索引号.注意索引号是从0开始的.若key或index不存在则返回nil.

例:

插入列表操作:

查看LIST:

```
127.0.0.1:6379> LRANGE 001 0 -1
(empty array)
```

可以看到,此时key为001的list是不存在的

向list的头部插入一个元素:

```
127.0.0.1:6379> LPUSH 001 "dataTeam"
(integer) 1
```

再向list的头部插入一个:

```
127.0.0.1:6379> LPUSH 001 15000
(integer) 2
```

再向list的头部插入一个:

```
127.0.0.1:6379> LPUSH 001 'Peter'
(integer) 3
```

查看list中索引为0的元素的值:

```
127.0.0.1:6379> LINDEX 001 0
"Peter"
```

查看list中索引为1的元素的值:

```
127.0.0.1:6379> LINDEX 001 1
"15000"
```

查看list中索引为2的元素的值:

```
127.0.0.1:6379> LINDEX 001 2
"dataTeam"
```

`LINDEX`指令不指定索引则报错:

```
127.0.0.1:6379> LINDEX 001
(error) ERR wrong number of arguments for 'lindex' command
```

`LPUSH`指令一次设置多个元素:

```
127.0.0.1:6379> LPUSH 002 'dataTeam' 12000 'Mary'
(integer) 3
```

查看list中索引为1的元素的值:

```
127.0.0.1:6379> LINDEX 002 1
"12000"
```

* `RPUSH`:
  * 语法:`RPUSH key element [element ...]`
  * 功能:将1个或多个值依次插入到列表尾部.其中key指定待插入的列表,element表示插入到列表的值.返回值为插入操作后的列表长度.

例:

一次向list尾部插入多个元素:

```
127.0.0.1:6379> RPUSH 003 'Tim' 20000 'Hr Team'
(integer) 3
```

获取list中索引为0的元素的值:

```
127.0.0.1:6379> LINDEX 003 0
"Tim"
```

获取list中索引为1的元素的值:

```
127.0.0.1:6379> LINDEX 003 1
"20000"
```

获取list中索引为2的元素的值:

```
127.0.0.1:6379> LINDEX 003 2
"Hr Team"
```

获取list中不存在的索引:

```
127.0.0.1:6379> LINDEX 003 3
(nil)
```

## 2.4.2 lpushx和rpushx命令

* `LPUSHX`
  * 语法:`LPUSHX key element [element ...]`
  * 功能:仅当key存在时向头部插入数据.返回值为插入操作后的列表长度.可以认为返回0则表示插入操作没有执行.
* `RPUSHX`:
  * 语法:`RPUSHX key element [element ...]`
  * 功能:仅当key存在时向尾部插入数据.返回值为插入操作后的列表长度.可以认为返回0则表示插入操作没有执行.

例:

删除key为003的list:

```
127.0.0.1:6379> DEL 003
(integer) 1
```

当key存在时向头部插入:

```
127.0.0.1:6379> LPUSHX 003 'dataTeam'
(integer) 0
```

向头部插入:

```
127.0.0.1:6379> LPUSH 003 'dataTeam'
(integer) 1
```

当key存在时向头部插入:

```
127.0.0.1:6379> LPUSHX 003 10000
(integer) 2
```

查看索引为1的元素的值:

```
127.0.0.1:6379> LINDEX 003 1
"dataTeam"
```

删除key为004的list:

```
127.0.0.1:6379> DEL 004
(integer) 0
```

当key存在时向尾部插入:

```
127.0.0.1:6379> RPUSHX 004 'Tim'
(integer) 0
```

向尾部插入:

```
127.0.0.1:6379> RPUSH 004 'Tim'
(integer) 1
```

当key存在时向尾部插入:

```
127.0.0.1:6379> RPUSHX 004 15000
(integer) 2
```

查看索引为1的元素的值:

```
127.0.0.1:6379> LINDEX 004 1
"15000"
```

## 2.4.3 用list模拟堆栈和队列

* `LPOP`:
  * 语法:`LPOP key`
  * 功能:从list头部弹出元素.若list中没有元素,则返回nil
* `RPOP`:
  * 语法:`RPOP key`
  * 功能:从list尾部弹出元素.若list中没有元素,则返回nil

例:使用`LPUSH`和`LPOP`命令模拟一个栈:

模拟入栈:

```
127.0.0.1:6379> LPUSH myStack 1
(integer) 1
127.0.0.1:6379> LPUSH myStack 2
(integer) 2
127.0.0.1:6379> LPUSH myStack 3
(integer) 3
```

模拟出栈:

```
127.0.0.1:6379> LPOP myStack
"3"
127.0.0.1:6379> LPOP myStack
"2"
127.0.0.1:6379> LPOP myStack
"1"
```

可以看到,模拟了栈的先入后出的特性

对一个没有元素的list使用`LPOP`:

```
127.0.0.1:6379> LPOP myStack
(nil)
```

例:使用`LPUSH`和`RPOP`命令模拟一个队列:

模拟入队:

```
127.0.0.1:6379> LPUSH myQueue 1
(integer) 1
127.0.0.1:6379> LPUSH myQueue 2
(integer) 2
127.0.0.1:6379> LPUSH myQueue 3
(integer) 3
```

模拟出队:

```
127.0.0.1:6379> RPOP myQueue
"1"
127.0.0.1:6379> RPOP myQueue
"2"
127.0.0.1:6379> RPOP myQueue
"3"
```

可以看到,模拟了队列的先入先出的特性

对一个没有元素的list使用`RPOP`:

```
127.0.0.1:6379> RPOP myQueue
(nil)
```

## 2.4.4 用lrange命令获取指定区间内的数据

* `LRANGE`:
  * 语法:`LRANGE key start stop`
  * 功能:获取key对应的list中指定区间内的数据.其中:
    * start:开始的索引
    * stop:结束的索引
    * 包含start位置和stop位置的元素

例:

删除list:

```
127.0.0.1:6379> DEL 003
(integer) 1
```

从尾部插入3个元素:

```
127.0.0.1:6379> RPUSH 003 'dataTeam' 15000 'Mary'
(integer) 3
```

获取从第0个索引到第1个索引的元素:

```
127.0.0.1:6379> LRANGE 003 0 1
1) "dataTeam"
2) "15000"
```

获取从第0个索引到第2个索引的元素:

```
127.0.0.1:6379> LRANGE 003 0 2
1) "dataTeam"
2) "15000"
3) "Mary"
```

获取从第0个索引到第4个索引的元素:

```
127.0.0.1:6379> LRANGE 003 0 4
1) "dataTeam"
2) "15000"
3) "Mary"
```

可以看到,此时由于list中只有3个元素,故只返回了3个元素的值.

start的索引值大于stop的索引值时:

```
127.0.0.1:6379> LRANGE 003 4 0
(empty array)
```

可以看到,当start的索引值大于stop的索引值时,返回值为`empty array`

## 2.4.5 用lset命令修改列表数据

* `LSET`:
  * 语法:`LSET key index element`
  * 功能:将key对应的list中指定index位置上的元素修改为element.若指定的key不存在,则报错key不存在;若index在list中不存在,则报错索引越界.

例:

删除list:

```
127.0.0.1:6379> DEL 003
(integer) 1
```

从list尾部插入元素:

```
127.0.0.1:6379> RPUSH 003 "Mike" 15000
(integer) 2
```

查看索引为1的元素:

```
127.0.0.1:6379> LINDEX 003 1
"15000"
```

修改索引为1的元素值:

```
127.0.0.1:6379> LSET 003 1 18000
OK
```

查看修改结果:

```
127.0.0.1:6379> LINDEX 003 1
"18000"
```

查看键名为003的List中的所有元素:

```
127.0.0.1:6379> LRANGE 003 0 -1
1) "Mike"
2) "18000"
```

可以看到,此时List中仅有2个元素

对List中一个不存在的索引修改元素值:

```
127.0.0.1:6379> LSET 003 5 20000
(error) ERR index out of range
```

可以看到,报错索引越界

对一个不存在的List修改其索引对应的元素值:

```
127.0.0.1:6379> LSET 005 1 12000
(error) ERR no such key
```

可以看到,对一个不存在的List修改其索引对应的元素值将报错key不存在

## 2.4.6 删除列表数据的命令

* `LPOP`
  * 语法:`LPOP key [count]`
  * 功能:返回并删除key对应的List头部的前count个元素.若list中没有元素或key对应的list不存在,则返回nil
* `RPOP`
  * 语法: `RPOP key [count]`
  * 功能:返回并删除key对应的List尾部的后count个元素.若list中没有元素或key对应的list不存在,则返回nil

例:

删除key对应的list:

```
127.0.0.1:6379> DEL 003
(integer) 1
```

从list尾部插入元素:

```
127.0.0.1:6379> RPUSH 003 "Mike" 15000 "dataTeam" "male"
(integer) 4
```

删除list头部的2个元素:

```
127.0.0.1:6379> LPOP 003 2
1) "Mike"
2) "15000"
```

查看删除后的结果:

```
127.0.0.1:6379> LRANGE 003 0 -1
1) "dataTeam"
2) "male"
```

删除list尾部的元素:

```
127.0.0.1:6379> RPOP 003
"male"
127.0.0.1:6379> RPOP 003
"dataTeam"
```

```
127.0.0.1:6379> RPOP 003
(nil)
```

可以看到,当list中没有元素时,执行删除操作的返回值为nil

查看删除后的结果:

```
127.0.0.1:6379> LRANGE 003 0 -1
1) "dataTeam"
```

对一个不存在的key使用删除元素操作:

```
127.0.0.1:6379> DEL 010
(integer) 0
```

```
127.0.0.1:6379> LPOP 010
(nil)
127.0.0.1:6379> RPOP 010 2
(nil)
```

可以看到,对一个不存在的key使用删除元素操作时,将返回nil

* `LREM`
  * 语法:`LREM key count element`.其中:
    * key:指向待删除的list
    * count:
      * `count > 0`时:从头到尾方向删除,删除数量为count个、值为element的元素
      * `count < 0`时:从尾到头方向删除,删除数量为count个、值为element的元素
      * `count = 0`时:删除列表中所有值为element的元素

例:

删除list:

```
127.0.0.1:6379> DEL 001
(integer) 1
```

创建list:

```
127.0.0.1:6379> LPUSH 001 1 1 2 2 1
(integer) 5
```

删除list中所有值为1的元素:

```
127.0.0.1:6379> LREM 001 0 1
(integer) 3
```

查看删除元素后的list:

```
127.0.0.1:6379> LRANGE 001 0 -1
1) "2"
2) "2"
```

删除并重新创建list:

```
127.0.0.1:6379> DEL 001
(integer) 1
127.0.0.1:6379> LPUSH 001 1 1 2 2 1
(integer) 5
```

从头到尾方向删除2个值为1的元素:

```
127.0.0.1:6379> LREM 001 2 1
(integer) 2
```

查看删除结果:

```
127.0.0.1:6379> LRANGE 001 0 -1
1) "2"
2) "2"
3) "1"
```

删除并重新创建list:

```
127.0.0.1:6379> DEL 001
(integer) 1
127.0.0.1:6379> LPUSH 001 1 1 2 2 1
(integer) 5
```

从尾到头方向删除2个值为1的元素:

```
127.0.0.1:6379> LREM 001 -2 1
(integer) 2
```

查看删除结果:

```
127.0.0.1:6379> LRANGE 001 0 -1
1) "1"
2) "2"
3) "2"
```


# 2.5 针对集合的命令

set:和list相同,也是在同一个key下存储多个元素;不同之处在于,set中存储的元素**不能重复**,且set是**无序**的

## 2.5.1 读写集合的命令

* `SADD`
  * 语法:`SADD key member [member ...]`
  * 功能:向key指定的集合中添加1个或多个元素
* `SMEMBERS`
  * 语法:`SMEMBERS key`
  * 功能:读取key对应集合里的所有数据

例:

向集合中添加元素:

```
127.0.0.1:6379> SADD teamName 'HR' 'Account' 'DataTeam' 'HR'
(integer) 3
```

注意此时添加的元素是有重复的.因此最终添加结果显示有3个元素被添加到了集合中

查看集合中的元素:

```
127.0.0.1:6379> SMEMBERS teamName
1) "Account"
2) "HR"
3) "DataTeam"
```

可以看到,读取时的顺序和写入时的顺序是不同的.

## 2.5.2 列表和集合类数据的使用场景

* 列表在写入时是有序的;集合是无序的
* 列表用于按一定规范存储同一类数据

例如:用`Name`,`Salary`,`TeamName`的规范存储同一类员工的数据:

```
127.0.0.1:6379> RPUSH 001 "Mike" 12000 "dataTeam"
(integer) 3
127.0.0.1:6379> RPUSH 002 "David" 13000 "businessTeam"
(integer) 3
127.0.0.1:6379> RPUSH 003 "Peter" 14000 "userTeam"
(integer) 3
```

在本例中,这3个列表中的第1个元素均为员工姓名;第2个元素均为员工工资;第3个元素均为员工部门

* 集合用于存储并列数据

例如:存储公司名称

```
127.0.0.1:6379> SADD companyName "Apple" "Google" "Facebook"
(integer) 3
```

```
127.0.0.1:6379> SMEMBERS companyName
1) "Apple"
2) "Facebook"
3) "Google"
```

在本例中,Apple、Facebook、Google均为公司名称,是并列关系

## 2.5.3 用`SISMEMBER`命令判断元素是否存在

集合是无序的,因此"读取指定索引的元素"的命令是没有意义的,因为存入集合的次序和输出次序不一定相同.

* `SISMEMBER`
  * 语法:`SISMEMBER key member`
  * 功能:判断某个元素是否在集合中.元素存在于集合中则返回1,否则返回0

例:

向set中写入:

```
127.0.0.1:6379> DEL teamName
(integer) 1
127.0.0.1:6379> SADD teamName 'HR' 'Account' 'DataTeam'
(integer) 3
```

判断给定的元素值在set中是否存在:

```
127.0.0.1:6379> SISMEMBER teamName HR
(integer) 1
127.0.0.1:6379> SISMEMBER teamName Dev
(integer) 0
```

## 2.5.4 获取集合的交集、并集和差集

* `SINTER`
  * 语法:`SINTER key [key ...]`
  * 功能:获取多个key对应的set的交集
* `SUNION`
  * 语法:`SUNION key [key ...]`
  * 功能:获取多个key对应的set的并集
* `SDIFF`
  * 语法:`SDIFF key [key ...]`
  * 功能:获取多个key对应的set的差集

例:

创建2个set:

```
127.0.0.1:6379> SADD Mike Math English Computer
(integer) 3
127.0.0.1:6379> SADD Tom Computer Math Piano
(integer) 3
```

取2个集合的交集:

```
127.0.0.1:6379> SINTER Mike Tom
1) "Computer"
2) "Math"
```

取2个集合的并集:

```
127.0.0.1:6379> SUNION Mike Tom
1) "Math"
2) "Computer"
3) "English"
4) "Piano"
```

取2个集合的差集:

```
127.0.0.1:6379> SDIFF Mike Tom
1) "English"
127.0.0.1:6379> SDIFF Tom Mike
1) "Piano"
```

注意:差集的含义是存在于集合A但不存在于集合B中的元素.因此`SDIFF Mike Tom`和`SDIFF Tom Mike`的返回值是不同的

## 2.5.5 用`SREM`命令删除集合数据

* `SREM`
  * 语法:`SREM key member [member ...]`
  * 功能:删除key对应的集合中的数据.该命令的返回值为删除的元素个数

例:

创建一个set:

```
127.0.0.1:6379> SADD number 1 2 4 8 16
(integer) 5
```

从set中删除值为1、4、5的元素

```
127.0.0.1:6379> SREM number 1 4 5
(integer) 2
```

可以看到,返回值为2.因为set中没有值为5的元素

```
127.0.0.1:6379> SMEMBERS number
1) "2"
2) "8"
3) "16"
```

从一个不存在的set中删除元素:

```
127.0.0.1:6379> SREM notExist 1
(integer) 0
```

可以看到,若从一个不存在的set中删除元素,返回值为0

对非set类型的对象调用`SREM`命令:

```
127.0.0.1:6379> LPUSH myList 1
(integer) 1
127.0.0.1:6379> SREM myList 1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
```

可以看到,对非set类型的对象调用`SREM`命令则报错


# 2.6 针对有序集合的命令

zset:有序集合(sorted set),也叫zset.和集合有一定的相似性,其中都不能出现重复的元素.在有序集合中,每个元素都会对一个一个score参数,以该参数作为排序的依据

## 2.6.1 读写有序集合的命令

* `ZADD`
  * 语法:`ZADD key [NX|XX] [CH] [INCR] score member [score member ...]`.其中:
    * NX:当key对应的zset不存在时才能添加元素
    * XX:当key对应的zset存在时才能添加元素
    * CH:不指定该选项时,则`ZADD`默认只返回新添加到zset中的元素数量;指定该选项时,则`ZADD`命令将返回被更新或添加的元素数量
    * INCR:当待插入的member不存在时,该参数无效;当待插入的member已存在时,则表示指定一个增量添加到已有member的分数上
    * score:用于描述元素的数值(权重)
    * memeber:元素的值
* `ZRANGE`
  * 语法:`ZRANGE key start stop [WITHSCORES]`.其中:
    * start:起始索引位置
    * stop:结束索引位置
    * WITHSCORES:同时展示元素所对应的score值
    * 功能:通过索引区间返回zset中指定区间内的成员.该命令以元素在zset中score的**升序**排序

例:

创建一个zset:

```
127.0.0.1:6379> ZADD emp 4.0 Mike 2.0 Peter 1.0 Tim 0.5 Johnson 0.0 David
(integer) 5
```

按元素在zset中的score升序排序,查询索引在\[0,2]范围内的元素:

```
127.0.0.1:6379> ZRANGE emp 0 2
1) "David"
2) "Johnson"
3) "Tim"
```

按元素在zset中的score升序排序,查询索引在\[0,2]范围内的元素及其权重:

```
127.0.0.1:6379> ZRANGE emp 0 2 WITHSCORES
1) "David"
2) "0"
3) "Johnson"
4) "0.5"
5) "Tim"
6) "1"
```

可以看到,按索引从zset中读取元素时,是按照元素在zset中的score升序排序的

* `ZREVRANGE`
  * 语法:`ZREVRANGE key start stop [WITHSCORES]`.其中:
    * start:起始索引位置
    * stop:结束索引位置
    * WITHSCORES:同时展示元素所对应的score值
    * 功能:通过索引区间返回zset中指定区间内的成员.该命令以元素在zset中score的**降序**排序

例:

查看zset中的所有元素及其对应的权重:

```
127.0.0.1:6379> ZRANGE emp 0 -1 WITHSCORES
 1) "David"
 2) "0"
 3) "Johnson"
 4) "0.5"
 5) "Tim"
 6) "1"
 7) "Peter"
 8) "2"
 9) "Mike"
10) "4"
```

按元素在zset中的score降序排序,查询索引在\[0,2]范围内的元素:

```
127.0.0.1:6379> ZREVRANGE emp 0 2
1) "Mike"
2) "Peter"
3) "Tim"
```

按元素在zset中的score降序排序,查询索引在\[0,2]范围内的元素及其权重:

```
127.0.0.1:6379> ZREVRANGE emp 0 2 WITHSCORES
1) "Mike"
2) "4"
3) "Peter"
4) "2"
5) "Tim"
6) "1"
```

* `ZRANGEBYSCORE`
  * 语法:`ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]`.其中:
    * key:zset对应的key
    * min/max:类似查询条件.查询条件默认为:score ∈ \[min, max],可以使用`(min`的方式来调整区间的开闭
    * WITHSCORES:同上文
    * LIMIT:偏移量.offset表示偏移量,count表示取多少条
    * 功能:返回有序集中指定分数区间内的成员,分数从低到高排序

例:

使用`ZRANGEBYSCORE`命令查看zset中所有成员,score升序排序:

```
127.0.0.1:6379> ZRANGEBYSCORE emp -inf +inf WITHSCORES
 1) "David"
 2) "0"
 3) "Johnson"
 4) "0.5"
 5) "Tim"
 6) "1"
 7) "Peter"
 8) "2"
 9) "Mike"
10) "4"
```

使用`ZRANGEBYSCORE`命令查看zset中score ∈ \[0, 4]的成员及其权重:

```
127.0.0.1:6379> ZRANGEBYSCORE emp 0 4 WITHSCORES
 1) "David"
 2) "0"
 3) "Johnson"
 4) "0.5"
 5) "Tim"
 6) "1"
 7) "Peter"
 8) "2"
 9) "Mike"
10) "4"
```

使用`ZRANGEBYSCORE`命令查看zset中score ∈ (0, 4]的成员及其权重

```
127.0.0.1:6379> ZRANGEBYSCORE emp (0 4 WITHSCORES
1) "Johnson"
2) "0.5"
3) "Tim"
4) "1"
5) "Peter"
6) "2"
7) "Mike"
8) "4"
```

* `ZREVRANGEBYSCORE`
  * 语法:`ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]`.其中:
    * key:zset对应的key
    * max/min:类似查询条件.查询条件默认为:score ∈ \[max, min],可以使用`(min`的方式来调整区间的开闭
    * WITHSCORES:同上文
    * LIMIT:偏移量.offset表示偏移量,count表示取多少条
    * 功能:返回有序集中指定分数区间内的成员,分数从高到低排序

例:

使用`ZREVRANGEBYSCORE`命令查看zset中所有成员,score降序排序:

```
127.0.0.1:6379> ZREVRANGEBYSCORE emp +inf -inf WITHSCORES
 1) "Mike"
 2) "4"
 3) "Peter"
 4) "2"
 5) "Tim"
 6) "1"
 7) "Johnson"
 8) "0.5"
 9) "David"
10) "0"
```

使用`ZREVRANGEBYSCORE`命令查看zset中score ∈ \[4, 0]的成员及其权重:

```
127.0.0.1:6379> ZREVRANGEBYSCORE emp 4 0 WITHSCORES
 1) "Mike"
 2) "4"
 3) "Peter"
 4) "2"
 5) "Tim"
 6) "1"
 7) "Johnson"
 8) "0.5"
 9) "David"
10) "0"
```

使用`ZREVRANGEBYSCORE`命令查看zset中score ∈ \[4, 0)的成员及其权重:

```
127.0.0.1:6379> ZREVRANGEBYSCORE emp 4 (0 WITHSCORES
1) "Mike"
2) "4"
3) "Peter"
4) "2"
5) "Tim"
6) "1"
7) "Johnson"
8) "0.5"
```

## 2.6.2 通过`ZINCRBY`命令修改元素的分值

* `ZINCRBY`
  * 语法:`ZINCRBY key increment member`.其中:
    * key:zset对应的键名
    * increment:score的变化量(可以为负数)
    * member:成员值
    * 功能:zset中对指定成员的分数加上增量increment.该命令的返回值为成员修改后的score

例:

查看有序集合中所有成员及其对应的score,按score升序排序:

```
127.0.0.1:6379> ZRANGEBYSCORE emp -inf +inf WITHSCORES
 1) "David"
 2) "0"
 3) "Johnson"
 4) "0.5"
 5) "Tim"
 6) "1"
 7) "Peter"
 8) "2"
 9) "Mike"
10) "4"
```

修改成员David的score,使该成员的score值减1:

```
127.0.0.1:6379> ZINCRBY emp -1 David
"-1"
```

对一个不存在于zset中的成员使用`ZINCRBY`命令:

```
127.0.0.1:6379> ZINCRBY emp -1 Kobe
"-1"
```

查看结果:

```
127.0.0.1:6379> ZRANGEBYSCORE emp -inf +inf WITHSCORES
 1) "David"
 2) "-1"
 3) "Kobe"
 4) "-1"
 5) "Johnson"
 6) "0.5"
 7) "Tim"
 8) "1"
 9) "Peter"
10) "2"
11) "Mike"
12) "4"
```

可以看到,对一个不存在于zset中的成员使用`ZINCRBY`命令后,该成员会被添加到zset中,并且该成员的score即为increment

## 2.6.3 用`ZSCORE`命令获取指定成员的分数

* `ZSCORE`
  * 语法:`ZSCORE key member`
  * 功能:查看key对应的set中,member的score.若key或member不存在则返回nil

例:

查看zset中的全部成员:

```
127.0.0.1:6379> ZRANGEBYSCORE emp -inf +inf WITHSCORES
 1) "David"
 2) "-1"
 3) "Kobe"
 4) "-1"
 5) "Johnson"
 6) "0.5"
 7) "Tim"
 8) "1"
 9) "Peter"
10) "2"
11) "Mike"
12) "4"
```

查看成员David的score:

```
127.0.0.1:6379> ZSCORE emp David
"-1"
```

查看zset中不存在的memeber:

```
127.0.0.1:6379> ZSCORE emp Allen
(nil)
```

对一个不存在的zset使用`ZSCORE`命令:

```
127.0.0.1:6379> ZSCORE notExist foo
(nil)
```

注意:`ZSCORE`命令只能返回1个元素的score

## 2.6.4 查看有序集合里的元素排名

* `ZRANK`
  * 语法:`ZRANK key member`
  * 功能:获取指定成员在zset中的索引,排序按score升序排序.若key或member不存在则返回nil
* `ZREVRANK`
  * 语法:`ZREVRANK key member`
  * 功能:获取指定成员在zset中的索引,排序按score降序排序.若key或member不存在则返回nil

例:

查看zset中所有成员及其对应score,按score升序排序:

```
127.0.0.1:6379> ZRANGEBYSCORE emp -inf +inf WITHSCORES
 1) "David"
 2) "-1"
 3) "Kobe"
 4) "-1"
 5) "Johnson"
 6) "0.5"
 7) "Tim"
 8) "1"
 9) "Peter"
10) "2"
11) "Mike"
12) "4"
```

查看David在zset中的索引,按score升序排序:

```
127.0.0.1:6379> ZRANK emp David
(integer) 0
```

查看zset中所有成员及其对应score,按score降序排序:

```
127.0.0.1:6379> ZREVRANGEBYSCORE emp +inf -inf WITHSCORES
 1) "Mike"
 2) "4"
 3) "Peter"
 4) "2"
 5) "Tim"
 6) "1"
 7) "Johnson"
 8) "0.5"
 9) "Kobe"
10) "-1"
11) "David"
12) "-1"
```

查看David在zset中的索引,按score降序排序:

```
127.0.0.1:6379> ZREVRANK emp David
(integer) 5
```

查看在zset中不存在的成员的索引:

```
127.0.0.1:6379> ZRANK emp Allen
(nil)
```

可以看到,当成员在zset中不存在时,返回值为nil

对一个不存在的zset查看其成员中的索引:

```
127.0.0.1:6379> ZRANK number one
(nil)
```

可以看到,当zset不存在时,返回值为nil

## 2.6.5 删除有序集合里的值

* `ZREM`
  * 语法:`ZREM key member [member ...]`
  * 功能:删除key指向的zset中的1个或多个成员.返回值为删除成员的数量

例:

查看zset中所有成员及其对应的score:

```
127.0.0.1:6379> ZRANGEBYSCORE emp -inf +inf WITHSCORES
 1) "David"
 2) "-1"
 3) "Kobe"
 4) "-1"
 5) "Johnson"
 6) "0.5"
 7) "Tim"
 8) "1"
 9) "Peter"
10) "2"
11) "Mike"
12) "4"
```

删除值为David的成员:

```
127.0.0.1:6379> ZREM emp David
(integer) 1
```

删除值为Kobe和值为Johnson的成员:

```
127.0.0.1:6379> ZREM emp Kobe Johnson
(integer) 2
```

查看zset中所有成员及其对应的score:

```
127.0.0.1:6379> ZRANGEBYSCORE emp -inf +inf WITHSCORES
1) "Tim"
2) "1"
3) "Peter"
4) "2"
5) "Mike"
6) "4"
```

删除一个zset中不存在的成员:

```
127.0.0.1:6379> ZREM emp Allen
(integer) 0
```

可以看到,对一个zset中不存在的成员执行删除操作,返回值为0

对一个不存在的zset执行`ZREM`命令:

```
127.0.0.1:6379> ZREM notExist one
(integer) 0
```

可以看到,对一个不存在的zset执行`ZREM`命令,返回值为0

* `ZREMRANGEBYRANK`
  * 语法:`ZREMRANGEBYRANK key start stop`
  * 功能:删除zset中索引在\[start, stop]范围内的成员,返回值为删除元素的个数.索引默认按score升序排序,但如果start和stop为负数,则表示要删除score较高的成员

例:

删除并创建一个zset:

```
127.0.0.1:6379> DEL emp
(integer) 1
127.0.0.1:6379> ZADD emp 0 David 0.5 Johnson 1 Tim 2 Peter 4 Mike
(integer) 5
```

查看zset中所有成员及其对应的score:

```
127.0.0.1:6379> ZRANGE emp 0 5 WITHSCORES
 1) "David"
 2) "0"
 3) "Johnson"
 4) "0.5"
 5) "Tim"
 6) "1"
 7) "Peter"
 8) "2"
 9) "Mike"
10) "4"
```

删除zset中索引在\[1,3]范围内的成员,按score升序排序:

```
127.0.0.1:6379> ZREMRANGEBYRANK emp 1 3
(integer) 3
```

查看删除后的zset:

```
127.0.0.1:6379> ZRANGE emp 0 5 WITHSCORES
1) "David"
2) "0"
3) "Mike"
4) "4"
```

注:没有`ZREVREMRANGEBYRANK`命令,但是可以通过负数索引的方式删除分数最高的几个成员

例:

删除并重新创建zset:

```
127.0.0.1:6379> DEL emp
(integer) 1
127.0.0.1:6379> ZADD emp 0 David 0.5 Johnson 1 Tim 2 Peter 4 Mike
(integer) 5
```

查看zset中所有成员及其对应的score:

```
127.0.0.1:6379> ZRANGE emp 0 5 WITHSCORES
 1) "David"
 2) "0"
 3) "Johnson"
 4) "0.5"
 5) "Tim"
 6) "1"
 7) "Peter"
 8) "2"
 9) "Mike"
10) "4"
```

按分数从高到低排序,删除分数排名第2到分数排名第4的3个成员:

```
127.0.0.1:6379> ZREMRANGEBYRANK emp -4 -2
(integer) 3
```

查看删除结果:

```
127.0.0.1:6379> ZRANGE emp 0 5 WITHSCORES
1) "David"
2) "0"
3) "Mike"
4) "4"
```

* `ZREMRANGEBYSCORE`
  * 语法:`ZREMRANGEBYSCORE key min max`
  * 功能:删除zset中score在\[min, max]范围内的成员,返回值为删除元素的个数

例:

删除并重新创建一个zset:

```
127.0.0.1:6379> DEL emp
(integer) 1
127.0.0.1:6379> ZADD emp 0 David 0.5 Johnson 1 Tim 2 Peter 4 Mike
(integer) 5
```

查看zset中所有成员及其对应的score:

```
127.0.0.1:6379> ZRANGE emp 0 5 WITHSCORES
 1) "David"
 2) "0"
 3) "Johnson"
 4) "0.5"
 5) "Tim"
 6) "1"
 7) "Peter"
 8) "2"
 9) "Mike"
10) "4"
```

删除score在\[0,1]范围内的成员:

```
127.0.0.1:6379> ZREMRANGEBYSCORE emp 0 1
(integer) 3
```

查看删除后的zset:

```
127.0.0.1:6379> ZRANGE emp 0 5 WITHSCORES
1) "Peter"
2) "2"
3) "Mike"
4) "4"
```

删除score在\[2,4)范围内的成员:

```
127.0.0.1:6379> ZREMRANGEBYSCORE emp 2 (4
(integer) 1
```

查看删除后的zset:

```
127.0.0.1:6379> ZRANGE emp 0 5 WITHSCORES
1) "Mike"
2) "4"
```

注:没有`ZREVREMRANGEBYSCORE`命令.因为无论是按score升序还是降序排序,最终要删除的都是给定score范围内的数据


# 第3章 实践Redis的常用命令


# 3.1 键操作命令

## 3.1.1 用`EXISTS`命令判断键是否存在

* `EXISTS`
  * 语法:`EXISTS key`
  * 功能:判断指定的key是否存在.存在返回1,否则返回0

例:

设置一个名为name的key:

```
127.0.0.1:6379> SET name "Peter"
OK
```

判断该key是否存在:

```
127.0.0.1:6379> EXISTS name
(integer) 1
```

判断一个不存在的key:

```
127.0.0.1:6379> EXISTS grade
(integer) 0
```

## 3.1.2 用`KEYS`命令查找键

* `KEYS`
  * 语法:`KEYS pattern`
  * 功能:用通配符或正则表达式来查找指定模式的键

例:

添加3个以`0`开头的键值对:

```
127.0.0.1:6379> SET 001 Peter
OK
127.0.0.1:6379> SET 002 Mike
OK
127.0.0.1:6379> SET 003 Tim
OK
```

查找所有以`0`开头的key:

```
127.0.0.1:6379> KEYS 0*
1) "001"
2) "003"
3) "002"
```

查找以`n`开头,`n`后边有1个任意字符,该字符后边以`me`结尾的key:

```
127.0.0.1:6379> KEYS n?me
1) "name"
```

查找所有的key:

```
127.0.0.1:6379> KEYS *
1) "001"
2) "name"
3) "003"
4) "002"
```

## 3.1.3 用`SCAN`命令查找键

* `SCAN`
  * 语法:`SCAN cursor [MATCH pattern] [COUNT count]`.其中:
    * cursor:游标,游标起始值一般为0
    * pattern:指定匹配模式
    * count:`COUNT`选项并不是一个硬性约束,而是一个提示或者建议,表示你希望每次迭代返回的大致结果数量.但实际返回的键数量可能会有所不同.确切的讲,`COUNT`提供了Redis应该在每次迭代中检查的桶(buckets)数量.担着不表示返回的结果数量一定与`COUNT`的值相匹配
  * 功能:`SCAN`返回一个包含两个元素的数组,第一个元素是用于进行下一次迭代的新游标, 而第二个元素则是一个数组,这个数组中包含了所有被迭代的元素

原因如下:

1. Redis内部使用哈希表来存储键,而哈希表又基于桶(bucket)来工作
2. 当执行`SCAN`命令时,Redis会尝试在每次迭代中检查大约`COUNT`数量的桶
3. 某个桶可能为空,也可能包含多个键
4. 当使用`MATCH`选项时,只有匹配该模式的键会被返回.因此,即使一个桶中有多个键,也可能只有一部分键与给定模式匹配

因此,当你指定`COUNT 1`时,Redis会尝试检查大约1个桶的内容.但根据哈希表的实际状态和`MATCH`模式,返回的键数量可能会多于或少于`COUNT`的值

总的来说,应该将`COUNT`视为一个估计或建议,而不是一个确切的限制.如果你需要准确控制返回的键数量,你可能需要在客户端进行进一步的处理

例:

查看键名以0开头的所有键:

```
127.0.0.1:6379> KEYS 0*
 1) "001"
 2) "019"
 3) "014"
 4) "004"
 5) "020"
 6) "018"
 7) "006"
 8) "003"
 9) "013"
10) "015"
11) "011"
12) "008"
13) "016"
14) "005"
15) "009"
16) "002"
17) "007"
18) "012"
19) "010"
20) "017"
```

可以看到,以0开头的键共有20个.即001 - 020

使用`SCAN`命令迭代获取key:

```
127.0.0.1:6379> SCAN 20 MATCH 0* COUNT 1
1) "28"
2) 1) "005"
   2) "009"
```

可以看到,即使指定`COUNT 1`,返回的key也可能不止1个.

```
127.0.0.1:6379> SCAN 28 MATCH 0* COUNT 1
1) "2"
2) 1) "017"
```

```
127.0.0.1:6379> SCAN 2 MATCH 0* COUNT 1
1) "18"
2) 1) "019"
   2) "014"
```

```
127.0.0.1:6379> SCAN 18 MATCH 0* COUNT 1
1) "22"
2) 1) "011"
```

```
127.0.0.1:6379> SCAN 22 MATCH 0* COUNT 1
1) "14"
2) (empty array)
```

可以看到,在迭代的过程中是有可能返回空数组的

```
127.0.0.1:6379> SCAN 14 MATCH 0* COUNT 1
1) "5"
2) 1) "012"
```

```
127.0.0.1:6379> SCAN 5 MATCH 0* COUNT 1
1) "21"
2) 1) "015"
```

```
127.0.0.1:6379> SCAN 21 MATCH 0* COUNT 1
1) "13"
2) 1) "007"
```

```
127.0.0.1:6379> SCAN 13 MATCH 0* COUNT 1
1) "19"
2) 1) "004"
```

```
127.0.0.1:6379> SCAN 19 MATCH 0* COUNT 1
1) "11"
2) 1) "002"
```

```
127.0.0.1:6379> SCAN 11 MATCH 0* COUNT 1
1) "27"
2) 1) "016"
```

```
127.0.0.1:6379> SCAN 27 MATCH 0* COUNT 1
1) "7"
2) 1) "010"
```

```
127.0.0.1:6379> SCAN 7 MATCH 0* COUNT 1
1) "23"
2) 1) "008"
```

```
127.0.0.1:6379> SCAN 23 MATCH 0* COUNT 1
1) "0"
2) (empty array)
```

遍历结束的标志是cursor再次变为0

注意:

* `KEYS`命令以阻塞的方式来查找并返回键.因此当待查找的键数量很多时,耗时会较长,且在这段时间内无法执行其他命令(Redis是单线程的)
* `SCAN`命令以非阻塞的方式查找并返回键.换言之,在大部分场景下`SCAN`命令能替代`KEYS`命令.**如果待查找的键个数比较少,那么用`KEYS`命令尚可,否则建议使用`SCAN`命令**

## 3.1.4 重命名键

* `RENAME`
  * 语法:`RENAME key newkey`
  * 功能:若旧键名不存在,则返回错误;若newkey已存在,则用key对应的值覆盖newkey对应的值
* `RENAMENX`
  * 语法:`RENAMENX key newkey`
  * 功能:若旧键名不存在,则返回错误;若newkey已存在,则返回0,不执行重命名命令

例:使用`RENAME`重命名键

设置键:

```
127.0.0.1:6379> SET visitPerson Peter
OK
```

重命名键:

```
127.0.0.1:6379> RENAME visitPerson VIPPerson
OK
```

查看旧的key和新的key的存在情况:

```
127.0.0.1:6379> EXISTS visitPerson
(integer) 0
```

可以看到,旧的key已经不存在了

```
127.0.0.1:6379> EXISTS VIPPerson
(integer) 1
127.0.0.1:6379> GET VIPPerson
"Peter"
```

新的key已存在且值为旧的key对应的值

例:使用`RENAME`覆盖key值

删除并重新设置2个键:

```
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SET visitPerson Peter
OK
127.0.0.1:6379> SET VIPPerson Mike
OK
127.0.0.1:6379> GET visitPerson
"Peter"
127.0.0.1:6379> GET VIPPerson
"Mike"
```

重命名键visitPerson为VIPPerson:

```
127.0.0.1:6379> RENAME visitPerson VIPPerson
OK
```

查看覆盖后的VIPPerson:

```
127.0.0.1:6379> EXISTS VIPPerson
(integer) 1
127.0.0.1:6379> GET VIPPerson
"Peter"
```

可以看到,覆盖操作后,VIPPerson的值即为覆盖操作前visitPerson的值

例:使用`RENAME`重命名一个不存在的键

```
127.0.0.1:6379> RENAME notExist VipPerson
(error) ERR no such key
```

例:使用`RENAMENX`重命名一个键

设置一个键:

```
127.0.0.1:6379> SET visitPerson Peter
OK
```

使用`RENAMENX`重命名键:

```
127.0.0.1:6379> RENAMENX visitPerson VIPPerson
(integer) 1
```

查看重命名结果:

```
127.0.0.1:6379> EXISTS visitPerson
(integer) 0
127.0.0.1:6379> EXISTS VIPPerson
(integer) 1
127.0.0.1:6379> GET VIPPerson
"Peter"
```

使用`RENAMENX`重命名一个已有的键名:

设置2个键:

```
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SET visitPerson Peter
OK
127.0.0.1:6379> SET VIPPerson Mike
OK
```

使用`RENAMENX`重命名:

```
127.0.0.1:6379> RENAMENX visitPerson VIPPerson
(integer) 0
127.0.0.1:6379> GET visitPerson
"Peter"
127.0.0.1:6379> GET VIPPerson
"Mike"
```

可以看到,重命名操作没有生效

使用`RENAMENX`命令重命名一个不存在的键:

```
127.0.0.1:6379> EXISTS notExist
(integer) 0
```

```
127.0.0.1:6379> RENAMENX notExist existed
(error) ERR no such key
```

可以看到,使用`RENAMENX`重命名一个不存在的键则报错

3.1.5 用`DEL`命令删除键

* `DEL`
  * 语法:`DEL key [key ...]`
  * 功能:删除键值对.该命令返回删除的键值对个数

例:

设置一个键:

```
127.0.0.1:6379> SET name Peter
OK
```

删除多个键:

```
127.0.0.1:6379> EXISTS notExist
(integer) 0
```

可以看到,键`notExist`是不存在的

```
127.0.0.1:6379> DEL name notExist
(integer) 1
```

可以看到,返回值表示删除了1个键

删除一个不存在的键:

```
127.0.0.1:6379> DEL notExist
(integer) 0
```

可以看到,返回值为0,表示删除操作失败

## 3.1.6 关于键生存时间的命令

### 3.1.6.1 查看键的生存周期

* `PTTL`
  * 语法:`PTTL key`
  * 功能:返回指定key的生存周期,单位:毫秒.若key不存在,则返回-2;若key存在但未设置生存周期
* `TTL`
  * 语法:`TTL key`
  * 功能:返回指定key的生存周期,单位:秒.若key不存在,则返回-2;若key存在但未设置生存周期

例:

设置一个生存周期为300秒的key

```
127.0.0.1:6379> SET val 100 EX 300
OK
```

查看该key的生存周期:

```
127.0.0.1:6379> PTTL val
(integer) 297805
127.0.0.1:6379> TTL val
(integer) 295
```

设置一个无生存周期的key:

```
127.0.0.1:6379> DEL val
(integer) 1
127.0.0.1:6379> SET val 100
OK
```

查看无生存周期key的生存周期:

```
127.0.0.1:6379> PTTL val
(integer) -1
127.0.0.1:6379> TTL val
(integer) -1
```

查看一个不存在的key的生存周期:

```
127.0.0.1:6379> EXISTS notExist
(integer) 0
```

```
127.0.0.1:6379> PTTL notExist
(integer) -2
127.0.0.1:6379> TTL notExist
(integer) -2
```

### 3.1.6.2 设置键的生存周期

* `EXPIRE`
  * 语法:`EXPIRE key seconds`
  * 功能:以秒为单位设置一个key的生存周期.设置成功返回1,否则返回0
* `PEXPIRE`
  * 语法:`PEXPIRE key milliseconds`
  * 功能:以毫秒为单位设置一个key的生存周期.设置成功返回1,否则返回0

例:

设置一个无生存周期的key:

```
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> SET val 100
OK
```

设置该key的生存周期为200秒:

```
127.0.0.1:6379> EXPIRE val 200
(integer) 1
```

查看该key的生存周期:

```
127.0.0.1:6379> TTL val
(integer) 173
```

设置该key的生存周期为20000毫秒:

```
127.0.0.1:6379> PEXPIRE val 20000
(integer) 1
```

查看该key的生存周期:

```
127.0.0.1:6379> PTTL val
(integer) 16070
```

给一个不存在的key设置生存周期:

```
127.0.0.1:6379> EXPIRE notExist 10
(integer) 0
127.0.0.1:6379> PEXPIRE notExist 1000
(integer) 0
```

### 3.1.6.3 删除键的生存周期

* `PERSIST`
  * 语法:`PERSIST key`
  * 功能:删除键的生存周期,即该键永不过期.删除操作成功则返回1,否则返回0

例:

设置一个生存周期为200秒的键:

```
127.0.0.1:6379> SET val 100 EX 200
OK
```

查看该key的生存周期:

```
127.0.0.1:6379> TTL val
(integer) 179
```

删除该key的生存周期:

```
127.0.0.1:6379> PERSIST val
(integer) 1
```

查看该key的生存周期:

```
127.0.0.1:6379> TTL val
(integer) -1
```

删除一个不存在的key的生存周期:

```
127.0.0.1:6379> EXISTS notExist
(integer) 0
127.0.0.1:6379> PERSIST notExist
(integer) 0
```


# 3.2 HyperLogLog相关命令

HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的.

***

注:

基数统计是指确定某个数据集中不同(唯一)元素的数量.简而言之,它是评估数据集的"独特性"或"多样性"的方法.例如,考虑以下数字序列:

`1, 3, 4, 5, 5, 6, 6, 7, 7, 7`

虽然这个序列有10个数字,但它只有6个不同的数字.因此,这个数据集的基数是6.

在大数据环境下,直接计算基数可能非常消耗资源和时间,尤其是当数据集很大而内存有限的时候.为了解决这个问题,我们需要一些可以提供近似结果的算法,而这些算法通常在时间和空间效率上都要比完全准确的方法好得多.

HyperLogLog就是这样的一个算法,它提供了一种近似的方法来估计基数,使用的内存非常少,但准确率相对较高.

基数统计在很多领域都有应用,比如统计网站的独立访客数量、统计某个数据库中独特的搜索查询数量或者分析大型数据集中的独特值数量等.

***

在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数.这和计算基数时,元素越多耗费内存就越多的set形成鲜明对比.

但是,因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素.

HyperLogLog(HLL)是一种在Redis中提供的数据结构,它的主要作用是为了提供一种内存效率很高的方法来估计大数据集的唯一元素数量.简单来说,**当你想知道某个数据集中有多少唯一值且不需要知道具体的唯一值时**,你可以使用HyperLogLog.

HyperLogLog的优势在于其极低的内存消耗.无论数据集大小,一个HyperLogLog只需要约12KB的内存.

举个例子:假设你想统计过去一个月你的网站有多少唯一访客.如果使用传统的set结构,每个唯一的用户ID都会被存储一次,这可能会消耗大量内存.但如果使用HyperLogLog,即使是数百万或数十亿的唯一用户ID,所需要的内存也仍然只有12KB.

需要注意的是,HyperLogLog是一个近似算法,这意味着**它提供的计数不是精确的**.但误差范围是可控的,并且对于许多应用来说,这个误差是可以接受的,特别是考虑到它所提供的巨大的内存节省.

在Redis中,你可以使用一系列的`PF*`命令来操作 HyperLogLog.例如`PFADD`来添加元素,`PFCOUNT`来获取近似的唯一元素数量等.

## 3.2.1 用`PFADD`添加键值对

* `PFADD`
  * 语法:`PFADD key element [element ...]`
  * 功能:添加指定元素到HyperLogLog中.如果至少有个1元素被添加返回1,否则返回0

例:

在1个键上同时添加多个值:

```
127.0.0.1:6379> PFADD Peter Math Computer Piano
(integer) 1
```

再向HyperLogLog中添加一个已存在的值:

```
127.0.0.1:6379> PFADD Peter Math
(integer) 0
```

可以看到,由于Math已存在,故返回0

再创建一个HyperLogLog:

```
127.0.0.1:6379> PFADD Mary Math Piano Math
(integer) 1
```

注意:此时键名为Mary的HyperLogLog中有重复的元素Math

## 3.2.2 用`PFCOUNT`统计基数值

* `PFCOUNT`
  * 语法:`PFCOUNT key [key ...]`
  * 功能:返回给定HyperLogLog的基数估算值.如果多个键,则返回多个键对应的值中不重复数据的数量;若HyperLogLog不存在则返回0

例:

查看Peter上的课外班数量:

```
127.0.0.1:6379> PFCOUNT Peter
(integer) 3
```

查看Mary上的课外班数量:

```
127.0.0.1:6379> PFCOUNT Mary
(integer) 2
```

可以看到,添加元素到HyperLogLog时,由于Math是重复的,因此统计出的基数值为2

查看Peter和Mary上的课外班数量:

```
127.0.0.1:6379> PFCOUNT Peter Mary
(integer) 3
```

虽然Peter的基数为3,Mary的基数为2.但是由于2个HyperLogLog中的元素值是重复的,因此它们的基数和仍然为3

查看一个不存在的HyperLogLog的基数值:

```
127.0.0.1:6379> PFCOUNT notExist
(integer) 0
```

注意:`PFCOUNT`命令返回的是对应基数的近似值,而非精确值.因此当基数量很大时统计结果不一定准确

## 3.2.3 用`PFMERGE`进行合并操作

* `PFMERGE`
  * 语法:`PFMERGE destkey sourcekey [sourcekey ...]`
  * 功能:把多个HyperLogLog合并成一个.无论sourcekey是否存在,都将返回OK.如果合并前destkey不存在,则会新建一个

例:

创建2个HyperLogLog:

```
127.0.0.1:6379> PFADD hll1 1 2 3
(integer) 1
```

```
127.0.0.1:6379> PFADD hll2 2 4 5
(integer) 1
```

合并2个HyperLogLog:

```
127.0.0.1:6379> PFMERGE hll hll1 hll2
OK
```

查看合并后的基数:

```
127.0.0.1:6379> PFCOUNT hll
(integer) 5
```

合并2个不存在的HyperLogLog:

```
127.0.0.1:6379> EXISTS a
(integer) 0
127.0.0.1:6379> EXISTS b
(integer) 0
127.0.0.1:6379> EXISTS c
(integer) 0
127.0.0.1:6379> PFMERGE a b c
OK
```

可以看到,合并2个不存在的HyperLogLog,同样返回OK

## 3.2.4 统计网站访问总人数

在网站分析方面有两个统计指标:第一个是统计总访问量,第二个是统计访问人数.统计总访问量比较好办,每来一次访问加1即可,而在统计访问人数时需要去除重复,比如某人在某天内访问了100次,但在统计访问人数时只能算作一次

例:

此处我们以webSite1和webSite2表示2个网站的key;以u1-u5表示5个不同的用户:

```
127.0.0.1:6379> PFADD webSite1 u1 u1 u2 u3 u1 u4 u2
(integer) 1
```

```
127.0.0.1:6379> PFADD webSite2 u1 u2 u3 u4 u5 u4 u3 u2
(integer) 1
```

统计2个网站的访问人数:

```
127.0.0.1:6379> PFCOUNT webSite1
(integer) 4
```

```
127.0.0.1:6379> PFCOUNT webSite2
(integer) 5
```


# 3.3 lua脚本相关命令

## 3.3.1 把lua脚本装载到缓存里

* `SCRIPT LOAD`
  * 语法:`SCRIPT LOAD script`
  * 将脚本script添加到脚本缓存中,但并不立即执行这个脚本.该命令返回给定脚本的SHA1校验和

例:

装载脚本:

```
127.0.0.1:6379> SCRIPT LOAD 'return 1 + 2'
"e412f6a7f0b07176d9824bb91205d9d54e88fdc0"
```

* `SCRIPT EXISTS`
  * 语法:`SCRIPT EXISTS script [script ...]`
  * 功能:校验指定的脚本是否已经被保存在缓存当中.返回值为一个列表,1表示脚本存在,0表示脚本不存在.列表中的元素和给定的SHA1校验和保持对应关系

例:

装载脚本:

```
127.0.0.1:6379> SCRIPT LOAD 'return 3 + 4'
"838b9ce712555508c95330c46ab526d2b01f4d5f"
```

根据校验和确认脚本是否存在:

```
127.0.0.1:6379> SCRIPT EXISTS e412f6a7f0b07176d9824bb91205d9d54e88fdc0 838b9ce712555508c95330c46ab526d2b01f4d5f abc123
1) (integer) 1
2) (integer) 1
3) (integer) 0
```

可以看到,由于不存在校验和为`abc123`的脚本,因此对于该校验和的返回值为0

## 3.3.2 通过`EVALSHA`命令执行缓存中的脚本

* `EVALSHA`
  * 语法:`EVALSHA sha1 numkeys key [key ...] arg [arg ...]`.其中:
    * sha1:脚本的sha1校验和
    * numkeys:参数个数
    * key:表示在脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量`KEYS`数组,用1为基址的形式访问(`KEYS[1]`,`KEYS[2]`,以此类推)
    * arg:附加参数,在Lua中通过全局变量`ARGV`数组访问,访问的形式和KEYS变量类似(`ARGV[1]`,`ARGV[2]`,诸如此类)
  * 功能:根据给定的sha1校验码,执行缓存在服务器中的脚本

例:执行上文加载的`return 1 + 2`

```
127.0.0.1:6379> EVALSHA e412f6a7f0b07176d9824bb91205d9d54e88fdc0 0
(integer) 3
```

## 3.3.3 清空缓存中lua脚本的命令

* `SCRIPT FLUSH`
  * 语法:`SCRIPT FLUSH`
  * 功能:清除所有Lua脚本缓存.该命令总是返回OK

确认脚本是否存在:

```
127.0.0.1:6379> SCRIPT EXISTS e412f6a7f0b07176d9824bb91205d9d54e88fdc0
1) (integer) 1
127.0.0.1:6379> SCRIPT EXISTS 838b9ce712555508c95330c46ab526d2b01f4d5f
1) (integer) 1
```

清空所有脚本:

```
127.0.0.1:6379> SCRIPT FLUSH
OK
```

确认脚本是否存在:

```
127.0.0.1:6379> SCRIPT EXISTS e412f6a7f0b07176d9824bb91205d9d54e88fdc0
1) (integer) 0
127.0.0.1:6379> SCRIPT EXISTS 838b9ce712555508c95330c46ab526d2b01f4d5f
1) (integer) 0
```

可以看到,脚本已经被清空了

## 3.3.4 用`EVAL`命令执行LUA脚本

* `EVAL`
  * 语法:`EVAL script numkeys key [key ...] arg [arg ...]`.其中:
    * numkeys:参数个数
    * key:表示在脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量`KEYS`数组,用1为基址的形式访问(`KEYS[1]`,`KEYS[2]`,以此类推)
    * arg:附加参数,在Lua中通过全局变量`ARGV`数组访问,访问的形式和KEYS变量类似(`ARGV[1]`,`ARGV[2]`,诸如此类)
  * 功能:直接运行脚本

例:

运行一段LUA脚本,该脚本接收1个Redis键名和1个参数.

```
127.0.0.1:6379> EVAL "return { KEYS[1], ARGV[1] }" 1 name 'Peter'
1) "name"
2) "Peter"
```

* `SCRIPT KILL`
  * 语法:`SCRIPT KILL`
  * 功能:杀死当前正在运行的LUA脚本.当且仅当这个脚本没有执行过任何写操作时,这个命令才生效,该命令执行后,当前正在运行的脚本会被杀死,执行这个脚本的客户端会从`EVAL`命令的阻塞当中退出,并收到一个错误作为返回值.

例:在没有LUA脚本正在运行时执行`SCRIPT KILL`

```
127.0.0.1:6379> SCRIPT KILL
(error) NOTBUSY No scripts in execution right now.
```


# 3.4 排序相关命令

## 3.4.1 用`SORT`命令进行排序

* `SORT`
  * 语法:`SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]`,其中:
    * BY:指定排序模式
    * LIMIT offset count:
      * offset:偏移量
      * count:需返回的元素个数
    * GET:以排序的结果作为键名,去获取这些键名对应的值
    * ASC/DESC:指定排序规则为升/降序
    * ALPHA:该选项用于指定对一个包含字符串值的集合键进行排序
    * STORE:默认状况下,排序结果会返回给客户端.使用STORE描述符,能够将结果存储在指定key上,若是Key已经存在则覆盖.而不将排序结果返回给客户端
  * 功能:升序或降序排列

例:使用`SORT`命令对list进行升序排列

创建list:

```
127.0.0.1:6379> LPUSH salary 10000 15000 13500 12000
(integer) 4
```

读取list时升序排列:

```
127.0.0.1:6379> SORT salary ASC
1) "10000"
2) "12000"
3) "13500"
4) "15000"
```

按写入顺序读取list:

```
127.0.0.1:6379> LRANGE salary 0 -1 
1) "12000"
2) "13500"
3) "15000"
4) "10000"
```

例:使用`SORT`命令对set进行降序排列

创建set:

```
127.0.0.1:6379> SADD name 'Peter' 'Tom' 'Mary'
(integer) 3
```

降序排序读取:

```
127.0.0.1:6379> SORT name DESC
(error) ERR One or more scores can't be converted into double
```

可以看到,`SORT`命令默认只能对数值型元素进行排序

使用`SORT`命令对字符串类型元素进行排序:

```
127.0.0.1:6379> SORT name DESC ALPHA
1) "Tom"
2) "Peter"
3) "Mary"
```

例:使用`SORT`命令对zset排序

创建zset:

```
127.0.0.1:6379> ZADD nameSet 4.0 Mike 2.0 Peter 1.0 Tim 0.5 Johnson
(integer) 4
```

按zset中元素的ASCII码升序排序:

```
127.0.0.1:6379> SORT nameSet ASC ALPHA
1) "Johnson"
2) "Mike"
3) "Peter"
4) "Tim"
```

按zset中元素的ASCII码降序排序:

```
127.0.0.1:6379> SORT nameSet DESC ALPHA
1) "Tim"
2) "Peter"
3) "Mike"
4) "Johnson"
```

可以看到,使用`SORT`命令读取zset时,排序与元素的score是无关的,仅与元素的字面量有关

## 3.4.2 用`BY`参数指定排序模式

例:

现有一list如下:

```
127.0.0.1:6379> LPUSH vipLevel VIP1 VIP3 VIP2
(integer) 3
```

按VIP后的数字降序排序:

```
127.0.0.1:6379> SORT vipLevel DESC BY VIP*
1) "VIP3"
2) "VIP2"
3) "VIP1"
```

## 3.4.3 用`LIMIT`参数返回部分排序结果

例:

创建list:

```
127.0.0.1:6379> RPUSH number 1 3 2 4 6 5 8 7
(integer) 8
```

升序排序取前3个元素:

```
127.0.0.1:6379> SORT number LIMIT 0 3 ASC
1) "1"
2) "2"
3) "3"
```

升序排序取第5个元素和第6个元素的值:

```
127.0.0.1:6379> SORT number LIMIT 4 2 ASC
1) "5"
2) "6"
```

## 3.4.4 `SORT`命令里`GET`参数的用法

`GET`参数:以排序的结果作为键名,去获取和这些键名相关的值

例:

创建一个list:

```
127.0.0.1:6379> LPUSH score 100 80 90 85
(integer) 4
```

以该list中的元素为键名,创建与这些键名有关的字符串:

创建键名以`name-`开头的且和list中的元素有关的字符串:

```
127.0.0.1:6379> SET name-100 Peter-100
OK
127.0.0.1:6379> SET name-80 Mary-80
OK
```

创建键名以`symbol-`开头且和list中的元素有关的字符串:

```
127.0.0.1:6379> SET symbol-90 RMB-90
OK
127.0.0.1:6379> SET symbol-85 USD-85
OK
```

以升序对list排序,获取键名为`name-list中的元素`的值:

```
127.0.0.1:6379> SORT score GET name-* ASC 
1) "Mary-80"
2) (nil)
3) (nil)
4) "Peter-100"
```

可以看到,由于存在键名为`name-100`和`name-80`的值,故能够取到

以降序对list排序,获取键名为`symbol-list中的元素`的值:

```
127.0.0.1:6379> SORT score GET symbol-* DESC
1) (nil)
2) "RMB-90"
3) "USD-85"
4) (nil)
```

以降序对list排序,获取键名为键名为`name-list中的元素`和键名为`symbol-list中的元素`的值:

```
127.0.0.1:6379> SORT score GET name-* GET symbol-* DESC
1) "Peter-100"	// name-100
2) (nil)			// symbol-100
3) (nil)			// name-90
4) "RMB-90"		// symbol-90
5) (nil)			// name-85
6) "USD-85"		// symbol-85
7) "Mary-80"		// name-80
8) (nil)			// symbol-80
```

## 3.4.5 通过`STORE`参数提升性能

例:将list中的元素倒序排序后保存为另一个list

查看list:

```
127.0.0.1:6379> LRANGE score 0 -1
1) "85"
2) "90"
3) "80"
4) "100"
```

将list倒序排序并保存为一个名为score-desc的list:

```
127.0.0.1:6379> SORT score DESC STORE score-desc
(integer) 4
```

查看保存的结果:

```
127.0.0.1:6379> LRANGE score-desc 0 -1
1) "100"
2) "90"
3) "85"
4) "80"
```


# 第4章 实践Redis服务器和客户端的操作


# 4.1 Redis服务器管理客户端的命令

## 4.1.1 获取和设置客户端的名字

* `CLIENT GETNAME`
  * 功能:获取客户端的名字
* `CLIENT SETNAME`
  * 语法:`CLIENT SETNAME name`
  * 功能:设置客户端的名字

例:

```
127.0.0.1:6379> CLIENT GETNAME
(nil)
```

```
127.0.0.1:6379> CLIENT SETNAME myName
OK
```

```
127.0.0.1:6379> CLIENT GETNAME
"myName"
```

## 4.1.2 通过`CLIENT LIST`命令查看客户端的信息

* `CLIENT LIST`
  * 功能:查看当前所有连接到服务器的客户端信息

例:

```
127.0.0.1:6379> CLIENT LIST
id=3 addr=127.0.0.1:59254 laddr=127.0.0.1:6379 fd=8 name=myName age=6363 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1
```

其中:

|   属性  |       含义      |
| :---: | :-----------: |
|   id  |     客户端编号     |
|  addr |     客户端地址     |
| laddr |     服务端地址     |
|  age  | 客户端的连接时长,单位:秒 |
|  idle | 客户端的空闲时长,单位:秒 |
|  cmd  |   客户端最近执行的命令  |
|  user |  登录到服务器用到的用户名 |

## 4.1.3 通过`CLIENT PAUSE`命令暂停客户端的命令

* `CLIENT PAUSE`
  * 语法:`CLIENT PAUSE timeout`
  * 功能:若当前Redis服务器负载过大,可通过该命令暂停执行来自客户端的命令.其中`timeout`的单位为毫秒.服务端会在暂停的时长结束后再执行来自客户端的命令

例:暂停10s后执行命令

```
127.0.0.1:6379> CLIENT PAUSE 10000
OK
```

```
127.0.0.1:6379> SET name Peter
OK
(2.67s)
```

其中的2.67s表示该命令被暂停的时长

## 4.1.4 通过`CLIENT KILL`命令中断客户端连接

* `CLIENT KILL`
  * 语法:`CLIENT KILL [ip:port]`
  * 功能:中断指定的客户端连接

例:

查看当前所有连接

```
127.0.0.1:6379> CLIENT LIST
id=3 addr=127.0.0.1:59254 laddr=127.0.0.1:6379 fd=8 name=myName age=6919 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1
id=4 addr=127.0.0.1:46400 laddr=127.0.0.1:6379 fd=9 name= age=20 idle=20 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 argv-mem=0 obl=0 oll=0 omem=0 tot-mem=20496 events=r cmd=command user=default redir=-1
id=5 addr=127.0.0.1:40846 laddr=127.0.0.1:6379 fd=10 name= age=2 idle=2 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 argv-mem=0 obl=0 oll=0 omem=0 tot-mem=20496 events=r cmd=command user=default redir=-1
```

注:此处通过`docker exec -it myFirstRedis /bin/bash`进入容器后,再执行`redis-cli`,即可再创建一个客户端连接

中断IP为127.0.0.1,端口为46400的连接:

```
127.0.0.1:6379> CLIENT KILL 127.0.0.1:46400
OK
127.0.0.1:6379> CLIENT LIST
id=3 addr=127.0.0.1:59254 laddr=127.0.0.1:6379 fd=8 name=myName age=7040 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1
id=5 addr=127.0.0.1:40846 laddr=127.0.0.1:6379 fd=10 name= age=123 idle=123 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 argv-mem=0 obl=0 oll=0 omem=0 tot-mem=20496 events=r cmd=command user=default redir=-1
```

在被中断的客户端中执行命令:

```
127.0.0.1:6379> CLIENT LIST
Error: Server closed the connection
```

可以看到,该客户端的连接已经被中断了

注:该命令只是中断客户端的链接,并不是中断服务器本身的服务

## 4.1.5 通过`SHUTDOWN`命令关闭服务器和客户端

* `SHUTDOWN`
  * 功能:终止服务端上的所有连接并终止服务端

例:

```
127.0.0.1:6379> SHUTDOWN
not connected>
```


# 4.2 查看Redis服务器的详细信息

## 4.2.1 通过`INFO`命令查看服务器信息

* `INFO`
  * 功能:查看服务端相关信息

例:

```
127.0.0.1:6379> INFO
# Server
redis_version:6.2.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:b61f37314a089f19
redis_mode:standalone
os:Linux 5.15.49-linuxkit x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:10.2.1
process_id:1
process_supervised:no
run_id:ec3f7b5d6eda8fa1c55555f82ede6dfd23e9a50c
tcp_port:6379
server_time_usec:1691911379981145
uptime_in_seconds:300
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:14189779
executable:/data/redis-server
config_file:
io_threads_active:0

# Clients
connected_clients:1
cluster_connections:0
maxclients:10000
client_recent_max_input_buffer:24
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0

# Memory
used_memory:873648
used_memory_human:853.17K
used_memory_rss:7856128
used_memory_rss_human:7.49M
used_memory_peak:933848
used_memory_peak_human:911.96K
used_memory_peak_perc:93.55%
used_memory_overhead:830384
used_memory_startup:809880
used_memory_dataset:43264
used_memory_dataset_perc:67.85%
allocator_allocated:1071104
allocator_active:1290240
allocator_resident:3805184
total_system_memory:8346099712
total_system_memory_human:7.77G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.20
allocator_frag_bytes:219136
allocator_rss_ratio:2.95
allocator_rss_bytes:2514944
rss_overhead_ratio:2.06
rss_overhead_bytes:4050944
mem_fragmentation_ratio:9.46
mem_fragmentation_bytes:7025240
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:20504
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0

# Persistence
loading:0
current_cow_size:0
current_cow_size_age:0
current_fork_perc:0.00
current_save_keys_processed:0
current_save_keys_total:0
rdb_changes_since_last_save:20
rdb_bgsave_in_progress:0
rdb_last_save_time:1691911079
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:0
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_last_cow_size:0
module_fork_in_progress:0
module_fork_last_cow_size:0

# Stats
total_connections_received:1
total_commands_processed:3
instantaneous_ops_per_sec:0
total_net_input_bytes:74
total_net_output_bytes:20555
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:5
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0
total_forks:0
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
total_error_replies:0
dump_payload_sanitizations:0
total_reads_processed:4
total_writes_processed:3
io_threaded_reads_processed:0
io_threaded_writes_processed:0

# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:557e052fd9196b70141b0e18a6c609dfceb06773
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

# CPU
used_cpu_sys:0.487641
used_cpu_user:0.287673
used_cpu_sys_children:0.003459
used_cpu_user_children:0.001504
used_cpu_sys_main_thread:0.485370
used_cpu_user_main_thread:0.284587

# Modules

# Errorstats

# Cluster
cluster_enabled:0

# Keyspace
```

|      属性     |               含义              |
| :---------: | :---------------------------: |
|    Server   |            服务端相关信息            |
|   Clients   |           已连接的客户端信息           |
|    Memory   |         Redis服务端内存相关信息        |
| Persistence |            持久化相关信息            |
|    Stats    |          和服务端相关的统计信息          |
| Replication |            主从复制相关信息           |
|     CPU     |        服务端所在机器的CPU相关信息        |
|   Cluster   |          Redis集群相关信息          |
|   Keyspace  | 和Redis数据库相关的统计信息.比如键的数量和超时时间等 |

## 4.2.2 查看客户端连接状况

* `INFO CLIENTS`
  * 功能:查看客户端的连接情况

例:

```
127.0.0.1:6379> INFO CLIENTS
# Clients
connected_clients:1
cluster_connections:0
maxclients:10000
client_recent_max_input_buffer:16
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
```

其中:

* `connected_clients`:正在连接的客户端数量
* `maxclients`:服务端允许的最大连接数

## 4.2.3 观察最大连接数

* `INFO STATS`
  * 功能:查看服务端运行情况

[INFO STATS命令详解](https://blog.csdn.net/HiJamesChen/article/details/108998197)

例:

```
127.0.0.1:6379> INFO STATS
# Stats
total_connections_received:1
total_commands_processed:6
instantaneous_ops_per_sec:0
total_net_input_bytes:150
total_net_output_bytes:24839
instantaneous_input_kbps:0.01
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:12
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0
total_forks:0
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
total_error_replies:0
dump_payload_sanitizations:0
total_reads_processed:7
total_writes_processed:6
io_threaded_reads_processed:0
io_threaded_writes_processed:0
```

其中:

* `rejected_connections`:因超过最大连接数而被拒绝的客户端连接次数

查看最大连接数:

```
127.0.0.1:6379> CONFIG GET maxclients
1) "maxclients"
2) "10000"
```

可以看到,一般情况下是不会出现`rejected_connections`大于0的情况

## 4.2.4 查看每秒执行多少条指令

在`INFO STATS`命令的返回值中,有一项名为`instantaneous_ops_per_sec`.

* `instantaneous_ops_per_sec`:表示当前每秒执行的指令数量

若集群中某台Redis服务器中该数值过大或过小,就需要观察负载均衡的相关配置了.或者当数据库压力较大而通过该命令发现作为缓存的Redis服务器执行的指令过少时,就需要调整缓存策略

## 4.2.5 观察内存用量

* `INFO MEMORY`
  * 功能:观察当前Redis服务器的内存使用情况

由于Redis是在内存中缓存数据,如果缓存数据太多,或者大量键没有设置过期时间(expired time),就会造成内存使用过大,从而导致OOM问题.此时需要通过`INFO MEMORY`命令观察Redis服务器的内存使用情况.

```
127.0.0.1:6379> INFO MEMORY
# Memory
used_memory:871904
used_memory_human:851.47K
used_memory_rss:7839744
used_memory_rss_human:7.48M
used_memory_peak:933848
used_memory_peak_human:911.96K
used_memory_peak_perc:93.37%
used_memory_overhead:830392
used_memory_startup:809880
used_memory_dataset:41512
used_memory_dataset_perc:66.93%
allocator_allocated:1133568
allocator_active:1368064
allocator_resident:3883008
total_system_memory:8346099712
total_system_memory_human:7.77G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.21
allocator_frag_bytes:234496
allocator_rss_ratio:2.84
allocator_rss_bytes:2514944
rss_overhead_ratio:2.02
rss_overhead_bytes:3956736
mem_fragmentation_ratio:9.44
mem_fragmentation_bytes:7008848
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:20512
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0
```

其中:

* `used_memory_human`:以人类可读形式展示Redis已使用的内存
* `used_memory_peak_human`:以人类可读形式展示Redis使用的内存峰值
* `used_memory_lua_human`:以人类可读形式展示LUA脚本占用的内存大小
* `used_memory_scripts_human`:以人类可读形式展示脚本占用的内存大小
* `mem_clients_slaves`:因客户端主从复制而使用的内存大小

## 4.2.6 通过`COMMAND`命令查看Redis命令

* `COMMAND`
  * 功能:返回Redis的命令信息

例:

```
127.0.0.1:6379> COMMAND
  1) 1) "psubscribe"
     2) (integer) -2
     3) 1) pubsub
        2) noscript
        3) loading
        4) stale
     4) (integer) 0
     5) (integer) 0
     6) (integer) 0
     7) 1) @pubsub
        2) @slow
  2) 1) "lpos"
     2) (integer) -3
     3) 1) readonly
     4) (integer) 1
     5) (integer) 1
     6) (integer) 1
     7) 1) @read
        2) @list
        3) @slow
  3) 1) "rpushx"
     2) (integer) -3
     3) 1) write
        2) denyoom
        3) fast
     4) (integer) 1
     5) (integer) 1
     6) (integer) 1
     7) 1) @write
        2) @list
        3) @fast
  4) 1) "setbit"
     2) (integer) 4
     3) 1) write
        2) denyoom
     4) (integer) 1
     5) (integer) 1
     6) (integer) 1
     7) 1) @write
        2) @bitmap
        3) @slow
  5) 1) "georadius_ro"
     2) (integer) -6
     3) 1) readonly
     4) (integer) 1
     5) (integer) 1
     6) (integer) 1
     7) 1) @read
        2) @geo
        3) @slow
  6) 1) "command"
     2) (integer) -1
     3) 1) random
        2) loading
        3) stale
     4) (integer) 0
     5) (integer) 0
     6) (integer) 0
     7) 1) @slow
        2) @connection
  7) 1) "debug"
     2) (integer) -2
     3) 1) admin
        2) noscript
        3) loading
        4) stale
     4) (integer) 0
     5) (integer) 0
     6) (integer) 0
     7) 1) @admin
        2) @slow
        3) @dangerous
  8) 1) "bzpopmin"
     2) (integer) -3
     3) 1) write
        2) noscript
        3) fast
     4) (integer) 1
     5) (integer) -2
     6) (integer) 1
     7) 1) @write
        2) @sortedset
        3) @fast
        4) @blocking
  9) 1) "xrevrange"
     2) (integer) -4
     3) 1) readonly
     4) (integer) 1
     5) (integer) 1
     6) (integer) 1
     7) 1) @read
        2) @stream
        3) @slow
...
223) 1) "hrandfield"
     2) (integer) -2
     3) 1) readonly
        2) random
     4) (integer) 1
     5) (integer) 1
     6) (integer) 1
     7) 1) @read
        2) @hash
        3) @slow
224) 1) "scan"
     2) (integer) -2
     3) 1) readonly
        2) random
     4) (integer) 0
     5) (integer) 0
     6) (integer) 0
     7) 1) @keyspace
        2) @read
        3) @slow
```

## 4.2.7 查看指定Redis命令的信息

* `COMMAND INFO`
  * 语法:`COMMAND INFO key [key ...]`
  * 功能:查看指定命令的详细信息

例:

```
127.0.0.1:6379> COMMAND INFO set
1) 1) "set"
   2) (integer) -3
   3) 1) write
      2) denyoom
   4) (integer) 1
   5) (integer) 1
   6) (integer) 1
   7) 1) @write
      2) @string
      3) @slow
```

```
127.0.0.1:6379> COMMAND INFO get
1) 1) "get"
   2) (integer) 2
   3) 1) readonly
      2) fast
   4) (integer) 1
   5) (integer) 1
   6) (integer) 1
   7) 1) @read
      2) @string
      3) @fast
```

## 4.2.8 获取指定命令的所有键

* `COMMAND GETKEYS`
  * 语法:`COMMAND GETKEYS command`
  * 功能:获取command命令中所有的键

例:

```
127.0.0.1:6379> COMMAND GETKEYS MSET name Peter age 18 score 100
1) "name"
2) "age"
3) "score"
```

`MSET`命令可以设置多个键值对,前边加上`COMMAND GETKEYS`即可获得该命令操作的所有key名

注:`COMMAND GETKEYS`命令后的command部分实际上并没有真正执行

```
127.0.0.1:6379> GET name
(nil)
127.0.0.1:6379> GET age
(nil)
127.0.0.1:6379> GET score
(nil)
```

例:

```
127.0.0.1:6379> COMMAND GETKEYS SET name Mary
1) "name"
```

```
127.0.0.1:6379> GET name
(nil)
```


# 4.3 查看并修改服务器的常用配置

## 4.3.1 查看服务器的配置

* `CONFIG GET`
  * 语法:`CONFIG GET pattern`
  * 功能:查看服务器配置

例:

```
127.0.0.1:6379> CONFIG GET *
  1) "rdbchecksum"
  2) "yes"
  3) "daemonize"
  4) "no"
  5) "io-threads-do-reads"
  6) "no"
  7) "lua-replicate-commands"
  8) "yes"
  9) "always-show-logo"
 10) "no"
 11) "protected-mode"
 12) "no"
 13) "rdbcompression"
 14) "yes"
 15) "rdb-del-sync-files"
 16) "no"
 17) "activerehashing"
 18) "yes"
 19) "stop-writes-on-bgsave-error"
 20) "yes"
 21) "set-proc-title"
 22) "yes"
 23) "dynamic-hz"
 24) "yes"
 25) "lazyfree-lazy-eviction"
 26) "no"
 27) "lazyfree-lazy-expire"
 28) "no"
 29) "lazyfree-lazy-server-del"
 30) "no"
 31) "lazyfree-lazy-user-del"
 32) "no"
 33) "lazyfree-lazy-user-flush"
 34) "no"
 35) "repl-disable-tcp-nodelay"
 36) "no"
 37) "repl-diskless-sync"
 38) "no"
 39) "gopher-enabled"
 40) "no"
 41) "aof-rewrite-incremental-fsync"
 42) "yes"
 43) "no-appendfsync-on-rewrite"
 44) "no"
 45) "cluster-require-full-coverage"
 46) "yes"
 47) "rdb-save-incremental-fsync"
 48) "yes"
 49) "aof-load-truncated"
 50) "yes"
 51) "aof-use-rdb-preamble"
 52) "yes"
 53) "cluster-replica-no-failover"
 54) "no"
 55) "cluster-slave-no-failover"
 56) "no"
 57) "replica-lazy-flush"
 58) "no"
 59) "slave-lazy-flush"
 60) "no"
 61) "replica-serve-stale-data"
 62) "yes"
 63) "slave-serve-stale-data"
 64) "yes"
 65) "replica-read-only"
 66) "yes"
 67) "slave-read-only"
 68) "yes"
 69) "replica-ignore-maxmemory"
 70) "yes"
 71) "slave-ignore-maxmemory"
 72) "yes"
 73) "jemalloc-bg-thread"
 74) "yes"
 75) "activedefrag"
 76) "no"
 77) "syslog-enabled"
 78) "no"
 79) "cluster-enabled"
 80) "no"
 81) "appendonly"
 82) "no"
 83) "cluster-allow-reads-when-down"
 84) "no"
 85) "crash-log-enabled"
 86) "yes"
 87) "crash-memcheck-enabled"
 88) "yes"
 89) "use-exit-on-panic"
 90) "no"
 91) "disable-thp"
 92) "yes"
 93) "cluster-allow-replica-migration"
 94) "yes"
 95) "replica-announced"
 96) "yes"
 97) "aclfile"
 98) ""
 99) "unixsocket"
100) ""
101) "pidfile"
102) ""
103) "replica-announce-ip"
104) ""
105) "slave-announce-ip"
106) ""
107) "masteruser"
108) ""
109) "cluster-announce-ip"
110) ""
111) "syslog-ident"
112) "redis"
113) "dbfilename"
114) "dump.rdb"
115) "appendfilename"
116) "appendonly.aof"
117) "server_cpulist"
118) ""
119) "bio_cpulist"
120) ""
121) "aof_rewrite_cpulist"
122) ""
123) "bgsave_cpulist"
124) ""
125) "ignore-warnings"
126) ""
127) "proc-title-template"
128) "{title} {listen-addr} {server-mode}"
129) "masterauth"
130) ""
131) "requirepass"
132) ""
133) "supervised"
134) "no"
135) "syslog-facility"
136) "local0"
137) "repl-diskless-load"
138) "disabled"
139) "loglevel"
140) "notice"
141) "maxmemory-policy"
142) "noeviction"
143) "appendfsync"
144) "everysec"
145) "oom-score-adj"
146) "no"
147) "acl-pubsub-default"
148) "allchannels"
149) "sanitize-dump-payload"
150) "no"
151) "databases"
152) "16"
153) "port"
154) "6379"
155) "io-threads"
156) "1"
157) "auto-aof-rewrite-percentage"
158) "100"
159) "cluster-replica-validity-factor"
160) "10"
161) "cluster-slave-validity-factor"
162) "10"
163) "list-max-ziplist-size"
164) "-2"
165) "tcp-keepalive"
166) "300"
167) "cluster-migration-barrier"
168) "1"
169) "active-defrag-cycle-min"
170) "1"
171) "active-defrag-cycle-max"
172) "25"
173) "active-defrag-threshold-lower"
174) "10"
175) "active-defrag-threshold-upper"
176) "100"
177) "lfu-log-factor"
178) "10"
179) "lfu-decay-time"
180) "1"
181) "replica-priority"
182) "100"
183) "slave-priority"
184) "100"
185) "repl-diskless-sync-delay"
186) "5"
187) "maxmemory-samples"
188) "5"
189) "maxmemory-eviction-tenacity"
190) "10"
191) "timeout"
192) "0"
193) "replica-announce-port"
194) "0"
195) "slave-announce-port"
196) "0"
197) "tcp-backlog"
198) "511"
199) "cluster-announce-bus-port"
200) "0"
201) "cluster-announce-port"
202) "0"
203) "cluster-announce-tls-port"
204) "0"
205) "repl-timeout"
206) "60"
207) "repl-ping-replica-period"
208) "10"
209) "repl-ping-slave-period"
210) "10"
211) "list-compress-depth"
212) "0"
213) "rdb-key-save-delay"
214) "0"
215) "key-load-delay"
216) "0"
217) "active-expire-effort"
218) "1"
219) "hz"
220) "10"
221) "min-replicas-to-write"
222) "0"
223) "min-slaves-to-write"
224) "0"
225) "min-replicas-max-lag"
226) "10"
227) "min-slaves-max-lag"
228) "10"
229) "maxclients"
230) "10000"
231) "active-defrag-max-scan-fields"
232) "1000"
233) "slowlog-max-len"
234) "128"
235) "acllog-max-len"
236) "128"
237) "lua-time-limit"
238) "5000"
239) "cluster-node-timeout"
240) "15000"
241) "slowlog-log-slower-than"
242) "10000"
243) "latency-monitor-threshold"
244) "0"
245) "proto-max-bulk-len"
246) "536870912"
247) "stream-node-max-entries"
248) "100"
249) "repl-backlog-size"
250) "1048576"
251) "maxmemory"
252) "0"
253) "hash-max-ziplist-entries"
254) "512"
255) "set-max-intset-entries"
256) "512"
257) "zset-max-ziplist-entries"
258) "128"
259) "active-defrag-ignore-bytes"
260) "104857600"
261) "hash-max-ziplist-value"
262) "64"
263) "stream-node-max-bytes"
264) "4096"
265) "zset-max-ziplist-value"
266) "64"
267) "hll-sparse-max-bytes"
268) "3000"
269) "tracking-table-max-keys"
270) "1000000"
271) "client-query-buffer-limit"
272) "1073741824"
273) "repl-backlog-ttl"
274) "3600"
275) "auto-aof-rewrite-min-size"
276) "67108864"
277) "tls-port"
278) "0"
279) "tls-session-cache-size"
280) "20480"
281) "tls-session-cache-timeout"
282) "300"
283) "tls-cluster"
284) "no"
285) "tls-replication"
286) "no"
287) "tls-auth-clients"
288) "yes"
289) "tls-prefer-server-ciphers"
290) "no"
291) "tls-session-caching"
292) "yes"
293) "tls-cert-file"
294) ""
295) "tls-key-file"
296) ""
297) "tls-key-file-pass"
298) ""
299) "tls-client-cert-file"
300) ""
301) "tls-client-key-file"
302) ""
303) "tls-client-key-file-pass"
304) ""
305) "tls-dh-params-file"
306) ""
307) "tls-ca-cert-file"
308) ""
309) "tls-ca-cert-dir"
310) ""
311) "tls-protocols"
312) ""
313) "tls-ciphers"
314) ""
315) "tls-ciphersuites"
316) ""
317) "logfile"
318) ""
319) "watchdog-period"
320) "0"
321) "dir"
322) "/data"
323) "save"
324) "3600 1 300 100 60 10000"
325) "client-output-buffer-limit"
326) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60"
327) "unixsocketperm"
328) "0"
329) "slaveof"
330) ""
331) "notify-keyspace-events"
332) ""
333) "bind"
334) ""
335) "oom-score-adj-values"
336) "0 200 800"
```

## 4.3.2 通过修改服务器配置设置密码

* `CONFIG SET`
  * 语法:`CONFIG SET key value`
  * 功能:修改服务端配置.修改配置后无需重启立即生效.但Redis服务端重启后该命令设置的选项会失效

例:设置密码为123456

```
127.0.0.1:6379> CONFIG SET requirepass 123456
OK
```

修改完成后,退出当前客户端,再重新连接服务端后,执行命令就会提示权限错误:

```
127.0.0.1:6379> exit
root@db18e24f57c6:/data# redis-cli
127.0.0.1:6379> CONFIG GET *
(error) NOAUTH Authentication required.
```

* `AUTH`
  * 语法:`AUTH [username] password`
  * 功能:输入用户名和密码

例:

```
127.0.0.1:6379> AUTH 123456
OK
127.0.0.1:6379> CONFIG GET *
  1) "rdbchecksum"
  2) "yes"
  3) "daemonize"
  4) "no"
  5) "io-threads-do-reads"
  6) "no"
  7) "lua-replicate-commands"
  8) "yes"
  9) "always-show-logo"
 10) "no"
 11) "protected-mode"
 12) "no"
 13) "rdbcompression"
 14) "yes"
 15) "rdb-del-sync-files"
 16) "no"
...
335) "oom-score-adj-values"
336) "0 200 800"
```

例:重启Redis服务端后确认密码是否存在

```
127.0.0.1:6379> exit
root@db18e24f57c6:/data# exit
exit
(base) root@192 ~ % docker stop myFirstRedis            
myFirstRedis		# 停止容器
(base) root@192 ~ % docker start myFirstRedis
myFirstRedis		# 重新启动容器
(base) root@192 ~ %  docker exec -it myFirstRedis /bin/bash
root@db18e24f57c6:/data# redis-cli
127.0.0.1:6379> CONFIG GET *		# 不输入密码的情况下命令依然生效
  1) "rdbchecksum"
  2) "yes"
  3) "daemonize"
  4) "no"
```

## 4.3.3 用`CONFIG REWRITE`命令改写Redis配置文件

* `CONFIG REWRITE`
  * 功能:使`CONFIG SET`命令造成的修改在Redis服务端重启后依然生效.前提条件是配置文件存在.若配置文件不存在则报错

例:

使用`CONFIG SET`命令临时设置密码:

```
127.0.0.1:6379> CONFIG SET requirepass 123456
OK
```

使用`CONFIG REWRITE`将配置写入`redis.conf`配置文件

```
127.0.0.1:6379> CONFIG REWRITE
(error) ERR The server is running without a config file
```

该报错是因为启动Redis服务端时未指定配置文件,故`CONFIG REWRITE`命令无法将配置项写入文件中.

## 4.3.4 启动Redis服务器时加载配置文件

* step1. 创建配置文件

创建配置文件`redis.conf`,其内容如下:

```
# 指定服务端监听的端口
port 6379

# 指定服务端监听的地址
bind 127.0.0.1

# 客户端空闲30000秒后终止该客户端连接
timeout 30000
```

* step2. 使用配置文件启动容器

创建容器:

```
(base) root@192 chapter4 % docker run -itd --name redisWithConfig -v /StudyRedisBaseOnDocker/conf/chapter4/redis.conf:/redisConfig/redis.conf -p 6379:6379 redis:latest redis-server /redisConfig/redis.conf
1717764a7a883dfefd9f4d08e4cb44c7b9255595595b6c256d80825d4a67da47
```

检查容器状态:

```
(base) root@192 chapter4 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS        PORTS                    NAMES
1717764a7a88   redis:latest   "docker-entrypoint.s…"   2 seconds ago   Up 1 second   0.0.0.0:6379->6379/tcp   redisWithConfig
```

进入容器检查配置文件:

```
(base) root@192 chapter4 % docker exec -it redisWithConfig /bin/bash
root@1717764a7a88:/data# cat /redisConfig/redis.conf
# 指定服务端监听的端口
port 6379

# 指定服务端监听的地址
bind 127.0.0.1

# 客户端空闲30000秒后终止该客户端连接
timeout 30000
```

* step3. 在容器中配置密码并使用`CONFIG REWRITE`命令覆写到配置文件中

修改密码并覆写到配置文件:

```
127.0.0.1:6379> CONFIG SET requirepass 123456
OK
127.0.0.1:6379> CONFIG REWRITE
(error) ERR Rewriting config file: Device or resource busy
```

TODO:此处猜测是因为我本机是一个macOS的缘故,或者是宿主机上我对该文件的权限没有设置对的缘故,导致的写入配置失败.


# 4.4 多个客户端连接远端服务器

## 4.4.1 多个Redis客户端连接远端服务器

在本节中,将新建三个Docker容器,以此模拟三台主机.在这三个Docker容器里,包含了一个Redis服务器、两个Redis客户端,并且两个Redis客户端会连接到Redis服务器上,以此模拟两台主机上的Redis客户端远程连接到Redis服务器上的效果.具体的效果图如下图示.

两台Redis客户端远程连接到Redis服务器的效果

![两台Redis客户端远程连接到Redis服务器的效果](/files/2myfaaL977hKP2HAYETp)

* step1. 创建表示redis-server的容器

```
(base) root@yuanhong StudyRedisBaseOnDocker % docker run --name redis-server -d redis:latest
834148eedc13916828f2c1dc72a6b8df30cfcde01b91b128ab099617008035e4
```

* step2. 创建并进入Redis客户端容器,在该容器内连接redis-server

创建并进入Redis客户端容器:

```
(base) root@yuanhong StudyRedisBaseOnDocker % docker run -it --name redis-client1 --link redis-server:server redis:latest /bin/bash
root@b33298a59a78:/data# 
```

注:`--link`选项的含义:

在Docker的早期版本中,`--link`选项是用来在两个容器之间建立一个链接的.这意味着从一个容器可以安全地连接到另一个容器,而无需公开任何不必要的端口到宿主机.当您使用`--link`选项时,Docker会在两个容器之间建立一个私有网络连接,并将链接的容器的环境变量、主机名和其他相关信息传递给源容器.这使得源容器可以轻松地发现和连接到链接的容器.

语法:`--link 链接容器名称:源容器内的主机名`

连接redis-server:

```
root@b33298a59a78:/data# redis-cli -h server -p 6379
server:6379> 
```

* step3. 尝试在客户端执行命令

```
server:6379> SET name Peter
OK
```

* step4. 再打开一个cmd窗口,再创建一个redis客户端容器

```
(base) root@yuanhong StudyRedisBaseOnDocker % docker run -it --name redis-client2 --link redis-server:server redis:latest /bin/bash
root@6945b1178f8c:/data# 
```

## 4.4.2 通过docker inspect命令观察IP地址

* step1. 检查redis-server容器的IP地址

```
(base) root@yuanhong ~ % docker inspect redis-server|grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",
```

可以看到,名为`redis-server`的容器,其IP地址为`172.17.0.2`

* step2. 检查redis-client1容器的IP地址

```
(base) root@yuanhong ~ % docker inspect redis-client1|grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.3",
                    "IPAddress": "172.17.0.3",
```

可以看到,名为`redis-client1`的容器,其IP地址为`172.17.0.3`

* step3. 检查redis-client2容器的IP地址

```
(base) root@yuanhong ~ % docker inspect redis-client2|grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.4",
                    "IPAddress": "172.17.0.4",
```

可以看到,名为`redis-client2`的容器,其IP地址为`172.17.0.4`

## 4.4.3 实践客户端命令

在redis-client1容器中,查看连接到Redis服务器的所有客户端信息:

```
server:6379> CLIENT LIST
id=3 addr=172.17.0.3:49948 laddr=172.17.0.2:6379 fd=8 name= age=724 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1
id=4 addr=172.17.0.4:39362 laddr=172.17.0.2:6379 fd=9 name= age=4 idle=4 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 argv-mem=0 obl=0 oll=0 omem=0 tot-mem=20496 events=r cmd=command user=default redir=-1
```

从2个连接的`addr`和`laddr`属性可以看出,和上一小节中获取的容器IP地址是相同的

设置redis-client1的连接名为client1:

```
server:6379> CLIENT GETNAME
(nil)
server:6379> CLIENT SETNAME client1
OK
server:6379> CLIENT GETNAME
"client1"
```

注:此时若在任意一个redis客户端上执行`SHUTDOWN`命令,则redis服务端和客户端都会关闭连接.该命令很容易造成生产事故!

## 4.4.4 通过info观察服务器状态

在redis-client2容器内连接redis-server后,查看服务器状态:

```
server:6379> INFO Stats
# Stats
total_connections_received:2
total_commands_processed:9
instantaneous_ops_per_sec:0
total_net_input_bytes:354
total_net_output_bytes:45639
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:39
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0
total_forks:0
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
total_error_replies:2
dump_payload_sanitizations:0
total_reads_processed:12
total_writes_processed:11
io_threaded_reads_processed:0
io_threaded_writes_processed:0
```

注:此处只查看了Stats部分的内容

在redis-client1容器中执行一些命令:

```
server:6379> SET salary 10000
OK
server:6379> SET gender male
OK
```

再次在redis-client2容器中查看服务器状态:

```
server:6379> INFO Stats
# Stats
total_connections_received:2
total_commands_processed:12
instantaneous_ops_per_sec:0
total_net_input_bytes:450
total_net_output_bytes:46610
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:41
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0
total_forks:0
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
total_error_replies:2
dump_payload_sanitizations:0
total_reads_processed:15
total_writes_processed:14
io_threaded_reads_processed:0
io_threaded_writes_processed:0
```

可以看到`total_commands_processed`配置项发生了变化


# 第5章 Redis数据库操作实战


# 5.1 切换数据库操作

在默认情况下,Redis服务器在启动时会**创建16个数据库**,不同的应用程序可以连到不同的数据库上,通过键值对的形式实现缓存等操作.

在实际项目里,常见的操作有**通过修改配置更改在启动Redis服务器时创建数据库的个数**,以及**通过`SELECT`命令切换当前程序所用的Redis数据库**.

## 5.1.1 查看和设置默认的数据库个数

* step1. 停止并删除之前创建的redis容器

```
(base) root@yuanhong StudyRedisBaseOnDocker % docker ps -a 
CONTAINER ID   IMAGE                        COMMAND                  CREATED        STATUS                    PORTS     NAMES
```

可以看到,此时没有任何容器被启动或停止

* step2. 创建一个空的配置文件`redis.conf`

```
(base) root@yuanhong StudyRedisBaseOnDocker % cd conf/chapter5/section5-1 
(base) root@yuanhong section5-1 % cat redis.conf
(base) root@yuanhong section5-1 % 
```

* step3. 基于上述配置文件创建redis服务端

```
(base) root@yuanhong section5-1 % docker run -itd --name redis-server -v /StudyRedisBaseOnDocker/conf/chapter5/section5-1/redis.conf:/redisConfig/redis.conf -p 6379:6379 redis:latest redis-server /redisConfig/redis.conf
2c51ddee569950dd0d6b460433f176239aa6ade427d61e2f57f87edec8d8204c
```

此时由于配置文件中没有内容,所以启动时还是会加载各种默认的配置参数

* step4. 检查数据库数量

```
(base) root@yuanhong section5-1 % docker exec -it redis-server /bin/bash
root@2c51ddee5699:/data# redis-cli
127.0.0.1:6379> CONFIG GET databases
1) "databases"
2) "16"
```

* step5. 停止容器并修改配置文件

停止容器:

```
127.0.0.1:6379> exit
root@2c51ddee5699:/data# exit
exit
(base) yanglei@yuanhong section5-1 % docker stop redis-server      
redis-server
```

修改配置文件的内容如下:

```
(base) yanglei@yuanhong section5-1 % cat redis.conf 
# 指定数据库数量
databases		 12
```

* step6. 重新启动容器

```
(base) yanglei@yuanhong section5-1 % docker start redis-server
redis-server 
```

* step7. 进入容器检查配置是否生效

```
(base) yanglei@yuanhong section5-1 % docker exec -it redis-server /bin/bash
root@2c51ddee5699:/data# redis-cli
127.0.0.1:6379> CONFIG GET databases
1) "databases"
2) "12"
```

## 5.1.2 用`SELECT`命令切换数据库

使用`CLIENT LIST`命令查看客户端连接信息:

```
127.0.0.1:6379> CLIENT LIST
id=3 addr=127.0.0.1:35774 laddr=127.0.0.1:6379 fd=8 name= age=121 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1
```

其中的`db=0`表示当前客户端使用的是0号数据库

在第0号数据库中设置一些键值对:

```
127.0.0.1:6379> SET name 'Peter'
OK
```

使用`SELECT`命令切换数据库:

```
127.0.0.1:6379> SELECT 1
OK
```

再次查看客户端连接信息:

```
127.0.0.1:6379[1]> CLIENT LIST
id=3 addr=127.0.0.1:35774 laddr=127.0.0.1:6379 fd=8 name= age=286 idle=0 flags=N db=1 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1
```

可以看到,此时`db=1`

获取刚刚设置的key:

```
127.0.0.1:6379[1]> GET name
(nil)
```

切换回0号数据库后再次获取刚刚设置的key:

```
127.0.0.1:6379[1]> SELECT 0
OK
127.0.0.1:6379> GET name
"Peter"
```

在实际应用中,一般不会更改Redis服务器的数据库个数.但是当不同的应用同时使用同一个Redis服务器时,建议让不同的应用使用不同的数据库,比如让订单应用模块使用0号数据库,会员应用模块使用1号数据库


# 5.2 Redis事务操作

## 5.2.1 事务的概念与ACID特性

ACID特性:

* A:Atomicity,表示原子性.即事务是一个不可分割的实体,事务中的操作要么都做,要么全都不执行.
* C:Consistency.表示一致性.即事务前后数据完整性必须一致,假设数据库里有很多完整性约束,比如ID字段不能为空,且必须是10位,在事务执行前后,这些完整性约束不能被违反
* I:Isolation.即一个事务内部操对其他事务是隔离的,并发执行的各事务间不能互相干扰
* D:Durability.指一个事务一旦提交,它对数据库的改变就是永久性的,哪怕数据库出现故障,事务执行后的操作也该丢失

## 5.2.2 实现Redis事务的相关命令

* `MULTI`
  * 功能:标记一个事务块的开始
* `DISCARD`
  * 功能:取消事务,放弃执行事务块内的所有命令
* `WATCH`
  * 语法:`WATCH key [key ...]`
  * 功能:监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断
* `UNWATCH`
  * 功能:取消`WATCH`命令对所有key的监控
  * 注意:该命令不能取消对指定键的监控,只能取消所有键的监控

注意:当你执行`EXEC`或`DISCARD`之后,Redis会自动取消对所有WATCH过的键的监视.所以,事务成功执行后(使用`EXEC`)或放弃事务后(使用 `DISCARD`),你不需要显式地调用`UNWATCH`

例:

```
127.0.0.1:6379> SET name 'Peter'
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET id '001'
QUEUED
127.0.0.1:6379(TX)> GET id
QUEUED
127.0.0.1:6379(TX)> SET depName 'Dev'
QUEUED
127.0.0.1:6379(TX)> SET age 25
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) "001"
3) OK
4) OK
127.0.0.1:6379> GET age
"25"
```

对于命令`SET name 'Peter'`而言,此时还没有开启事务,因此返回的结果是`OK`

然后通过`MULTI`命令开启了事务.可以看到开启事务后,命令提示符尾部多了个`(TX)`

从`SET id '001'`命令的返回值(`QUEUED`)可以看出,该命令返回的结果并不是`OK`,而是`QUEUED`,表示该命令当前没有执行,而是放到了事务队列中

对于`GET id`命令而言也是一样,该命令的返回值并不是`id`对应的值,而是`QUEUED`,同样表示该命令被放到了事务队列中

最终通过`EXEC`命令执行事务.该命令会一次性地返回包含在事务队列中所有命令的执行结果.一旦执行完`EXEC`命令,就退出了事务状态.因此可以看到`GET age`命令会立即返回结果

本例表明:当通过`MULTI`命令开启事务状态后,**之后的命令不是立即执行,而是会被放入事务队列**.当exec命令出现后,则会一次性地执行事务队列中的命令

## 5.2.3 通过`DISCARD`命令撤销事务中的操作

例:

```
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET name 'Peter'
QUEUED
127.0.0.1:6379(TX)> SET age 19
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> GET age
(nil)
127.0.0.1:6379> GET name
(nil)
```

注:开启事务前执行过`FLUSHDB`

可以看到,执行了`DISCARD`命令后,退出了事务状态

在实际的应用中,如果能确保`MULTI`后的命令均能正确执行,那么可以通过exec来提交事务;反之,则可以通过`DISCARD`命令来撤销操作,以此体现事务的"原子性".

## 5.2.4 通过`WATCH`命令监听键的变化

注:本节使用的是我本地的redis,而非是容器中的redis

在Redis中,`WATCH`命令是为了实现乐观锁提供的一种机制.它使得我们可以监视一个或多个键,然后根据这些键的值是否被其他客户端修改来决定是否继续执行事务.

乐观锁的核心思想是假设数据在大部分时间都不会产生冲突,因此先进行数据操作,如果后续发现数据有冲突(比如被其他客户端修改过)再进行相应的处理.

`WATCH`的工作流程如下:

1. 使用`WATCH`命令监视一个或多个键
2. 执行一系列命令,这些命令会基于你所监视的键的当前值
3. 使用`MULTI`开始一个事务
4. 将你想在事务中执行的所有命令加入队列
5. 使用`EXEC`命令尝试执行事务.如果从执行`WATCH`命令开始,所监视的任何键都没有被其他客户端修改过,那么事务将成功执行.否则,事务将不执行任何命令并返回一个错误,告诉你有至少一个被监视的键已被修改

这个机制允许你确保在执行事务时,被监视的键的值与你上次检查时的值保持一致.

例如,考虑一个简单的场景,你想从一个键`counter`中递增一个值，但只有在该值没有被更改过的情况下才这样做。你可以使用`WATCH`来确保在你检查`counter`值和递增它之间,没有其他客户端修改它

* step1. 确保存在名为`counter`的键存在:

```
127.0.0.1:6379> SET counter 5
OK
```

* step2. 监视这个键

```
127.0.0.1:6379> WATCH counter
OK
```

* step3. 查看该键当前的值

```
127.0.0.1:6379> GET counter
"5"
```

* step4. 再开一个`redis-cli`客户端,在该客户端中修改`counter`的值

```
(base) root@yuanhong ~ % redis-cli
127.0.0.1:6379> SET counter 6
OK
```

* step5. 回到原始的`redis-cli`,开启一个事务并尝试递增`counter`

```
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR counter
QUEUED
127.0.0.1:6379(TX)> EXEC
(nil)
```

由于`counter`在你开始事务之前已经被修改,所以`EXEC`返回`nil`,表示事务没有执行.

## 5.2.5 Redis持久化与事务持久性

Redis会把数据缓存在内存中,这在带来性能便利的同时,会给事务的持久性带来一定的障碍.事务的持久性是指,一旦Redis的事务通过`EXEC`命令执行完成后,对Redis数据库的影响应当是永久性的.不过假设某事务执行后Redis服务器因断电重启,那么保存在该服务器内存里的Redis数据就会丢失,所以在这种场景里出现故障时无法确保事务的持久性

对此,可以通过Redis的持久化来确保事务的持久性.Redis持久化是指把Redis缓存数据从内存中保存到硬盘上.Redis持久化的方式有两种:

* AOF:Append Only File(AOF)
* RDB:Redis DataBase(RDB)

在后继章节里会详细讲述这两种能确保事务持久性的方式,这里仅给出实现AOF和RDB持久化的基本配置,以此来讨论这两种持久化与事务持久性的关系

**AOF持久化方式能确保事务的持久性,而RDB方式则不能**.

设置Redis服务器基于AOF的持久化:

```
127.0.0.1:6379> CONFIG SET appendfsync always
OK
127.0.0.1:6379> CONFIG REWRITE
OK
```

AOF持久化的方式,针对Redis数据的事务能即时存入硬盘文件中.这样一旦出现故障,数据也不会丢失,由此能确保事务的持久性

设置Redis服务器基于RDB的持久化:

```
127.0.0.1:6379> CONFIG SET dir /StudyRedisBaseOnDocker/conf/chapter5/section5-1/
OK
127.0.0.1:6379> CONFIG SET dbfilename redisRDB.rdb
OK
127.0.0.1:6379> CONFIG REWRITE
OK
```

* `CONFIG SET dir`:设置待持久化文件的路径
* `CONFIG SET dbfilename`:设置持久化文件的文件名

基于RDB的持久化方式是需要满足一定条件后(例如1分钟内至少有100个键被修改)才会触发持久性,反之不触发

所以,在某些场合里无法即时把Redis的修改记录到硬盘上,如果此时发生故障,数据就无法恢复,也就是说,Redis的RDB持久化方式无法确保事务的持久性


# 5.3 地理位置相关操作

## 5.3.1 用`GEOADD`命令存储地理位置

* `GEOADD`
  * 语法:`GEOADD key longitute latitude member [longitute latitude member ...]`.其中:
    * longitute:经度
    * latitude:纬度
    * member:位置名称
  * 功能:存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的key中.添加成功返回1,失败则返回错误信息

例:添加地理位置:

```
127.0.0.1:6379> GEOADD pos 120.52 30.40 pos1
(integer) 1
127.0.0.1:6379> GEOADD pos 120.52 31.53 pos2
(integer) 1
127.0.0.1:6379> GEOADD pos 120.12 30.40 pos3
(integer) 1
127.0.0.1:6379> GEOADD pos 120.12 31.53 pos4
(integer) 1
```

可以看到,4个地理位置的键均为`pos`,但是这4个地理位置的别名不同.

例:添加地理位置失败:

```
127.0.0.1:6379> GEOADD errPos 1221.12 311.53 errPos
(error) ERR invalid longitude,latitude pair 1221.120000,311.530000
```

在Redis的`GEO`数据结构中,经纬度的上下限是基于`WGS-84`地理坐标系统的.具体限制如下：

* Longitude(经度):-180°到180°
* Latitude(纬度):-85.05112878°到85.05112878°

纬度的上下限并不是完全的-90°到90°,是因为这样可以确保地球的形状在某些投影下仍然为正方形.这个特定的纬度约为±85.0511°是Web墨卡托投影的一个特点,这种投影经常被用于地图服务,例如Google Maps.

## 5.3.2 获取地理位置的经纬度信息

* `GEOPOS`
  * 语法:`GEOPOS key member [member ...]`
  * 功能:从给定的key里返回所有指定名称(member)的位置(经度和纬度),不存在的返回nil

例:查询地理位置数据

```
127.0.0.1:6379> GEOPOS pos pos1
1) 1) "120.52000075578689575"
   2) "30.39999952668997452"
127.0.0.1:6379> GEOPOS pos pos4
1) 1) "120.11999756097793579"
   2) "31.53000103201371473"
127.0.0.1:6379> GEOPOS pos notExist
1) (nil)
127.0.0.1:6379> GEOPOS notExist notExist
1) (nil)
```

## 5.3.3 查询指定范围内的地理信息

* `GEORADIUS`
  * 语法:`GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]`.其中:
    * `longitude`/`latitude`:指定待查询地理信息的中心点
    * `radius`:半径.
      * `m`:米
      * `km`:千米
      * `ft`:英尺
      * `mi`:英里
    * `WITHCOORD`:将位置元素的经度和纬度也一并返回
    * `WITHDIST`:在返回位置元素的同时,将位置元素与中心点之间的距离也一并返回
    * `WITHHASH`:以52位有符号整数的形式,返回位置元素经过原始`geohash`编码的有序集合分值.这个选项主要用于底层应用或者调试,实际中的作用并不大
    * `ASC/DESC`:查找结果根据距离从近到远/从远到近排序
  * 功能:以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素

例:查询pos中距给定中心点距离不超过200km的位置元素的经纬度信息和距给定中心点的距离,按位置元素距中心点从远到近排序

```
127.0.0.1:6379> GEORADIUS pos 120.52 30.40 200 km WITHCOORD WITHDIST DESC
1) 1) "pos4"
   2) "131.3478"
   3) 1) "120.11999756097793579"
      2) "31.53000103201371473"
2) 1) "pos2"
   2) "125.6858"
   3) 1) "120.52000075578689575"
      2) "31.53000103201371473"
3) 1) "pos3"
   2) "38.3739"
   3) 1) "120.11999756097793579"
      2) "30.39999952668997452"
4) 1) "pos1"
   2) "0.0001"
   3) 1) "120.52000075578689575"
      2) "30.39999952668997452"
```

* `GEORADIUSBYMEMBER`
  * 语法:`GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]`
  * 功能:和`GEORADIUS`命令一样,都可以找出位于指定范围内的元素.但是`GEORADIUSBYMEMBER`命令的中心点是由给定的位置元素决定的,而不是使用经度和纬度来决定中心点

例:查询pos中距离位置元素pos1距离不超过200km的位置元素的经纬度信息和距给定中心点的距离,按位置元素距中心点从远到近排序

```
127.0.0.1:6379> GEORADIUSBYMEMBER pos pos1 200 km WITHCOORD WITHDIST DESC
1) 1) "pos4"
   2) "131.3479"
   3) 1) "120.11999756097793579"
      2) "31.53000103201371473"
2) 1) "pos2"
   2) "125.6859"
   3) 1) "120.52000075578689575"
      2) "31.53000103201371473"
3) 1) "pos3"
   2) "38.3740"
   3) 1) "120.11999756097793579"
      2) "30.39999952668997452"
4) 1) "pos1"
   2) "0.0000"
   3) 1) "120.52000075578689575"
      2) "30.39999952668997452"
```

## 5.3.4 查询地理位置间的距离

* `GEODIST`
  * 语法:`GEODIST key member1 member2 [m|km|ft|mi]`
  * 返回两个给定位置之间的距离.距离单位默认为米.若待计算的地理位置不存在,则返回nil

例:计算位置元素pos1到pos2之间间隔的千米数:

```
127.0.0.1:6379> GEODIST pos pos1 pos2 km
"125.6859"
```

例:待计算的地理位置不存在的情况

```
127.0.0.1:6379> GEODIST pos pos1 notExist
(nil)
```


# 5.4 位图数据类型的应用

在Redis里,位图(Bitmap)是由一串二进制数字组成的,它不是一种数据类型,而是基于字符串、能面向字节操作的对象.位图的长度不固定,但是在计算机里8位(bit)能组成一个字节(Byte),所以位图的长度一般是8或者是8的倍数

## 5.4.1 `SETBIT`和`GETBIT`操作

* `SETBIT`
  * 语法:`SETBIT key offset value`.其中:
    * `offset`:偏移量(偏移量从左到右计算)
    * `value`:待设置的值
  * 功能:设置指定键的位图数据
* `GETBIT`
  * 语法:`GETBIT key offset`
  * 功能:读取位图指定位数据.若指定位不存在则返回0

例:

设置位图:

```
127.0.0.1:6379> SETBIT myBitmap 0 1
(integer) 0
127.0.0.1:6379> SETBIT myBitmap 1 1
(integer) 0
127.0.0.1:6379> SETBIT myBitmap 2 0
(integer) 0
127.0.0.1:6379> SETBIT myBitmap 3 0
(integer) 0
127.0.0.1:6379> SETBIT myBitmap 5 1
(integer) 0
```

注意:设置位图数据时只能将值设置为0或1:

```
127.0.0.1:6379> SETBIT myBitmap 7 3
(error) ERR bit is not an integer or out of range
```

读取位图:

```
127.0.0.1:6379> GETBIT myBitmap 1
(integer) 1
127.0.0.1:6379> GETBIT myBitmap 3
(integer) 0
```

读取一个不存在的位:

```
127.0.0.1:6379> GETBIT myBitmap 7
(integer) 0
```

## 5.4.2 用`BITOP`对位图进行运算

* `BITOP`
  * 语法:`BITOP operation destkey key [key ...]`.其中:
    * `operation`:操作符.
      * `AND`:按位与
      * `OR`:按位或
      * `XOR`:按位异或(如果两个比较的位相同,则结果为0;如果两个比较的位不同,则结果为1)
      * `NOT`:按位取反
    * `destkey`:用于保存运算结果的key名
  * 功能:操作位图

例:

创建2个位图:

```
127.0.0.1:6379> SETBIT bit1 0 1
(integer) 0
127.0.0.1:6379> SETBIT bit1 1 1
(integer) 0
127.0.0.1:6379> SETBIT bit1 3 1
(integer) 0
127.0.0.1:6379> SETBIT bit2 2 1
(integer) 0
```

即:

* `bit1`:`1011`
* `bit2`:`0100`

按位与操作:

注:按位与操作的结果应为`0000`

```
127.0.0.1:6379> BITOP AND result bit1 bit2
(integer) 1
```

查看结果:

```
127.0.0.1:6379> GETBIT result 0
(integer) 0
127.0.0.1:6379> GETBIT result 1
(integer) 0
127.0.0.1:6379> GETBIT result 2
(integer) 0
127.0.0.1:6379> GETBIT result 3
(integer) 0
127.0.0.1:6379> get result
"\x00"
```

按位或操作:

注:按位或操作的结果应为`1111`

```
127.0.0.1:6379> BITOP OR result bit1 bit2
(integer) 1
```

查看结果:

```
127.0.0.1:6379> GETBIT result 0
(integer) 1
127.0.0.1:6379> GETBIT result 1
(integer) 1
127.0.0.1:6379> GETBIT result 2
(integer) 1
127.0.0.1:6379> GETBIT result 3
(integer) 1
```

按位取反操作:

注:`bit1`按位取反的结果应为`0100`

```
127.0.0.1:6379> BITOP not result bit1
(integer) 1
```

查看结果:

```
127.0.0.1:6379> GETBIT result 0
(integer) 0
127.0.0.1:6379> GETBIT result 1
(integer) 0
127.0.0.1:6379> GETBIT result 2
(integer) 1
127.0.0.1:6379> GETBIT result 3
(integer) 0
```

按位异或操作:

注:按位异或的结果应为`1111`

```
BITOP xor result bit1 bit2
```

查看结果:

```
127.0.0.1:6379> GETBIT result 0
(integer) 1
127.0.0.1:6379> GETBIT result 1
(integer) 1
127.0.0.1:6379> GETBIT result 2
(integer) 1
127.0.0.1:6379> GETBIT result 3
(integer) 1
```

## 5.4.3 `BITCOUNT`操作

* `BITCOUNT`
  * 语法:`BITCOUNT key [start end]`
  * 功能:统计key在指定范围内的1的出现次数.`0, -1`或不写参数表示统计范围为整个key

注:关于`BITCOUNT`命令中的`start, end`:

`start, end`**表示字符串中的字节偏移,而不是比特偏移**.这一点非常重要.

* `start`:是子范围的起始字节位置(从0开始)
* `end`:是子范围的结束字节位置

例如,对于字符串"foobar":

* 字符"f"在字节偏移0中
* 字符"o"在字节偏移1和2中
* 字符"b"在字节偏移3中
* ...以此类推。

如果你执行命令`BITCOUNT key 0 1`.它会计算"fo"中设置为1的比特位的数量,因为"fo"占据了字节偏移0到1

还有一些其他需要注意的事项:

1. 当start和end都是正数或0时,范围包括start和end两端
2. 当start或end是负数时,它表示从字符串的末尾开始的偏移.例如,-1表示字符串的最后一个字节
3. 如果start大于字符串的长度,或end小于0,结果将是 0
4. 如果end在start之后,则它们会被交换.所以命令`BITCOUNT key 2 0`和`BITCOUNT key 0 2`会产生相同的结果

例:统计用户在线天数

设置用户在线天数

```
127.0.0.1:6379> SETBIT user1 0 1
(integer) 0
127.0.0.1:6379> SETBIT user1 3 1
(integer) 0
127.0.0.1:6379> SETBIT user1 7 1
(integer) 0
```

统计用户在线天数:

```
127.0.0.1:6379> BITCOUNT user1
(integer) 3
127.0.0.1:6379> BITCOUNT user1 0 0
(integer) 3
```


# 5.5 慢查询实战分析

## 5.5.1 慢查询相关的配置参数

* `slowlog-log-slower-than`:单位为微秒.超过该参数指定时长的查询会记录到日志中
* `slowlog-max-len`:在慢查询日志中可以记录的日志条数.当慢查询日志的数量已经到达该参数时,每有一条新的慢查询日志写入,就会有最老的一条日志被删除

例:设置慢查询的时长和慢查询日志的条数

```
127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 1
OK
127.0.0.1:6379> CONFIG SET slowlog-max-len 100
OK
127.0.0.1:6379> CONFIG REWRITE
OK
```

此时配置文件内容如下:

```
# Generated by CONFIG REWRITE
slowlog-max-len 100
slowlog-log-slower-than 1
save 3600 1
save 300 100
save 60 10000
user default on nopass ~* &* +@all
dir "/redis-6.2.13"
```

## 5.5.2 用`SLOWLOG GET`命令观察慢查询

* `SLOWLOG GET`:返回所有的慢查询日志

```
127.0.0.1:6379> SET name Peter
OK
127.0.0.1:6379> GET name
"Peter"
```

```
127.0.0.1:6379> SLOWLOG GET
1) 1) (integer) 4
   2) (integer) 1692022815
   3) (integer) 3
   4) 1) "GET"
      2) "name"
   5) "127.0.0.1:57267"
   6) ""
2) 1) (integer) 3
   2) (integer) 1692022811
   3) (integer) 18
   4) 1) "SET"
      2) "name"
      3) "Peter"
   5) "127.0.0.1:57267"
   6) ""
3) 1) (integer) 2
   2) (integer) 1692022564
   3) (integer) 6865
   4) 1) "CONFIG"
      2) "REWRITE"
   5) "127.0.0.1:57267"
   6) ""
4) 1) (integer) 1
   2) (integer) 1692022560
   3) (integer) 5
   4) 1) "CONFIG"
      2) "SET"
      3) "slowlog-max-len"
      4) "100"
   5) "127.0.0.1:57267"
   6) ""
5) 1) (integer) 0
   2) (integer) 1692022556
   3) (integer) 13
   4) 1) "CONFIG"
      2) "SET"
      3) "slowlog-log-slower-than"
      4) "1"
   5) "127.0.0.1:57267"
   6) ""
```

我们以`GET name`命令的慢查询日志为例:

* `1) (integer) 4`:表示慢查询日志ID
* `2) (integer) 1692022815`:命令运行时的时间戳
* `3) (integer) 3`:命令运行的时长.单位为微秒
* `4) 1) "GET" 2) "name"`:一个数组.其中第一个元素是执行的命令,后续的元素是命令的参数
* `5) "127.0.0.1:57267"`:客户端地址和端口
* `6) ""`:客户端名称

## 5.5.3 慢查询相关命令

* `SLOWLOG GET n`:查看指定最新的n条慢查询日志

```
127.0.0.1:6379> SLOWLOG GET 1
1) 1) (integer) 5
   2) (integer) 1692022854
   3) (integer) 16
   4) 1) "SLOWLOG"
      2) "GET"
   5) "127.0.0.1:57267"
   6) ""
```

* `SLOWLOG LEN`:获取慢查询日志的长度

```
127.0.0.1:6379> SLOWLOG LEN
(integer) 9
```

* `SLOWLOG RESET`:清空慢查询日志

```
127.0.0.1:6379> SLOWLOG RESET
OK
127.0.0.1:6379> SLOWLOG LEN
(integer) 1
```

注:此处有1条慢查询日志是因为慢查询阈值设置的太低,导致`SLOWLOG LEN`也成为了慢查询


# 第6章 Redis数据持久化操作


# 6.1 Redis持久化机制概述

对于Redis而言,持久化机制是指**把内存中的数据存储为硬盘文件,这样当Redis重启或服务器故障时能根据持久化后的硬盘文件恢复数据**.

## 6.1.1 基于AOF的持久化机制

在AOF持久化的过程中,会以日志的方式记录每个Redis"写"命令,并在Redis服务器重启时重新执行AOF日志文件中的命令,从而达到"恢复数据"的效果.如下图示:

![AOF持久化大致流程](/files/prPGqhcrnEwSDWtLVg5Q)

当AOF持久化功能打开时,每当发生写命令,该命令就会被记录到AOF缓冲区里,AOF缓冲区会根据事先配置的策略定期与硬盘文件进行同步操作,而且当AOF文件大道一定程度后,该文件会被重写,即在不影响持久化结果的前提下进行压缩.此外,当Redis服务器重启时,会加载硬盘上的AOF日志文件,以实现数据恢复的效果.

当Redis因发生故障而重启时,Redis服务器会按照如下步骤,根据AOF日志文件恢复数据:

* step1. 创建一个伪客户端(fake client)

之所以叫伪客户端,是因为该客户端在本地,不带有任何网络连接,但该伪客户端执行命令的效果和真实的带网络的客户端没有任何差别

* step2. 执行命令

该伪客户端从AOF日志文件里依次读取每条写命令并执行,直到完成所有写命令

## 6.1.2 基于RDB的持久化机制

基于RDB的持久化方式会把当前内存中所有Redis键值对数据以快照(snapshot)的方式写入硬盘中,如果需要恢复数据,把快照文件读到内存中即可.

**RDB快照文件是经压缩的二进制格式的文件**.它的存储路径可以在Redis服务器启动前通过配置参数来设置,也可以在Redis运行时通过命令来设置

2种方式可以触发RDB持久化机制:

* `SAVE`和`BGSAVE`等命令手动触发
* 根据配置文件中设定的方式定期把数据写入快照

基于RDB的持久化方式比较适合数据备份和灾备的场景,但RDB无法实现即时备份.

虽然RDB无法实现实时备份,但相比于AOF,RDB持久化也有其优点:

1. **备份文件体积较小**:RDB生成的是数据集的二进制快照,通常比AOF文件要小得多.因为AOF记录了所有的写操作命令,随着时间的推移,AOF文件可能会变得非常大
2. **快速数据恢复**:由于RDB是数据的快照,当Redis启动时,它可以直接加载RDB文件进行数据恢复,这通常比重新执行AOF文件中的所有命令要快得多
3. **低I/O开销**:RDB的持久化是间隔一段时间进行的(基于配置的时间和数据变化阈值),而不是像AOF那样每次写操作都要进行磁盘I/O.这意味着在高并发写入的场景下,RDB可能会提供更好的性能
4. **简单的备份策略**:RDB文件提供了一个非常简单的方式来备份Redis数据.管理员可以简单地将RDB文件复制到另一个位置或系统,以实现数据备份
5. **避免潜在的AOF问题**:尽管AOF提供了更精确的数据恢复能力,但它也可能带来一些问题,例如文件过大、AOF重写带来的性能问题等.使用RDB可以避免这些问题

反之,与RDB相比,AOF(Append Only File)持久化方式具有以下优点:

1. **数据持久性更强**:AOF持久化记录了所有的写操作命令,因此它可以提供更强的数据持久性.即使在系统故障后,只会丢失最后一次`fsync`以来的数据.与RDB相比,AOF通常可以提供更短的数据丢失窗口
2. **更为健壮**:由于AOF文件是一个命令日志,即使文件中的部分数据因为某些原因变得损坏或不一致,Redis也可以加载文件中的大部分数据.而RDB文件,如果损坏,可能会导致整个文件都无法加载
3. **实时或近实时备份**:你可以配置Redis以多种模式写入AOF文件:每次写操作后、每秒或者完全由操作系统决定.这意味着,相对于RDB的定期快照,AOF可以提供更为实时的数据备份
4. **更精确的数据恢复**:由于AOF记录了每一个写操作,所以在系统重启后,它可以通过重新执行这些操作来恢复数据,从而实现非常精确的数据恢复
5. **易于人工干预和理解**:AOF文件是一个文本文件,记录了所有的Redis写命令.这意味着,在必要时,你可以手动编辑AOF文件(尽管不推荐这么做),或者更容易地理解其内容


# 6.2 AOF持久化机制实战

## 6.2.1 AOF配置文件说明

在默认情况下,Redis服务器是不会开启AOF持久化机制的,如果需要开启,可以在`redis.conf`等配置文件里通过修改`appendonly`参数值为`yes`来开启.如果要关闭基于AOF的持久化功能,将该参数值设置为`no`即可.

```
appendonly yes
```

用`appendonly`参数开启AOF持久化后,通过`appendfsync`参数可以设置持久化策略,该参数有3个取值:`always`、`everysec`、`no`

* `appendfsync`参数:设置持久化策略
  * `always`:每次发生Redis的写命令时都会触发持久化动作,这样可能会影响到Redis甚至是Redis所在服务器的性能
  * `everysec`:以一秒的频率触发持久化动作,在这种方式下能很好地平衡持久化需求和性能间的关系,一般情况下取这个值
  * `no`:会由操作系统来决定持久化的频率,这种方式对其他另外两种而言性能最好,但可能每次持久化操作间的间隔有些长,这样当故障发生时可能会丢失较多的数据
* `dir`参数:设置持久化文件所在路径
* `appendfilename`:设置持久化文件的文件名.默认为`appendonly.aof`
* `aof-load-truncated`:定义AOF文件的加载策略.具体表现为:在AOF持久化文件损坏的前提下,启动Redis时是否会加载,默认取值`yes`

随着持久化数据的增多,对应的AOF文件会越来越大,这可能会影响到性能.对此,Redis提供了AOF文件重写功能.具体而言,Redis能创建新的AOF文件来替代现有的AOF文件,在数据恢复时,这两个文件的效果是相同的,但新文件不会包含冗余命令,所以文件大小会比原来的小

可以通过如下三个参数来定义重写时的策略:

* `no-appendfsync-on-rewrite`:该参数用于平衡性能和安全性.如果该参数取值为`yes`,那么在重写AOF文件时能提升性能,但可能在重写AOF文件时丢失数据;如果取值为`no`,则不会丢失数据,但相比于取值为`yes`时的性能可能会降低.这个参数的默认取值是`no`
* `auto-aof-rewrite-percentage`:该选项指定了AOF文件增长的百分比.当当前AOF文件的大小相对于上一次AOF重写后的大小增长了该参数指定指定的百分比时,Redis将触发一个自动重写操作.前提是AOF文件的大小也超过了`auto-aof-rewrite-min-size`指定的值.默认该选项的值为`100`,这意味着当当前AOF文件的大小是上次重写后大小的两倍(即增长了100%)时,Redis 会考虑进行自动重写.但还需要满足AOF文件大小超过`auto-aof-rewrite-min-size`指定的阈值
* `auto-aof-rewrite-min-size`:该选项指定了AOF文件的最小大小.只有当AOF文件的大小超过这个值时,Redis才会考虑进行AOF重写.这是为了防止在AOF文件还很小的时候频繁地进行重写.该选项默认值为`64mb`,即只有当AOF文件的大小超过64MB时,Redis才会考虑进行自动重写.需要注意的是,仅当AOF文件大小超过这个值时,Redis才会考虑进行重写.但是否真的执行重写还取决于其他条件,例如`auto-aof-rewrite-percentage`选项

注意:`auto-aof-rewrite-percentage`和`auto-aof-rewrite-min-size`这两个参数是逻辑且关系,即只有同时满足这2个条件时才会触发重写操作.

此外,可以通过`BGREWRITEAOF`命令来手动触发针对AOF持久化文件的重写操作

## 6.2.2 实践AOF持久化

* step1. 编写配置文件

```
(base) root@yuanhong chpater6 % cat redis.conf
# 启用AOF持久化机制
appendonly yes

# 持久化频率为每秒持久化1次
appendfsync everysec

# 持久化文件的存储路径
dir /StudyRedisBaseOnDocker/conf/chpater6

# 持久化文件名称
appendfilename "my_appendonly.aof"
```

* step2. 根据配置文件启动redis-server

```
(base) root@yuanhong chpater6 % redis-server redis.conf 
```

注:此处是直接在宿主机上启动的redis-server,而非在容器中

* step3. 使用客户端连接后执行一些读写命令

```
(base) root@yuanhong ~ % redis-cli 
127.0.0.1:6379> GET name
(nil)
127.0.0.1:6379> SET name "Peter"
OK
127.0.0.1:6379> SET age 18
OK
```

* step4. 查看AOF持久化文件的内容

```
(base) root@yuanhong chpater6 % cat my_appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$5
Peter
*3
$3
SET
$3
age
$2
18
```

逐条命令分析含义:

```
*2
$6
SELECT
$1
0
```

这部分内容对应命令`SELECT 0`.其中:

* `*2`:表示接下来的命令有2个参数
* `$6`:表示接下来的参数长度为6字节
* `SELECT`:表示第一个参数,即命令名`SELECT`
* `$1`:表示接下来的参数长度为1字节
* `0`:表示第二个参数.该参数的含义为要选择的数据库编号,即`0`

```
*3
$3
SET
$4
name
$5
Peter
```

这部分内容对应命令`SET name "Peter"`

* `*3`:表示接下来的命令有3个参数
* `$3`:表示接下来的参数长度为3字节
* `SET`:表示第1个参数,即命令`SET`
* `$4`:表示接下来的参数长度为4字节
* `name`:表示第2个参数,即键名`name`
* `$5`:表示接下来的参数长度为5字节
* `Peter`:表示第3个参数,即键`name`对应的值`Peter`

```
*3
$3
SET
$3
age
$2
18
```

这部分内容对应命令:`SET age 18`

* `*3`:表示接下来的命令有3个参数
* `$3`:表示接下来的参数长度为3字节
* `SET`:表示第1个参数,即命令`SET`
* `$3`:表示接下来的参数长度为3字节
* `age`:表示第2个参数,即键名`age`
* `$2`:表示接下来的参数长度为2字节
* `18`:表示第3个参数,即键`age`对应的值`18`

## 6.2.3 观察重写AOF文件的效果

* step1. 向键为`nameList`的list中添加若干数据

```
127.0.0.1:6379> LPUSH nameList "Peter"
(integer) 1
127.0.0.1:6379> LPUSH nameList "Mary"
(integer) 2
127.0.0.1:6379> LPUSH nameList "Mike"
(integer) 3
127.0.0.1:6379> LPUSH nameList "Tom"
(integer) 4
```

* step2. 查看AOF文件

```
(base) root@yuanhong chpater6 % cat my_appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$5
Peter
*3
$3
SET
$3
age
$2
18
*3
$5
LPUSH
$8
nameList
$5
Peter
*3
$5
LPUSH
$8
nameList
$4
Mary
*3
$5
LPUSH
$8
nameList
$4
Mike
*3
$5
LPUSH
$8
nameList
$3
Tom
```

* step3. 运行`BGREWRITEAOF`命令手动触发AOF文件的重写动作

```
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
```

查看被重写后的AOF文件

```
 cat my_appendonly.aof
REDIS0009?	redis-ver6.2.13?
redis-bits?@?ctime?dused-mem??q?
                                 aof-preamble???agenameList##omMikeMaryPeter?namePeter???%?%                                                                    
```

虽然文件内容被压缩了,但从一些人类可读的字符中仍然可以看到之前AOF文件里记录的多条`LPUSH`命令被合并为1条

在实际项目里,如果没有特殊情况,一般不会主动运行`BGREWRITEAOF`命令手动触发AOF文件的重写动作,而是会通过autoaof-rewrite-percentage和auto-aof-rewrite-min-size这两个参数来定义触发重写AOF文件的条件

## 6.2.4 模拟数据恢复的流程

此处我们基于刚刚的配置文件和AOF文件启动一个容器,看容器中是否存在刚刚写入的键值对

* step1. 启动容器

```
(base) root@yuanhong chpater6 % docker run -itd --name redis-server -v /StudyRedisBaseOnDocker/conf/chpater6/redis.conf:/redisConf/redis.conf:rw -v /StudyRedisBaseOnDocker/conf/chpater6/my_appendonly.aof:/StudyRedisBaseOnDocker/conf/chpater6/my_appendonly.aof:rw -p 6379:6379 redis:latest redis-server /redisConf/redis.conf
d46b73c5a69e947baf5f3a461a467438165997357ccc1c365dd20fdbf0754571
```

* step2. 确认容器状态

```
(base) root@yuanhong chpater6 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS        PORTS                    NAMES
d46b73c5a69e   redis:latest   "docker-entrypoint.s…"   2 seconds ago   Up 1 second   0.0.0.0:6379->6379/tcp   redis-server
```

* step3. 连接redis服务器并验证数据是否存在

```
(base) root@yuanhong chpater6 % docker exec -it redis-server /bin/bash
root@d46b73c5a69e:/data# redis-cli
127.0.0.1:6379> GET name
"Peter"
127.0.0.1:6379> LRANGE nameList 0 -1
1) "Tom"
2) "Mike"
3) "Mary"
4) "Peter"
127.0.0.1:6379> GET age
"18"
```

若AOF文件损坏,可以通过`redis-check-aof`命令进行修复

## 6.2.5 修复AOF文件

例:

```
(base) root@yuanhong chpater6 % redis-check-aof --fix /StudyRedisBaseOnDocker/conf/chpater6/my_appendonly.aof
The AOF appears to start with an RDB preamble.
Checking the RDB preamble to start:
[offset 0] Checking RDB file --fix
[offset 27] AUX FIELD redis-ver = '6.2.13'
[offset 41] AUX FIELD redis-bits = '64'
[offset 53] AUX FIELD ctime = '1692079763'
[offset 68] AUX FIELD used-mem = '1077712'
[offset 84] AUX FIELD aof-preamble = '1'
[offset 86] Selecting DB ID 0
[offset 164] Checksum OK
[offset 164] \o/ RDB looks OK! \o/
[info] 3 keys read
[info] 0 expires
[info] 0 already expired
RDB preamble is OK, proceeding with AOF tail...
AOF analyzed: size=164, ok_up_to=164, ok_up_to_line=1, diff=0
AOF is valid
```

注意:`redis-check-aof`是一个二进制,不是Redis中的一个命令.且修复后会生成一个RDB文件.

当你使用`redis-check-aof --fix`命令修复AOF文件时,工具可能会发现AOF中的某些命令序列是不完整或损坏的.为了修复这些问题并确保数据的完整性,工具会进行以下操作:

1. 它启动一个Redis实例并重播AOF文件中的所有命令,就像Redis在启动时重播AOF文件一样
2. 之后,它会将这个Redis实例的当前数据集导出为一个RDB文件
3. 最后,它会将这个新的RDB文件追加到原始的AOF文件中

这个新生成的RDB文件是AOF文件修复过程的一部分.当Redis下次启动并加载这个修复后的AOF文件时,它首先会加载RDB文件中的数据集,然后再继续重播RDB之后的所有AOF命令.这确保了数据的完整性和连续性.

使用这种方法的好处是:

* RDB是一种紧凑的二进制格式,加载速度非常快
* 通过将当前数据集导出为RDB格式并追加到AOF文件的方式,可以确保数据的完整性,即使原始AOF文件中的某些部分是损坏的

总之,生成RDB文件是`redis-check-aof --fix`命令修复AOF文件的一部分,目的是为了确保数据的完整性和提高加载速度


# 6.3 RDB持久化机制实战

注:本小节开始前,请检查上一小节中执行`redis-server`位置处是否存在`dump.rdb`文件,若存在则删除.否则启动`redis-server`会失败

## 6.3.1 编写配置文件,生成RDB快照

* step1. 编写配置文件

```
(base) root@yuanhong section6-3 % cat redis.conf 
# 5秒内有1个或1个以上的键被修改时生成快照
save 5 1

# 300秒内有100个或100个以上的键被修改时生成快照
save 300 100

# 60秒内有1000个或1000个以上的键被修改时生成快照
save 60 1000

# 快照文件存储路径
dir /StudyRedisBaseOnDocker/conf/chpater6/section6-3

# 快照文件名
dbfilename my_dump.rdb
```

其中3个`save`选项之间是逻辑或关系,即只要有1个条件被满足,就会生成快照.可以看到,RDB持久化文件只是当条件满足后生成快照,因此无法即时保存当前状态的内存数据.也就是说,通过RDB恢复数据时,会丢失上次生成快照后更新的数据

注:此处设置`save 5 1`是为了触发快照机制,生产环境不会设置这么频繁的条件的

* step2. 启动redis-server

```
(base) root@yuanhong section6-3 % redis-server /StudyRedisBaseOnDocker/conf/chpater6/section6-3/redis.conf
```

* step3. 连接该redis-server并设置一些键值对

```
SET empID 001
SET empName "Mike"
```

此时满足了"5秒内有1个或1个以上的键被修改时生成快照"这个条件,因此已经有持久化文件被存储到配置文件指定的路径中了

* step4. 观察redis-server的日志

```
14021:M 15 Aug 2023 15:55:18.285 # Server initialized
14021:M 15 Aug 2023 15:55:18.285 * Ready to accept connections
14021:M 15 Aug 2023 15:55:27.866 * 1 changes in 5 seconds. Saving...
14021:M 15 Aug 2023 15:55:27.867 * Background saving started by pid 14036
14036:C 15 Aug 2023 15:55:27.868 * DB saved on disk
14021:M 15 Aug 2023 15:55:27.967 * Background saving terminated with success
14021:M 15 Aug 2023 15:55:33.715 * 1 changes in 5 seconds. Saving...
14021:M 15 Aug 2023 15:55:33.716 * Background saving started by pid 14042
14042:C 15 Aug 2023 15:55:33.723 * DB saved on disk
14021:M 15 Aug 2023 15:55:33.817 * Background saving terminated with success
```

* step5. 查看日志指定的路径下是否存在rdb日志文件

```
(base) root@yuanhong section6-3 % ls
my_dump.rdb	redis.conf
```

其他和RDB持久化有关的配置参数:

* `stop-writes-on-bgsave-error`:该参数默认是`yes`,表示当执行`BGSAVE`持久化命令时如果有错误,Redis服务器会终止写入操作;如果取值是`no`,那么即使出现错误也会继续写入
* `rdbcompression`:该参数默认是`yes`,表示在持久化时会压缩文件
* `rdbchecksum`:该参数默认是`yes`,表示在用RDB快照文件进行数据恢复时开启对快照文件的校验.如果设置为`no`,就无法确保快照文件的正确性

在实际项目里,上述参数一般会使用默认值

## 6.3.2 用快照文件恢复数据

和之前的思路一样,起一个容器来验证数据是否恢复:

* step1. 启动容器

```
(base) root@yuanhong section6-3 % docker run -itd --name redis-server -v /StudyRedisBaseOnDocker/conf/chpater6/section6-3/redis.conf:/redisConfig/redis.conf:rw -v /StudyRedisBaseOnDocker/conf/chpater6/section6-3/my_dump.rdb:/StudyRedisBaseOnDocker/conf/chpater6/section6-3/my_dump.rdb:rw redis:latest redis-server /redisConfig/redis.conf
9e2f67eb841dde2ce31b58edb32468b88ebe58da7232f9d67ce1a89c18bbda76
```

* step2. 检查容器状态

```
(base) root@yuanhong section6-3 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS        PORTS      NAMES
9e2f67eb841d   redis:latest   "docker-entrypoint.s…"   2 seconds ago   Up 1 second   6379/tcp   redis-server
```

* step3. 进入容器确认数据

```
(base) root@yuanhong section6-3 % docker exec -it redis-server /bin/bash
root@9e2f67eb841d:/data# redis-cli
127.0.0.1:6379> GET empID
"001"
127.0.0.1:6379> GET empName
"Mike1"
```

## 6.3.3 `SAVE`和`BGSAVE`命令

* `SAVE`:执行该命令后,Redis服务器会把当前内存里的数据写入快照文件,在写入的过程中会暂停执行其他命令,直到写完快照文件之后,才会执行其他命令.若执行成功,则返回OK.在实际项目里,**如果当前Redis内存数据很多,那么一旦执行`SAVE`命令,服务器就会长时间暂停执行命令,造成大量连接阻塞,从而导致线上问题,所以一般在执行`SAVE`命令时需要非常谨慎**
* `BGSAVE`:执行该命令后,Redis服务器会创建一个新的进程,在该进程里把内存数据写入快照文件里,在写的过程中Redis服务器能继续执行其他来自客户端的命令.该命令的返回值为`Background saving started`,表示已经启动后台写操作进程了
* `LASTSAVE`:查看`BGSAVE`的操作结果.该命令返回的是一个时间戳,表示最近一次把内存数据存入快照文件的时间.如果该时间和`BGSAVE`命令的运行时间能对应上,则能说明`BGSAVE`命令成功执行

例:

客户端执行`SAVE`:

```
127.0.0.1:6379> SAVE
OK
```

此时服务端日志为:

```
15341:M 15 Aug 2023 16:31:12.133 * DB saved on disk
```

客户端执行`BGSAVE`:

```
127.0.0.1:6379> BGSAVE
Background saving started
```

此时服务端日志为:

```
15341:M 15 Aug 2023 16:31:58.071 * Background saving terminated with success
```

查看上一次成功创建快照的时间戳:

```
127.0.0.1:6379> LASTSAVE
(integer) 1692088318
```


# 6.4 如何选用持久化方式

## 6.4.1 对比两种持久化方式

AOF可以设置一秒写一次持久化文件,所以相对RDB而言,这种方式能更好地记录内存数据,从而能更好地达到持久化的效果,且AOF是以"在文件末尾追加"的方式写入数据,所以性能较好.不过**AOF持久化的文件一般会大于RDB快照,所以用AOF恢复数据时速度会比RDB要慢**

在某些个别场景里,Redis服务器在重启时无法加载AOF持久化文件,从而导致无法恢复数据,这种情况虽然出现的概率很小,但是一旦出现,或许就是灾难性的

***

某些场景是指:

1. **AOF文件损坏**:
   * 如果Redis或宿主机突然崩溃,如电源故障,这可能会导致AOF文件的某些部分不完整或损坏
   * 如果磁盘已满或发生其他文件系统错误,可能会导致AOF文件写入不完整
2. **AOF格式版本不兼容**:
   * 如果你尝试使用一个较新版本的Redis加载一个由较旧版本的Redis创建的AOF文件,可能会遇到兼容性问题
3. **文件系统权限问题**:
   * 如果Redis进程没有适当的权限来读取AOF文件或其所在的目录,它将无法加载该文件
4. **配置问题**:
   * 如果Redis的配置文件中的`appendonly`选项被设置为`no`,那么Redis将不会加载AOF文件,即使它存在
   * `appendfilename`或`dir`配置可能指向了错误的AOF文件或目录
5. **AOF文件过大**:
   * 如果AOF文件非常大,Redis在启动时可能需要较长时间来加载它.虽然这并不是一个真正的"无法加载"的情况,但在实际操作中可能会被误解为加载失败
6. **硬件问题**:
   * 磁盘故障、损坏的RAM或其他硬件问题可能会影响AOF文件的完整性和可读性
7. **外部因素**:
   * 如果有外部进程(例如备份或监控工具)锁定了AOF文件,这可能会干扰Redis读取文件
8. **AOF文件内部的命令错误**:
   * 在某些情况下,AOF文件中的命令可能会引起Redis启动时的错误.例如,一个命令可能引用了一个不存在的键

为了更好地理解和解决加载AOF文件时的问题,可以查看Redis的日志文件.这通常会提供有关为什么Redis无法加载AOF文件的详细信息和错误消息.如果出现问题,你可以使用`redis-check-aof`工具来检查和修复AOF文件.

***

相对而言,RDB的快照是二进制文件,所以一般比AOF要小,所以在恢复数据时占优势,而且通过`BGSAVE`等方式生成快照时,Redis服务器会新创建一个子进程,所以不会影响Redis服务器继续执行命令.不过RDB持久化的缺陷之前也已经提到,即无法即时恢复数据

综合考虑到这两种方式的优缺点,在实际项目里可以同时用到这两种方式,当出现数据误删的情况时,可以用AOF持久化文件来恢复数据,在一般情况下,可以用RDB快照来恢复数据.一旦出现因AOF持久化文件损坏而无法恢复数据的情况,就可以用RDB的方式来恢复数据,最大限度地提升Redis内存数据的安全性

## 6.4.2 综合使用两种持久化方式

* step1. 编写配置文件

```
(base) root@yuanhong section6-4 % cat redis.conf
# 快照或AOF文件存储路径
dir /StudyRedisBaseOnDocker/conf/chpater6/section6-4

# 5秒内有1个或1个以上的键被修改时生成快照
save 5 1

# 300秒内有100个或100个以上的键被修改时生成快照
save 300 100

# 60秒内有1000个或1000个以上的键被修改时生成快照
save 60 1000

# 快照文件名
dbfilename my_dump.rdb

# 启用AOF持久化机制
appendonly yes

# 持久化频率为每秒持久化1次
appendfsync everysec

# 持久化文件名称
appendfilename my_appendonly.aof
```

* step2. 启动redis-server

```
(base) root@yuanhong section6-4 % redis-server /StudyRedisBaseOnDocker/conf/chpater6/section6-4/redis.conf
```

* step3. 写入一些键值对

```
(base) root@yuanhong ~ % redis-cli
127.0.0.1:6379> SET name Peter
OK
127.0.0.1:6379> SET age 18
OK
```

* step4. 查看配置文件指定的路径下的文件情况

```
(base) root@yuanhong section6-4 % ls
my_appendonly.aof	my_dump.rdb		redis.conf
```

可以看到,AOF文件和RDB文件同时存在

## 6.4.3 查看持久化状态的命令

* `INFO persistence`:查看持久化相关状态

```
127.0.0.1:6379> INFO persistence
# Persistence
loading:0
current_cow_size:0
current_cow_size_age:0
current_fork_perc:0.00
current_save_keys_processed:0
current_save_keys_total:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1692090209
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:0
aof_enabled:1
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_last_cow_size:0
module_fork_in_progress:0
module_fork_last_cow_size:0
aof_current_size:87
aof_base_size:0
aof_pending_rewrite:0
aof_buffer_length:0
aof_rewrite_buffer_length:0
aof_pending_bio_fsync:0
aof_delayed_fsync:0
```

其中:

* `rdb_last_save_time`:上次RDB持久化快照的生成时间
* `aof_enabled`:是否启用AOF持久化机制
* `aof_last_write_status`:上次AOF同步数据的操作是否成功
* `aof_current_rewrite_time_sec`:表示当前AOF重写操作(如果有的话)已经进行的秒数.如果没有AOF重写操作正在进行,该值为`-1`
* `aof_last_rewrite_time_sec`:上一次AOF重写操作所花费的总时间.单位:秒


# 第7章 搭建Redis集群


# 7.1 搭建基于主从复制模式的集群

在主从复制模式的集群里,主节点一般是一个,从节点一般是两个或多个,写入主节点的数据会被复制到从节点上,这样一旦主节点出现故障,应用系统就能切换到从节点去读写数据,提升系统的可用性.再采用主从复制模式里默认的读写分离机制,就能提升系统的缓存读写性能.对性能和实时性不高的系统而言,主从复制模式足以满足一般的性能和安全性方面的需求.

## 7.1.1 主从复制模式概述

在实际应用中,如果有相应的设置,在向一台Redis服务器里写数据后,这个数据可以复制到另外一台(或多台)Redis服务器,这里数据源服务器叫主服务器(Master Server),复制数据目的地所在的服务器叫从服务器(Slave Server)

这种主从复制模式能带来2个好处:

1. 可以把写操作集中到主服务器上,把读操作集中到从服务器上,以提升读写性能
2. 由于出现了数据备份,因此能提升数据的安全性,比如当主Redis服务器失效后,能很快切换到从服务器上读数据

![Redis主从结构](/files/jzJPaDZDja3z0hiV14fI)

主从复制模式的要点:

1. 一个主服务器可以带一个或多个从服务器,从服务器可以再带从服务器,但在复制数据时只能把主服务器的数据复制到从服务器上,反之不能
2. 一台从服务器只能跟随一台主服务器,不能出现一从多主的模式
3. 在Redis2.8以后的版本里,采用异步的复制模式,即进行主从复制时不会影响主服务器上的读写数据操作

## 7.1.2 用命令搭建主从集群

本例中使用Docker搭建1主2从模式的集群.配置主从关系时,需要在从节点上使用`SLAVEOF`命令

* step1. 创建主节点容器

```
(base) root@yuanhong ~ % docker run -itd --name redis-master -p 6379:6379 redis:latest
402d3999682b4c6a173d85a79eb5dfa4d491992df99730e03559f8c9f713a4db
```

```
(base) root@yuanhong ~ % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                    NAMES
402d3999682b   redis:latest   "docker-entrypoint.s…"   4 seconds ago   Up 2 seconds   0.0.0.0:6379->6379/tcp   redis-master
```

* step2. 创建从节点1容器

```
(base) root@yuanhong ~ % docker run -itd --name redis-slave1 -p 6380:6380 redis:latest
8ab67ec497a9ab1a587e17e1509f99e96b58e381d39acfa82f3ba421fd7d761b
```

```
(base) root@yuanhong ~ % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                              NAMES
8ab67ec497a9   redis:latest   "docker-entrypoint.s…"   33 seconds ago   Up 32 seconds   6379/tcp, 0.0.0.0:6380->6380/tcp   redis-slave1
402d3999682b   redis:latest   "docker-entrypoint.s…"   5 minutes ago    Up 5 minutes    0.0.0.0:6379->6379/tcp             redis-master
```

* step3. 检查主节点容器的IP地址

```
(base) root@yuanhong ~ % docker inspect redis-master|grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",
```

可以看到,主节点容器的IP地址为`172.17.0.2`

* step4. 进入主节点容器,检查当前的主从模式状态

```
(base) root@yuanhong ~ % docker exec -it redis-master /bin/bash
root@402d3999682b:/data# redis-cli
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:86fe8508c48c7f521d6b057d36b6d8ef757f795b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
```

其中:

* `role:master`:表示该节点的角色为主服务器
* `connected_slaves:0`:表示当前该主服务器没有携带从服务器
* step5. 进入从节点容器,检查当前主从模式状态

```
(base) root@yuanhong ~ % docker exec -it redis-slave1 /bin/bash
root@8ab67ec497a9:/data# redis-cli
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:da60f945afea248b514978a4ad6d0fac764de983
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
```

* step6. 设置从节点容器为从服务器

在redis-slave1容器中执行如下命令:

```
127.0.0.1:6379> SLAVEOF 172.17.0.2 6379
OK
```

检查从节点容器的主从状态:

```
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_read_repl_offset:56
slave_repl_offset:56
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:f23930d90daac10a59515702826293135f94c133
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56
```

其中:

* `role:slave`:表示该节点的角色为从服务器
* `master_host:172.17.0.2`:主节点IP地址
* `master_port:6379`:主节点端口
* step7. 在主节点中查看主从状态

```
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.3,port=6379,state=online,offset=714,lag=1
master_failover_state:no-failover
master_replid:f23930d90daac10a59515702826293135f94c133
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:728
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:728
```

* `connected_slaves:1`:可以看到,此时已经有1个从节点了
* step8. 创建从节点2容器

```
(base) root@yuanhong ~ % docker run -itd --name redis-slave2 -p 6381:6381 redis:latest
f4a1f14d506c49a9df1663dd45dfbbec9685c52ce683ec54c98bd3124c3789db
```

```
(base) root@yuanhong ~ % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                              NAMES
f4a1f14d506c   redis:latest   "docker-entrypoint.s…"   11 seconds ago   Up 10 seconds   6379/tcp, 0.0.0.0:6381->6381/tcp   redis-slave2
8ab67ec497a9   redis:latest   "docker-entrypoint.s…"   25 minutes ago   Up 25 minutes   6379/tcp, 0.0.0.0:6380->6380/tcp   redis-slave1
402d3999682b   redis:latest   "docker-entrypoint.s…"   30 minutes ago   Up 30 minutes   0.0.0.0:6379->6379/tcp             redis-master
```

* step9. 进入从节点2容器,设置该容器为从服务器

```
(base) root@yuanhong ~ % docker exec -it redis-slave2 /bin/bash
root@f4a1f14d506c:/data# redis-cli
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:93c3dae3b5381a7a4b710163e6f33ddef085cd00
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
```

可以看到,此时该容器为主节点

```
127.0.0.1:6379> SLAVEOF 172.17.0.2 6379
OK
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_read_repl_offset:1344
slave_repl_offset:1344
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:f23930d90daac10a59515702826293135f94c133
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1344
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1345
repl_backlog_histlen:0
```

* step10. 在主节点容器中查看主从情况

```
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.17.0.3,port=6379,state=online,offset=1456,lag=0
slave1:ip=172.17.0.4,port=6379,state=online,offset=1456,lag=0
master_failover_state:no-failover
master_replid:f23930d90daac10a59515702826293135f94c133
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1456
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1456
```

可以看到,此时有2个从节点

## 7.1.3 通过配置搭建主从集群

* step1. 编写主节点的配置文件

```
(base) root@yuanhong section7-1 % cat master.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user master_user on >master_password  ~* &* +@all

# 指定端口
port 6379
```

* step2. 根据配置文件启动主节点容器

启动容器:

```
(base) root@yuanhong section7-1 % docker run -itd --name redis-master -v /StudyRedisBaseOnDocker/conf/chapter7/section7-1/master.conf:/redisConf/master.conf:rw -p 6379:6379 redis:latest redis-server /redisConf/master.conf
689437cb98ce2bd563413be89529872b74421b63f73b9dcd25f7a995b084c0ea
```

检查容器状态:

```
(base) root@yuanhong section7-1 % docker ps   
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                    NAMES
689437cb98ce   redis:latest   "docker-entrypoint.s…"   12 seconds ago   Up 11 seconds   0.0.0.0:6379->6379/tcp   redis-master
```

进入容器检查用户名密码是否配置正确:

```
(base) root@yuanhong section7-1 % docker exec -it redis-master /bin/bash
root@689437cb98ce:/data# redis-cli
127.0.0.1:6379> AUTH master_user master_password
OK
127.0.0.1:6379> ACL WHOAMI
"master_user"
```

* step3. 检查主节点容器IP地址

```
(base) root@yuanhong section7-1 % docker inspect redis-master|grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",
```

* step4. 编写从节点1的配置文件

```
(base) root@yuanhong section7-1 % cat slave_1.conf
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user slave_1_user on >slave_1_password  ~* &* +@all

# 指定端口
port 6380

# 指定主节点IP和端口
slaveof 172.17.0.2 6379

# 指定主节点用户名
masteruser master_user

# 指定主节点密码
masterauth master_password
```

* step5. 根据配置文件启动从节点1容器

启动容器:

```
(base) root@yuanhong section7-1 % docker run -itd --name redis-slave1 -v /StudyRedisBaseOnDocker/conf/chapter7/section7-1/slave_1.conf:/redisConf/slave_1.conf:rw -p 6380:6380 redis:latest redis-server /redisConf/slave_1.conf
2e52199b473cc1a3062975fa1371d2230ebb9991154d5428577ce9f01be9cd1f
```

检查容器状态:

```
(base) root@yuanhong section7-1 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                              NAMES
2e52199b473c   redis:latest   "docker-entrypoint.s…"   1 second ago     Up 1 second     6379/tcp, 0.0.0.0:6380->6380/tcp   redis-slave1
689437cb98ce   redis:latest   "docker-entrypoint.s…"   13 minutes ago   Up 13 minutes   0.0.0.0:6379->6379/tcp             redis-master
```

进入容器检查用户名密码是否配置正确:

```
(base) root@yuanhong section7-1 % docker exec -it redis-slave1 /bin/bash
root@2e52199b473c:/data# redis-cli -h 127.0.0.1 -p 6380
127.0.0.1:6380> AUTH slave_1_user slave_1_password
OK
127.0.0.1:6380> ACL WHOAMI
"slave_1_user"
127.0.0.1:6380> 
```

* step6. 在从节点1容器中确认主从状态

```
127.0.0.1:6380> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_read_repl_offset:322
slave_repl_offset:322
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:523bf7a915b56cfe75b1b1032b312e484ccbc0a6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:322
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:322
```

* step7. 在主节点容器中确认主从状态

```
root@689437cb98ce:/data# redis-cli
127.0.0.1:6379> AUTH master_user master_password
OK
127.0.0.1:6379> ACL WHOAMI
"master_user"
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.3,port=6380,state=online,offset=392,lag=0
master_failover_state:no-failover
master_replid:523bf7a915b56cfe75b1b1032b312e484ccbc0a6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:392
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:392
```

* step8. 编写从节点2的配置文件

```
(base) root@yuanhong section7-1 % cat slave_2.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user slave_2_user on >slave_2_password  ~* &* +@all

# 指定端口
port 6381

# 指定主节点IP和端口
slaveof 172.17.0.2 6379

# 指定主节点用户名
masteruser master_user

# 指定主节点密码
masterauth master_password
```

* step9. 根据配置文件启动从节点2容器

启动容器:

```
(base) root@yuanhong section7-1 % docker run -itd --name redis-slave2 -v /StudyRedisBaseOnDocker/conf/chapter7/section7-1/slave_2.conf:/redisConf/slave2.conf:rw -p 6381:6381 redis:latest redis-server /redisConf/slave2.conf
bffc87be1f2fb321aadb07b520d452d2c980fa4d94d383bda19f187ff46df578
```

检查容器状态:

```
(base) root@yuanhong section7-1 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                              NAMES
bffc87be1f2f   redis:latest   "docker-entrypoint.s…"   10 seconds ago   Up 10 seconds   6379/tcp, 0.0.0.0:6381->6381/tcp   redis-slave2
2e52199b473c   redis:latest   "docker-entrypoint.s…"   12 minutes ago   Up 12 minutes   6379/tcp, 0.0.0.0:6380->6380/tcp   redis-slave1
689437cb98ce   redis:latest   "docker-entrypoint.s…"   26 minutes ago   Up 26 minutes   0.0.0.0:6379->6379/tcp             redis-master
```

进入容器检查用户名密码是否配置正确:

```
(base) root@yuanhong section7-1 % docker exec -it redis-slave2 /bin/bash
root@bffc87be1f2f:/data# redis-cli -h 127.0.0.1 -p 6381
127.0.0.1:6381> AUTH slave_2_user slave_2_password
OK
127.0.0.1:6381> ACL WHOAMI
"slave_2_user"
```

* step10. 在从节点2容器中确认主从状态

```
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:1232
slave_repl_offset:1232
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:523bf7a915b56cfe75b1b1032b312e484ccbc0a6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1232
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1051
repl_backlog_histlen:182
```

* step11. 在主节点容器中确认主从状态

```
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.17.0.3,port=6380,state=online,offset=1260,lag=0
slave1:ip=172.17.0.4,port=6381,state=online,offset=1260,lag=0
master_failover_state:no-failover
master_replid:523bf7a915b56cfe75b1b1032b312e484ccbc0a6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1260
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1260
```

可以看到,此时主节点上可以看到2个从节点的信息

* step12. 测试

主节点上写入:

```
127.0.0.1:6379> SET age 18
OK
```

分别在2个从节点上读取:

```
127.0.0.1:6380> GET age
"18"
```

```
127.0.0.1:6381> GET age
"18"
```

## 7.1.4 配置读写分离效果

在2个从节点上查看配置主从状态:

```
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_read_repl_offset:2419
slave_repl_offset:2419
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:523bf7a915b56cfe75b1b1032b312e484ccbc0a6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2419
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1051
repl_backlog_histlen:1369
```

```
127.0.0.1:6380> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:10
master_sync_in_progress:0
slave_read_repl_offset:2587
slave_repl_offset:2587
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:523bf7a915b56cfe75b1b1032b312e484ccbc0a6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2587
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2587
```

其中:

* `slave_read_only:1`:表示从服务器为只读

修改slave2节点为可读可写:

* step1. 停止redis-slave2容器

```
(base) root@yuanhong section7-1 % docker stop redis-slave2
redis-slave2
```

* step2. 修改redis-slave2的配置文件

```
(base) root@yuanhong section7-1 % cat slave_2.conf                      
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user slave_2_user on >slave_2_password  ~* &* +@all

# 指定端口
port 6381

# 指定主节点IP和端口
slaveof 172.17.0.2 6379

# 指定主节点用户名
masteruser master_user

# 指定主节点密码
masterauth master_password

# 允许从节点执行写入操作
slave-read-only no
```

* step3. 启动redis-slave2容器

```
(base) root@yuanhong section7-1 % docker start redis-slave2             
redis-slave2
```

* step4. 进入redis-slave2容器并尝试写入

```
(base) root@yuanhong section7-1 % docker exec -it redis-slave2 /bin/bash
root@bffc87be1f2f:/data# redis-cli -h 127.0.0.1 -p 6381
127.0.0.1:6381> AUTH slave_2_user slave_2_password
OK
127.0.0.1:6381> GET age
"18"
127.0.0.1:6381> SET name Peter
OK
```

可以看到,此时已经可以在从节点2上写入了

在主节点和从节点1上读取:

```
127.0.0.1:6379> GET name
(nil)
```

```
127.0.0.1:6380> GET name
(nil)
```

因此,需要注意的是:

在Redis主从集群模式下,从节点(Slave)默认是只读的.当你将从节点设置为可写入模式(`slave-read-only no`),这意味着客户端可以直接写入到这个从节点.然而，这种配置并不是常规的使用模式,并且有其特定的行为和限制.

以下是关于在从节点上写入时发生的事情的解释:

1. **写入不会同步到主节点或其他从节点**:当你直接在从节点上进行写入操作时,这些写入不会被传播到主节点或其他从节点.这是因为在Redis的复制模型中,只有主节点才会将其写入操作传播到从节点
2. **数据可能丢失**:由于从节点上的写入不会同步到其他节点,因此如果这个从节点出现故障或重新启动,这些写入可能会丢失.因为当从节点重新连接到主节点时,它会从主节点获取最新的数据快照,从而丢弃任何在从节点上直接进行的写入
3. **可能导致数据不一致**:由于从节点上的写入不会传播,这意味着集群中的不同节点可能会有不同的数据,从而导致数据不一致

基于上述原因,允许直接在从节点上进行写入(将`slave-read-only`设置为`no`)通常不是推荐的做法.这种配置可能会导致数据丢失、数据不一致,并且与Redis的传统复制模型不符

如果您需要一个可写的分布式数据存储解决方案,您可能需要考虑使用Redis Cluster,它支持多个主节点,并可以在节点之间自动分片数据

## 7.1.5 用心跳机制提高主从复制的可靠性

在Redis主从复制模式里,如果主从服务器之间有数据同步的情况,那么从服务器会默认以一秒一次的频率向主服务器发送`REPLCONF ACK`命令,以此来确保二者间连接通畅.这种用定时交互命令来确保连接的机制叫做"心跳"机制.

在主节点上执行`INFO replication`命令,可以看到从节点的心跳情况:

```
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.17.0.3,port=6380,state=online,offset=7137,lag=1
slave1:ip=172.17.0.4,port=6381,state=online,offset=7137,lag=1
master_failover_state:no-failover
master_replid:523bf7a915b56cfe75b1b1032b312e484ccbc0a6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:7137
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:7137
```

其中:

```
slave0:ip=172.17.0.3,port=6380,state=online,offset=7137,lag=1
slave1:ip=172.17.0.4,port=6381,state=online,offset=7137,lag=1
```

此处的`lag`表示的就是从节点与主节点之间的延迟(单位:秒)

在Redis中,`min-slaves-to-write`参数用于指定主节点需要多少个从节点(slaves)处于已同步状态(即与主节点的数据差异小于`min-slaves-max-lag`所指定的秒数),才允许写入操作

参数的详细含义如下:

* `min-slaves-to-write`:这是一个整数值,指定了主节点要求处于已同步状态的从节点数量,才允许执行写入操作.如果已同步的从节点数量少于此值,主节点将拒绝所有的写入命令并返回一个错误
* `min-slaves-max-lag`:与`min-slaves-to-write`配合使用,它定义了一个从节点被认为是已同步的最大延迟值(单位:秒).如果从节点的延迟大于此值,它将被认为是未同步的

这两个参数的目的是在某些场景下增加数据的持久性和可用性.例如,如果你有一个Redis设置,其中数据的可用性和不丢失写入操作是关键要求,那么你可能希望确保至少有一定数量的从节点已经接收和确认了最近的写入,才继续更多的写入

但是,请注意,这些设置可能会增加写入操作的延迟,尤其是在网络不稳定或从节点经常出现延迟的情况下.在配置这些参数之前,你应该仔细考虑其影响,并根据你的具体需求进行测试

在主节点的配置文件中增加配置心跳的参数:

```
(base) root@yuanhong section7-1 % cat master.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user master_user on >master_password  ~* &* +@all

# 指定端口
port 6379

# 至少有2个从节点处于已同步状态时才允许执行写入操作
min-slaves-to-write 2

# 从节点延迟超过15秒即判定为未同步
min-slaves-max-lag 15
```

重新启动主节点容器:

```
(base) root@yuanhong section7-1 % docker start redis-master
redis-master
```

## 7.1.6 用偏移量检查数据是否一致

在主节点中查看偏移量:

```
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
min_slaves_good_slaves:2
slave0:ip=172.17.0.3,port=6381,state=online,offset=644,lag=0
slave1:ip=172.17.0.4,port=6380,state=online,offset=644,lag=0
master_failover_state:no-failover
master_replid:46d5db13c64599bf1bae2d4c276229e00cc78a46
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:644
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:644
```

其中:

* `master_repl_offset`:表示主节点向从节点发送的字节数

在从节点中查看偏移量:

```
127.0.0.1:6380> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:644
slave_repl_offset:644
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:46d5db13c64599bf1bae2d4c276229e00cc78a46
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:644
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:644
```

```
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:9
master_sync_in_progress:0
slave_read_repl_offset:644
slave_repl_offset:644
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:46d5db13c64599bf1bae2d4c276229e00cc78a46
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:644
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:644
```

其中:

* `slave_repl_offset`:表示从节点从主节点中接收到的字节数.若和主节点的`master_repl_offset`一致,说明主从服务器之间的数据是同步的

在主节点上写入1条数据:

```
127.0.0.1:6379> SET val 1
OK
```

再观察主节点的同步信息:

```
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
min_slaves_good_slaves:2
slave0:ip=172.17.0.3,port=6381,state=online,offset=906,lag=0
slave1:ip=172.17.0.4,port=6380,state=online,offset=906,lag=0
master_failover_state:no-failover
master_replid:46d5db13c64599bf1bae2d4c276229e00cc78a46
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:906
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:906
```

观察从节点的同步信息:

```
127.0.0.1:6380> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:906
slave_repl_offset:906
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:46d5db13c64599bf1bae2d4c276229e00cc78a46
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:906
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:906
```

```
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_read_repl_offset:906
slave_repl_offset:906
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:46d5db13c64599bf1bae2d4c276229e00cc78a46
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:906
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:906
```

此时从节点从主节点处读取到的字节数等于主节点向从节点发送的字节数.说明刚刚的写操作同步成功了.若同步出现问题,则可以通过`master_repl_offset`和`slave_repl_offset`参数值进行排查


# 7.2 搭建哨兵模式的集群

在上文提到的主从复制模式的集群里,一方面可以提升数据的安全性,比如主服务器失效后,可以启动从服务器上的备份数据,另一方面也可以通过读写分离来提升性能.但是当主服务器发生故障后,需要手动进行数据恢复动作.比如让应用程序连到从服务器上,同时需要重新设置主从关系.

也就是说,基于主从复制模式的集群在发生故障时可能会出现数据丢失等情况,对此可以在主从模式的基础上再引入"哨兵(Sentinel)"机制,一方面用哨兵进程监控主从服务器是否可用,另一方面当主服务器故障发生时通过哨兵机制可以实现"故障自动恢复"的效果.

## 7.2.1 哨兵模式概述

一般来说,哨兵机制会和主从复制模式整合使用,**在基于哨兵的模式里会在一台或多台服务器上引入哨兵进程,这些节点也叫哨兵节点**

哨兵节点一般不存储数据,它的作用是监控主从模式里的主服务器节点.当哨兵节点监控的主服务器发生故障时,哨兵节点会主导"故障自动恢复"的流程,具体来讲就是会在该主服务器下属的从服务器中选出一个新的主服务器,并完成相应的数据和配置更改动作

也就是说,如果采用这种模式,可以让故障自动修复,从而提升系统的可用性.在项目中,一般会配置多个主从模式集群,所以会引入多个哨兵节点.如下图示:

![基于哨兵模式的集群](/files/dzwifNlcahPK2mic7L32)

***

多个哨兵相互监控的目的:

当一个Sentinel节点检测到Redis主服务器可能出现问题时,它不会立即采取行动.相反,它会询问其他Sentinel节点以验证其观察结果.只有当足够数量的Sentinel节点同意主服务器出现问题时(这是通过`sentinel monitor`配置中的仲裁值定义的),故障转移过程才会开始

此外,Sentinel节点之间的通信还有其他几个目的:

1. **选举新的领导者**:当主服务器被认为是下线的并且需要进行故障转移时,Sentinel节点之间会进行选举,选择一个Sentinel作为领导者来执行故障转移
2. **配置同步**:当一个Sentinel节点检测到主服务器的配置更改(例如IP地址或端口更改)时,它会通知其他Sentinel节点
3. **状态信息共享**:Sentinel节点之间会定期交换关于它们监视的Redis服务器的状态信息

为了实现Sentinel之间的这种通信,你需要确保Sentinel配置中的`sentinel announce-ip`和`sentinel announce-port`已正确设置,并且所有Sentinel节点都可以互相访问.这通常在启动Sentinel时会自动进行,但在某些网络配置或Docker环境中可能需要手动设置

***

## 7.2.2 搭建哨兵模式集群

* step1. 按7.1小节的步骤启动一个1主2从的redis集群
* step2. 编写哨兵节点1的配置文件

```
(base) root@192 section7-2 % cat sentinel_1.conf 
# 指定哨兵进程监听的端口
port 16379

# 指定sentinel监视的redis服务器信息和判定不可用的条件
# 其中:
# sentinel monitor: 这是一个命令 告诉Sentinel监视一个Redis服务器
# redis-master-node: 被监视的Redis服务器的名字 任意命名即可
# 172.17.0.2: 被监视的Redis服务器的IP地址
# 6379: 被监视的Redis服务器的端口号
# 2: 仲裁值.即Sentinel判定Redis服务器是不可用的前提条件.
# 表示当至少有2个Sentinel报告说Redis服务器是不可用的时,才认为该服务器是不可用的
# 这可以防止一个Sentinel因为网络问题或其他原因错误地认为主服务器是不可用的
sentinel monitor redis-master-node 172.17.0.2 6379 2

# 指定被监视的redis服务器的用户名
sentinel auth-user redis-master-node master_user

# 指定被监视的redis服务器的密码
sentinel auth-pass redis-master-node master_password

# 指定哨兵节点的日志文件位置
dir /

# 指定哨兵节点的日志文件名称
logfile sentinel_1.log
```

* step3. 启动哨兵节点1容器

创建容器:

```
(base) root@192 section7-2 % docker run -itd --name redis-sentinel1 -v /StudyRedisBaseOnDocker/conf/chapter7/section7-2/sentinel_1.conf:/redisConf/sentinel_1.conf:rw -p 16379:16379 redis:latest redis-sentinel /redisConf/sentinel_1.conf
e4ff677f7c7ee7f256a65ec0e1a092b9bc229963f124f5c7d988de3276d6b190
```

查看容器状态:

```
(base) root@192 section7-2 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS          PORTS                                NAMES
e4ff677f7c7e   redis:latest   "docker-entrypoint.s…"   3 seconds ago   Up 2 seconds    6379/tcp, 0.0.0.0:16379->16379/tcp   redis-sentinel1
fc6d5912a8dc   redis:latest   "docker-entrypoint.s…"   8 hours ago     Up 23 minutes   6379/tcp, 0.0.0.0:6381->6381/tcp     redis-slave2
2e52199b473c   redis:latest   "docker-entrypoint.s…"   8 hours ago     Up 23 minutes   6379/tcp, 0.0.0.0:6380->6380/tcp     redis-slave1
689437cb98ce   redis:latest   "docker-entrypoint.s…"   9 hours ago     Up 23 minutes   0.0.0.0:6379->6379/tcp               redis-master
```

* step4. 进入哨兵节点1容器,查看哨兵节点信息

```
root@e4ff677f7c7e:/data# redis-cli -h 127.0.0.1 -p 16379
127.0.0.1:16379> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=ok,address=172.17.0.2:6379,slaves=2,sentinels=1
```

其中:

* `status=ok`:主节点状态为ok
* `slaves=2`:该主节点有2个从节点
* `sentinels=1`:该主节点有1个哨兵
* step5. 编写哨兵节点2的配置文件

```
(base) root@192 section7-2 % cat sentinel_2.conf 
# 指定哨兵进程监听的端口
port 16380

# 指定sentinel监视的redis服务器信息和判定不可用的条件
# 其中:
# sentinel monitor: 这是一个命令 告诉Sentinel监视一个Redis服务器
# redis-master-node: 被监视的Redis服务器的名字 任意命名即可
# 172.17.0.2: 被监视的Redis服务器的IP地址
# 6379: 被监视的Redis服务器的端口号
# 2: 仲裁值.即Sentinel判定Redis服务器是不可用的前提条件.
# 表示当至少有2个Sentinel报告说Redis服务器是不可用的时,才认为该服务器是不可用的
# 这可以防止一个Sentinel因为网络问题或其他原因错误地认为主服务器是不可用的
sentinel monitor redis-master-node 172.17.0.2 6379 2

# 指定被监视的redis服务器的用户名
sentinel auth-user redis-master-node master_user

# 指定被监视的redis服务器的密码
sentinel auth-pass redis-master-node master_password

# 指定哨兵节点的日志文件位置
dir /

# 指定哨兵节点的日志文件名称
logfile sentinel_2.log
```

* step6. 启动哨兵节点2容器

启动容器:

```
(base) root@192 section7-2 % docker run -itd --name redis-sentinel2 -v /StudyRedisBaseOnDocker/conf/chapter7/section7-2/sentinel_2.conf:/redisConf/sentinel_2.conf:rw -p 16380:16380 redis:latest redis-sentinel /redisConf/sentinel_2.conf
02bde56f5041a0fdc57235b4ff217b7102a871037d68c791ec8222d10755b960
```

查看容器状态:

```
(base) root@192 section7-2 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                NAMES
02bde56f5041   redis:latest   "docker-entrypoint.s…"   2 seconds ago    Up 2 seconds    6379/tcp, 0.0.0.0:16380->16380/tcp   redis-sentinel2
e4ff677f7c7e   redis:latest   "docker-entrypoint.s…"   36 minutes ago   Up 9 minutes    6379/tcp, 0.0.0.0:16379->16379/tcp   redis-sentinel1
fc6d5912a8dc   redis:latest   "docker-entrypoint.s…"   8 hours ago      Up 59 minutes   6379/tcp, 0.0.0.0:6381->6381/tcp     redis-slave2
2e52199b473c   redis:latest   "docker-entrypoint.s…"   9 hours ago      Up 9 minutes    6379/tcp, 0.0.0.0:6380->6380/tcp     redis-slave1
689437cb98ce   redis:latest   "docker-entrypoint.s…"   9 hours ago      Up 59 minutes   0.0.0.0:6379->6379/tcp               redis-master
```

* step7. 进入哨兵节点2容器,查看哨兵节点信息

```
(base) root@192 section7-2 % docker exec -it redis-sentinel2 /bin/bash
root@02bde56f5041:/data# redis-cli -h 127.0.0.1 -p 16380
127.0.0.1:16380> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=ok,address=172.17.0.2:6379,slaves=2,sentinels=2
```

可以看到,此时有2个哨兵节点在监视该集群了

集群架构如下图示:

![哨兵集群架构图](/files/8FQixjPecvCaEXwD5hPX)

2个哨兵节点同时监控Redis主节点(即:redis-master).Redis主节点和从节点之间依然存在数据同步的复制模式.

## 7.2.3 哨兵节点的常用配置

* `sentinel down-after-milliseconds`:该配置项指定了Sentinel认为Redis服务器(无论是主服务器还是从服务器)已下线之前,它应该在多长时间内无法与该服务器通信.具体来说,这是Sentinel判断Redis实例为"主观下线"的时间阈值."主观下线"是Sentinel内部的术语,意思是**一个Sentinel个体认为一个Redis实例已经下线,但它还没有与其他Sentinel个体达成一致来确认这一点**.当一个Redis实例被标记为"主观下线"后,Sentinel会询问其他Sentinel节点,以确认该实例是否真的下线.只有当超过配置中定义的Sentinel数量(例如`sentinel monitor` 中定义的仲裁值)同意该实例已下线时,它才会被标记为"客观下线".一旦一个主服务器被标记为"客观下线",故障转移过程就会开始
* `sentinel failover-timeout`:定义了故障转移的超时时间.其作用如下:

1. **故障转移超时**:如果一个故障转移操作开始但在这个超时时间内没有完成,那么该操作将被中止.这确保了故障转移不会永远地挂起,特别是在出现网络分区或其他复杂问题的情况下
2. **重新尝试故障转移**:如果因某种原因故障转移失败(例如没有足够的从服务器或网络问题),Sentinel将在这个超时时间后再次尝试故障转移
3. **保护已经发生的故障转移**:如果在故障转移完成后,原主服务器重新上线但是作为从服务器,Sentinel会在`failover-timeout`期间保护新的主服务器不被其他旧的主服务器替代.这样可以防止频繁的角色切换
4. **配置同步**:此超时时间还决定了在首次尝试同步配置和其他状态信息失败后,Sentinel之间多久会再次尝试同步

* step1. 修改哨兵节点1的配置文件

```
(base) root@yuanhong section7-2 % cat sentinel_1.conf 
# 指定哨兵进程监听的端口
port 16379

# 指定sentinel监视的redis服务器信息和判定不可用的条件
# 其中:
# sentinel monitor: 这是一个命令 告诉Sentinel监视一个Redis服务器
# redis-master-node: 被监视的Redis服务器的名字 任意命名即可
# 172.17.0.2: 被监视的Redis服务器的IP地址
# 6379: 被监视的Redis服务器的端口号
# 2: 仲裁值.即Sentinel判定Redis服务器是不可用的前提条件.
# 表示当至少有2个Sentinel报告说Redis服务器是不可用的时,才认为该服务器是不可用的
# 这可以防止一个Sentinel因为网络问题或其他原因错误地认为主服务器是不可用的
sentinel monitor redis-master-node 172.17.0.2 6379 2

# 指定被监视的redis服务器的用户名
sentinel auth-user redis-master-node master_user

# 指定被监视的redis服务器的密码
sentinel auth-pass redis-master-node master_password

# 指定哨兵节点的日志文件位置
dir /

# 指定哨兵节点的日志文件名称
logfile sentinel_1.log

# 指定判定redis-master-node节点主观下线的时间阈值为60000毫秒 即60秒
sentinel down-after-milliseconds redis-master-node 60000

# 指定了故障转移的超时时间为180000毫秒 即180秒
sentinel failover-timeout redis-master-node 180000
```

* step2. 重启哨兵节点1容器

重启容器:

```
(base) root@yuanhong section7-2 % docker stop redis-sentinel1
redis-sentinel1
(base) root@yuanhong section7-2 % docker start redis-sentinel1
redis-sentinel1
```

查看容器状态:

```
(base) root@yuanhong section7-2 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED        STATUS          PORTS                                NAMES
02bde56f5041   redis:latest   "docker-entrypoint.s…"   11 hours ago   Up 44 minutes   6379/tcp, 0.0.0.0:16380->16380/tcp   redis-sentinel2
e4ff677f7c7e   redis:latest   "docker-entrypoint.s…"   12 hours ago   Up 2 seconds    6379/tcp, 0.0.0.0:16379->16379/tcp   redis-sentinel1
fc6d5912a8dc   redis:latest   "docker-entrypoint.s…"   19 hours ago   Up 44 minutes   6379/tcp, 0.0.0.0:6381->6381/tcp     redis-slave2
2e52199b473c   redis:latest   "docker-entrypoint.s…"   20 hours ago   Up 44 minutes   6379/tcp, 0.0.0.0:6380->6380/tcp     redis-slave1
689437cb98ce   redis:latest   "docker-entrypoint.s…"   20 hours ago   Up 44 minutes   0.0.0.0:6379->6379/tcp               redis-master
```

* step3. 修改哨兵节点2的配置文件

```
(base) root@yuanhong section7-2 % cat sentinel_2.conf 
# 指定哨兵进程监听的端口
port 16380

# 指定sentinel监视的redis服务器信息和判定不可用的条件
# 其中:
# sentinel monitor: 这是一个命令 告诉Sentinel监视一个Redis服务器
# redis-master-node: 被监视的Redis服务器的名字 任意命名即可
# 172.17.0.2: 被监视的Redis服务器的IP地址
# 6379: 被监视的Redis服务器的端口号
# 2: 仲裁值.即Sentinel判定Redis服务器是不可用的前提条件.
# 表示当至少有2个Sentinel报告说Redis服务器是不可用的时,才认为该服务器是不可用的
# 这可以防止一个Sentinel因为网络问题或其他原因错误地认为主服务器是不可用的
sentinel monitor redis-master-node 172.17.0.2 6379 2

# 指定被监视的redis服务器的用户名
sentinel auth-user redis-master-node master_user

# 指定被监视的redis服务器的密码
sentinel auth-pass redis-master-node master_password

# 指定哨兵节点的日志文件位置
dir /

# 指定哨兵节点的日志文件名称
logfile sentinel_2.log

# 指定判定redis-master-node节点主观下线的时间阈值为60000毫秒 即60秒
sentinel down-after-milliseconds redis-master-node 60000

# 指定了故障转移的超时时间为180000毫秒 即180秒
sentinel failover-timeout redis-master-node 180000
```

* step4. 重启哨兵节点2容器

重启容器:

```
(base) root@yuanhong section7-2 % docker stop redis-sentinel2 
redis-sentinel2
(base) root@yuanhong section7-2 % docker start redis-sentinel2
redis-sentinel2
```

查看容器状态:

```
(base) root@yuanhong section7-2 % docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED        STATUS          PORTS                                NAMES
02bde56f5041   redis:latest   "docker-entrypoint.s…"   11 hours ago   Up 2 seconds    6379/tcp, 0.0.0.0:16380->16380/tcp   redis-sentinel2
e4ff677f7c7e   redis:latest   "docker-entrypoint.s…"   12 hours ago   Up 2 minutes    6379/tcp, 0.0.0.0:16379->16379/tcp   redis-sentinel1
fc6d5912a8dc   redis:latest   "docker-entrypoint.s…"   19 hours ago   Up 46 minutes   6379/tcp, 0.0.0.0:6381->6381/tcp     redis-slave2
2e52199b473c   redis:latest   "docker-entrypoint.s…"   20 hours ago   Up 46 minutes   6379/tcp, 0.0.0.0:6380->6380/tcp     redis-slave1
689437cb98ce   redis:latest   "docker-entrypoint.s…"   21 hours ago   Up 46 minutes   0.0.0.0:6379->6379/tcp               redis-master
```

## 7.2.4 哨兵模式下的故障自动恢复效果

本小节中由于容器的读写问题,故直接使用了宿主机来模拟故障自动恢复的流程:

注:使用哨兵模式要求主从节点的用户名和密码要一致,故需要先修改2个从节点的用户名和密码

* step1. 修改slave1.conf

```
(base) root@yuanhong section7-1 % cat slave_1.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
# user slave_1_user on >slave_1_password  ~* &* +@all

# 使用sentinel保持集群高可用时 需保证从节点与主节点的用户密码一致
user master_user on >master_password ~* &* +@all

# 指定端口
port 6380

# 指定主节点IP和端口
slaveof 127.0.0.1 6379

# 指定主节点用户名
masteruser "master_user"

# 指定主节点密码
masterauth "master_password"
```

* step2. 修改slave2.conf

```
(base) root@yuanhong section7-1 % cat slave_2.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
# user slave_2_user on >slave_2_password  ~* &* +@all

# 使用sentinel保持集群高可用时 需保证从节点与主节点的用户密码一致
user master_user on >master_password ~* &* +@all

# 指定端口
port 6381

# 指定主节点IP和端口
slaveof 127.0.0.1 6379

# 指定主节点用户名
masteruser "master_user"

# 指定主节点密码
masterauth "master_password"
```

* step3. 启动1主2从,共3个进程,模拟一个redis集群

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-1/master.conf
```

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-1/slave_1.conf
```

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-1/slave_2.conf
```

* step4. 修改sentinel\_1.conf和sentinel\_2.conf

```
(base) root@yuanhong section7-2 % cat sentinel_1.conf 
# 指定哨兵进程监听的端口
port 16379

# 指定sentinel监视的redis服务器信息和判定不可用的条件
# 其中:
# sentinel monitor: 这是一个命令 告诉Sentinel监视一个Redis服务器
# redis-master-node: 被监视的Redis服务器的名字 任意命名即可
# 172.17.0.2: 被监视的Redis服务器的IP地址
# 6379: 被监视的Redis服务器的端口号
# 2: 仲裁值.即Sentinel判定Redis服务器是不可用的前提条件.
# 表示当至少有2个Sentinel报告说Redis服务器是不可用的时,才认为该服务器是不可用的
# 这可以防止一个Sentinel因为网络问题或其他原因错误地认为主服务器是不可用的
# sentinel monitor redis-master-node 172.17.0.2 6379 2
sentinel monitor redis-master-node 127.0.0.1 6379 2

# 指定被监视的redis服务器的用户名
sentinel auth-user redis-master-node master_user

# 指定被监视的redis服务器的密码
sentinel auth-pass redis-master-node master_password

# 指定哨兵节点的日志文件位置
# dir /
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-2/logs"

# 指定哨兵节点的日志文件名称
logfile "sentinel_1.log"

# 指定判定redis-master-node节点主观下线的时间阈值为60000毫秒 即60秒
sentinel down-after-milliseconds redis-master-node 60000

# 指定了故障转移的超时时间为180000毫秒 即180秒
sentinel failover-timeout redis-master-node 180000
```

```
(base) root@yuanhong section7-2 % cat sentinel_2.conf
# 指定哨兵进程监听的端口
port 16380

# 指定sentinel监视的redis服务器信息和判定不可用的条件
# 其中:
# sentinel monitor: 这是一个命令 告诉Sentinel监视一个Redis服务器
# redis-master-node: 被监视的Redis服务器的名字 任意命名即可
# 172.17.0.2: 被监视的Redis服务器的IP地址
# 6379: 被监视的Redis服务器的端口号
# 2: 仲裁值.即Sentinel判定Redis服务器是不可用的前提条件.
# 表示当至少有2个Sentinel报告说Redis服务器是不可用的时,才认为该服务器是不可用的
# 这可以防止一个Sentinel因为网络问题或其他原因错误地认为主服务器是不可用的
# sentinel monitor redis-master-node 172.17.0.2 6379 2
sentinel monitor redis-master-node 127.0.0.1 6379 2

# 指定被监视的redis服务器的用户名
sentinel auth-user redis-master-node master_user

# 指定被监视的redis服务器的密码
sentinel auth-pass redis-master-node master_password

# 指定哨兵节点的日志文件位置
# dir /
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-2/logs"

# 指定哨兵节点的日志文件名称
logfile "sentinel_2.log"

# 指定判定redis-master-node节点主观下线的时间阈值为60000毫秒 即60秒
sentinel down-after-milliseconds redis-master-node 60000

# 指定了故障转移的超时时间为180000毫秒 即180秒
sentinel failover-timeout redis-master-node 180000
```

* step5. 启动哨兵节点

```
(base) root@yuanhong ~ % redis-sentinel /StudyRedisBaseOnDocker/conf/chapter7/section7-2/sentinel_1.conf
```

```
(base) root@yuanhong ~ % redis-sentinel /StudyRedisBaseOnDocker/conf/chapter7/section7-2/sentinel_2.conf
```

* step6. 查看哨兵监控的主从状态

```
(base) root@yuanhong ~ % redis-cli -h 127.0.0.1 -p 16379
127.0.0.1:16379> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=2
```

```
127.0.0.1:16380> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=2
```

可以看到,此时哨兵监视的主节点为`127.0.0.1:6379`

* step7. 中断主节点的redis-server进程
* step8. 查看哨兵监控的主从状态

```
127.0.0.1:16379> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=odown,address=127.0.0.1:6379,slaves=2,sentinels=2
```

```
127.0.0.1:16380> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=odown,address=127.0.0.1:6379,slaves=2,sentinels=2
```

可以看到,2个哨兵节点都判定主节点已经无法连接了,到达了配置文件中设定的仲裁值

* step9. 再次查看哨兵监控的主从状态

```
127.0.0.1:16379> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=ok,address=127.0.0.1:6380,slaves=2,sentinels=2
```

```
127.0.0.1:16380> INFO sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-master-node,status=ok,address=127.0.0.1:6380,slaves=2,sentinels=2
```

可以看到,此时已经将1个从节点提升为主节点了

* step10. 在redis-slave1.conf对应的进程中查看主从情况

```
127.0.0.1:6380> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=12694,lag=0
master_failover_state:no-failover
master_replid:297c170534252df94f51b2a7d28c0e4e2f5d94d9
master_replid2:cd5124305681522e99babcb5fc46bc587e918089
master_repl_offset:12694
second_repl_offset:4920
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:12694
```

可以看到,该节点已经被提升为主节点了

* step11. 在redis-slave2.conf对应的进程中查看主从情况

```
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:233402
slave_repl_offset:233402
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:297c170534252df94f51b2a7d28c0e4e2f5d94d9
master_replid2:cd5124305681522e99babcb5fc46bc587e918089
master_repl_offset:233402
second_repl_offset:4920
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:233402
```

可以看到,该节点现在从属于`127.0.0.1:6380`了.说明故障自动恢复动作已经完成

## 7.2.5 通过日志观察故障恢复流程

查看哨兵节点1的日志:

```
17785:X 17 Aug 2023 13:42:44.264 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
17785:X 17 Aug 2023 13:42:44.264 # Redis version=6.2.13, bits=64, commit=00000000, modified=0, pid=17785, just started
17785:X 17 Aug 2023 13:42:44.264 # Configuration loaded
17785:X 17 Aug 2023 13:42:44.265 * Increased maximum number of open files to 10032 (it was originally set to 256).
17785:X 17 Aug 2023 13:42:44.265 * monotonic clock: POSIX clock_gettime
17785:X 17 Aug 2023 13:42:44.266 * Running mode=sentinel, port=16379.
17785:X 17 Aug 2023 13:42:44.268 # Sentinel ID is e5c0c593123d4e75dc3fa299cd77eed746db5562
17785:X 17 Aug 2023 13:42:44.269 # +monitor master redis-master-node 127.0.0.1 6379 quorum 2
17785:X 17 Aug 2023 13:42:44.272 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:42:44.273 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:42:48.981 * +sentinel sentinel 8c9ec52ac1a98be6e5a2eca98e9b880d2f39381d 127.0.0.1 16380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:20.854 # +sdown master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:20.915 # +odown master redis-master-node 127.0.0.1 6379 #quorum 2/2
17785:X 17 Aug 2023 13:44:20.918 # +new-epoch 1
17785:X 17 Aug 2023 13:44:20.918 # +try-failover master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:20.920 # +vote-for-leader e5c0c593123d4e75dc3fa299cd77eed746db5562 1
17785:X 17 Aug 2023 13:44:20.925 # 8c9ec52ac1a98be6e5a2eca98e9b880d2f39381d voted for e5c0c593123d4e75dc3fa299cd77eed746db5562 1
17785:X 17 Aug 2023 13:44:21.004 # +elected-leader master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.004 # +failover-state-select-slave master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.073 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.073 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.134 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.804 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.804 # +failover-state-reconf-slaves master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.863 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.011 # -odown master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.854 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.855 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.937 # +failover-end master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.938 # +switch-master redis-master-node 127.0.0.1 6379 127.0.0.1 6380
17785:X 17 Aug 2023 13:44:22.939 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6380
17785:X 17 Aug 2023 13:44:22.940 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17785:X 17 Aug 2023 13:45:22.980 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
```

其中:

* `17785:X 17 Aug 2023 13:44:20.854 # +sdown master redis-master-node 127.0.0.1 6379`:`sdown`表示主观下线
* `17785:X 17 Aug 2023 13:44:20.915 # +odown master redis-master-node 127.0.0.1 6379 #quorum 2/2`:odown表示客观下线.
  * `#quorum 2/2`:表示仲裁值为2,已经达到仲裁值了,故判定主节点客观下线
* `17785:X 17 Aug 2023 13:44:20.918 # +try-failover master redis-master-node 127.0.0.1 6379`:开始启动故障恢复流程
* 故障恢复的各个流程如下:

```
17785:X 17 Aug 2023 13:44:20.920 # +vote-for-leader e5c0c593123d4e75dc3fa299cd77eed746db5562 1
17785:X 17 Aug 2023 13:44:20.925 # 8c9ec52ac1a98be6e5a2eca98e9b880d2f39381d voted for e5c0c593123d4e75dc3fa299cd77eed746db5562 1
17785:X 17 Aug 2023 13:44:21.004 # +elected-leader master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.004 # +failover-state-select-slave master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.073 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.073 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.134 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.804 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.804 # +failover-state-reconf-slaves master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:21.863 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.011 # -odown master redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.854 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.855 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17785:X 17 Aug 2023 13:44:22.937 # +failover-end master redis-master-node 127.0.0.1 6379
```

* `17785:X 17 Aug 2023 13:44:22.938 # +switch-master redis-master-node 127.0.0.1 6379 127.0.0.1 6380`:表示已经哨兵已经操作集群切换了主节点
* 切换主节点后加载集群中的其他节点:

```
17785:X 17 Aug 2023 13:44:22.939 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6380
17785:X 17 Aug 2023 13:44:22.940 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17785:X 17 Aug 2023 13:45:22.980 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
```

查看哨兵节点2的日志:

```
17792:X 17 Aug 2023 13:42:46.955 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
17792:X 17 Aug 2023 13:42:46.955 # Redis version=6.2.13, bits=64, commit=00000000, modified=0, pid=17792, just started
17792:X 17 Aug 2023 13:42:46.961 # Configuration loaded
17792:X 17 Aug 2023 13:42:46.962 * Increased maximum number of open files to 10032 (it was originally set to 256).
17792:X 17 Aug 2023 13:42:46.962 * monotonic clock: POSIX clock_gettime
17792:X 17 Aug 2023 13:42:46.963 * Running mode=sentinel, port=16380.
17792:X 17 Aug 2023 13:42:46.965 # Sentinel ID is 8c9ec52ac1a98be6e5a2eca98e9b880d2f39381d
17792:X 17 Aug 2023 13:42:46.965 # +monitor master redis-master-node 127.0.0.1 6379 quorum 2
17792:X 17 Aug 2023 13:42:46.966 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-master-node 127.0.0.1 6379
17792:X 17 Aug 2023 13:42:46.967 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6379
17792:X 17 Aug 2023 13:42:48.310 * +sentinel sentinel e5c0c593123d4e75dc3fa299cd77eed746db5562 127.0.0.1 16379 @ redis-master-node 127.0.0.1 6379
17792:X 17 Aug 2023 13:44:20.807 # +sdown master redis-master-node 127.0.0.1 6379
17792:X 17 Aug 2023 13:44:20.923 # +new-epoch 1
17792:X 17 Aug 2023 13:44:20.924 # +vote-for-leader e5c0c593123d4e75dc3fa299cd77eed746db5562 1
17792:X 17 Aug 2023 13:44:21.863 # +config-update-from sentinel e5c0c593123d4e75dc3fa299cd77eed746db5562 127.0.0.1 16379 @ redis-master-node 127.0.0.1 6379
17792:X 17 Aug 2023 13:44:21.864 # +switch-master redis-master-node 127.0.0.1 6379 127.0.0.1 6380
17792:X 17 Aug 2023 13:44:21.864 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6380
17792:X 17 Aug 2023 13:44:21.864 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17792:X 17 Aug 2023 13:45:21.865 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
```

其中:

```
17792:X 17 Aug 2023 13:44:20.807 # +sdown master redis-master-node 127.0.0.1 6379
17792:X 17 Aug 2023 13:44:20.923 # +new-epoch 1
17792:X 17 Aug 2023 13:44:20.924 # +vote-for-leader e5c0c593123d4e75dc3fa299cd77eed746db5562 1
17792:X 17 Aug 2023 13:44:21.863 # +config-update-from sentinel 
```

可以看到主观下线的部分是相同的,但后续哨兵节点2并没有进行操作.这是因为只能由1个哨兵节点完成故障自动恢复的动作,因此如果有多个哨兵节点同时监控到主节点失效,最终只能有1个哨兵节点通过竞争得到执行故障恢复操作的权限

从两个哨兵节点的日志中可以看到,故障恢复操作的权限最终被哨兵节点1竞争得到,因此当哨兵节点2发现主节点失效后,只能停留在主观下线(sdown)阶段,无法继续进行故障恢复操作.只有当哨兵节点1完成故障恢复操作后,哨兵节点2才能再次感知到重构后的主从复制模式集群,并继续监控该集群中的节点.日志如下:

```
17792:X 17 Aug 2023 13:44:21.864 # +switch-master redis-master-node 127.0.0.1 6379 127.0.0.1 6380
17792:X 17 Aug 2023 13:44:21.864 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-master-node 127.0.0.1 6380
17792:X 17 Aug 2023 13:44:21.864 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17792:X 17 Aug 2023 13:45:21.865 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
```

## 7.2.6 故障节点恢复后的表现

* step1. 编辑master.conf

```
(base) root@yuanhong section7-1 % cat master.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on <default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user master_user on <master_password ~* &* +@all

# 指定端口
port 6379

# 至少有2个从节点处于已同步状态时才允许执行写入操作
min-replicas-to-write 2

# 指定主节点用户名
masteruser "master_user"

# 指定主节点密码
masterauth "master_password"

# 从节点延迟超过15秒即判定为未同步
min-replicas-max-lag 15
```

由于故障节点会作为从节点恢复到集群中,所以需要配置主节点的用户名和密码(其实这个应该在最初就配置进去).如果不配置,虽然后续也能加入集群,但是无法同步数据

* step2. 再次启动之前的redis-master进程

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-1/master.conf
```

* step3. 登录该节点,查看主从状态

```
(base) root@yuanhong ~ % redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> AUTH master_user master_password
OK
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_read_repl_offset:1
slave_repl_offset:1
master_link_down_since_seconds:-1
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
min_slaves_good_slaves:0
master_failover_state:no-failover
master_replid:fbafcc3ad823a52ef2254f8ba76cf8943f2ee918
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
```

可以看到,该节点自动以从节点的身份接入了集群

哨兵节点1的日志如下:

```
17785:X 17 Aug 2023 13:44:22.940 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17785:X 17 Aug 2023 13:45:22.980 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17785:X 17 Aug 2023 14:37:31.849 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17785:X 17 Aug 2023 14:37:41.762 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
```

哨兵节点2的日志如下:

```
17792:X 17 Aug 2023 13:44:21.864 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17792:X 17 Aug 2023 13:45:21.865 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
17792:X 17 Aug 2023 14:37:31.759 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-master-node 127.0.0.1 6380
```

从中大家能看到,哨兵节点不仅能自动恢复故障,而且当故障节点恢复后会自动把它重新加入到集群中,而无须人工干预.也就是说,与简单的"主从复制模式集群"相比,基于哨兵模式的集群能很好地提升系统的可靠性


# 7.3 搭建cluster集群

因为cluster的中文翻译是"集群",所以在一些资料里会把此处提到的cluster集群称为Redis集群.从广义上来讲,只要两台服务器之间有关联,它们就可以构成一个集群,所以在前文里把由"一主二从"和"哨兵节点加一主二从"等构成的系统称为"集群".这里为了避免混淆,将提到的集群称为cluster集群

**相比于哨兵模式,cluster集群能支持扩容,且无须额外的节点来监控状态,所以使用这种模式集群的系统会用得更多些**

## 7.3.1 哈希槽与cluster集群

在cluster集群中,有16384个哈希槽(hash slot),在设置Redis的键(key)时,会先用CRC16算法对key运算,然后使用16384对这个结果取模,取模的结果是多少,就把这个key放到该结果锁编号的哈希槽中.数学算法如下所示,其中`slotIndex`表示该key所存放的槽的编号

```

slotIndex = HASH_SLOT = CRC16(key) mod 16384

```

***

Redis Cluster使用16384作为哈希槽的数量,因为它是一个相对小而合理的数值,可以在集群的各个节点之间实现均衡的数据分配,同时保持管理和重新分片的复杂性在可接受的范围内.

选择16384的几个原因如下:

1. **性能和管理的平衡**:选择一个太小的哈希槽数可能会导致数据分布不均匀.相反,选择一个太大的哈希槽数会增加管理的复杂性,例如在节点之间迁移哈希槽时.16384提供了一个合适的中间值
2. **分片和迁移**:当需要将哈希槽从一个节点迁移到另一个节点时(例如当添加或删除集群节点时),较小的哈希槽数意味着每个槽可能包含更多的键,这会使迁移操作更加耗时.16384个哈希槽提供了更细粒度的数据分片,从而使得单个哈希槽的迁移成为一个相对快速的操作
3. **历史和常规选择**:这也可能是基于经验和其他分布式系统的常见实践.例如,一些其他的系统或数据库也可能使用类似的哈希槽或分区数量
4. **二进制因子**:16384是2^14.在计算机科学中,使用2的幂是很常见的,因为它们在计算上是高效的

总之,16384是一个经过权衡后的选择,它在分布均匀性、管理的复杂性和操作效率之间达到了一个平衡

***

比如某cluster集群由三台Redis服务器组成,那么编号从0到5460号哈希槽会被分配到第一台Redis服务器,5461到10922号哈希槽会被分配到第二台服务器,10923到16383号哈希槽会被分配到第三台服务器上,如下图示:

![三台服务器容纳16384个哈希槽效果示意图](/files/j7p8KxRW9uhgunoFyAqK)

同理,如果某cluster集群是由六台Redis服务器组成的,那么每台服务器上也会被平均分配一定数量的哈希槽.此外,cluster集群里也支持主从复制模式,即分配到一定数量哈希槽的Redis服务器也可以携带一个或多个从节点.下图为包含三主三从的cluster集群的效果:

![三主三从的cluster集群效果图](/files/Xt7DALW3nxUoYdYFoMtD)

## 7.3.2 初步搭建cluster集群

### a. 编写配置文件

* step1. 编写redisClusterMaster1的配置文件

```
(base) root@yuanhong section7-3 % cat clusterMaster1.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 6379

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterMaster1.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-6379.conf
```

* step2. 编写redisClusterMaster2的配置文件

```
(base) root@yuanhong section7-3 % cat clusterMaster2.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 6380

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterMaster2.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-6380.conf
```

* step3. 编写redisClusterMaster3的配置文件

```
(base) root@yuanhong section7-3 % cat clusterMaster3.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 6381

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterMaster3.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-6381.conf
```

* step4. 编写clusterSlave1的配置文件

```
(base) root@yuanhong section7-3 % cat clusterSlave1.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 26379

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterSlave1.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-26379.conf
```

注:此处并没有在配置文件中配置主从关系,后续步骤中会设置主从关系

* step5. 编写clusterSlave2的配置文件

```
(base) root@yuanhong section7-3 % cat clusterSlave2.conf
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 26380

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterSlave2.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-26380.conf
```

* step6. 编写clusterSlave3的配置文件

```
(base) root@yuanhong section7-3 % cat clusterSlave3.conf
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 26381

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterSlave3.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-26381.conf
```

可以看到,和搭建主从模式的集群一样,也是要求集群中所有节点的用户名和密码一致

### b. 启动节点

* step1. 启动clusterMaster1节点

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterMaster1.conf
```

* step2. 启动clusterMaster2节点

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterMaster2.conf
```

* step3. 启动clusterMaster3节点

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterMaster3.conf
```

* step4. 启动clusterSlave1节点

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterSlave1.conf
```

* step5. 启动clusterSlave2节点

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterSlave2.conf
```

* step6. 启动clusterSlave3节点

```
(base) root@yuanhong ~ % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterSlave3.conf
```

### c. 查看配置文件

* step1. 查看nodes-6379.conf

```
(base) root@yuanhong logs % cat nodes-6379.conf 
351a708157443107901867c29bb9b15f4a97d982 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
```

其中:

* `myself,master`:表示该节点属于master节点,且只连接到myself自身,没有同其他Redis节点关联
* step2. 查看nodes-6380.conf

```
(base) root@yuanhong logs % cat nodes-6380.conf 
41cc395f669224ace3ffea9c303a91133e6d2d1b :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
```

* step3. 查看nodes-6381.conf

```
(base) root@yuanhong logs % cat nodes-6381.conf
0daaf91c7e5f1e86201681657aa4ccfef924a6a4 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
```

* step4. 查看nodes-26379.conf

```
(base) root@yuanhong logs % cat nodes-26379.conf 
35aff0967d6acf83b3c8aa162ee0ecf672c35d77 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
```

* step5. 查看nodes-26380.conf

```
(base) root@yuanhong logs % cat nodes-26380.conf
fedc556590df33b8475f03577f71567d0aed53c5 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
```

* step6. 查看nodes-26381.conf

```
(base) root@yuanhong logs % cat nodes-26381.conf
e32c99aa8333002891153fd2144cfa63302e53b0 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
```

可以看到,此时各个节点都没有互相关联

### d. 关联各个节点

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> AUTH cluster_user cluster_password
OK
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6380
OK
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6381
OK
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 26379
OK
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 26380
OK
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 26381
OK
```

`CLUSTER MEET`命令用于向Redis Cluster节点引入新的节点.这是集群扩展或修改时的基本操作.以下是命令的功能和使用场景的详细描述:

#### 功能

当你执行`CLUSTER MEET`命令时,你实际上是在告诉一个现有的Redis Cluster节点去与另一个Redis节点(可能是新的节点)进行通信并建立连接

具体来说,执行此命令后:

1. 指定的现有节点(通常称为目标节点)将尝试与新节点建立连接
2. 一旦连接建立,两个节点将交换集群配置信息,并开始周期性地彼此同步
3. 新节点将被引入到集群网络中,但此时它可能还没有承担任何哈希槽的责任.为了使其成为集群的功能性部分,您可能需要进一步分配哈希槽或从其他节点迁移哈希槽

#### 使用场景

1. **扩展集群**:当你想要向现有的Redis Cluster添加更多节点以增加容量或提供更高的可用性时,您会使用此命令
2. **集群维护**:如果某个节点出现故障并且需要被替换,或者您只是想要重新配置集群的节点布局,你也可能会使用此命令
3. **初次设置集群**:在初次创建Redis Cluster时,您需要使用`meet`命令将所有节点连接在一起

```bash
CULSTER MEET <existing-node-ip> <existing-node-port> <new-node-ip> <new-node-port>
```

其中:

* `existing-node-ip`:现有节点的IP
* `existing-node-port`:现有节点的端口
* `new-node-ip`:要引入的新节点的IP
* `new-node-port`:要引入的新节点的端口

查看集群情况:

```
127.0.0.1:6379> CLUSTER INFO
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:0
cluster_current_epoch:5
cluster_my_epoch:1
cluster_stats_messages_ping_sent:73
cluster_stats_messages_pong_sent:88
cluster_stats_messages_meet_sent:5
cluster_stats_messages_sent:166
cluster_stats_messages_ping_received:88
cluster_stats_messages_pong_received:78
cluster_stats_messages_received:166
```

* `cluster_known_nodes`:当前cluster集群中有6个节点
* `cluster_state`:目前集群处于fail状态,因为还没有给集群中的节点分配哈希槽

### e. 分配哈希槽

为3个主节点分配哈希槽,其命令语法如下:

* `CLUSTER ADDSLOTS`:是Redis Cluster的一个命令,用于将一个或多个哈希槽分配给当前节点.这是构建Redis Cluster时的一个重要步骤,因为它决定了哪些键被存储在哪个节点

该命令的基本语法如下：

```
CLUSTER ADDSLOTS slot [slot ...]
```

其中:

* `slot`:是一个介于0到16383之间的数字,代表一个哈希槽

需要注意的是,一旦一个哈希槽被分配给一个节点,它不能被重新分配给另一个节点,除非你首先使用`CLUSTER DELSLOTS`命令从原始节点中删除它

redis-cli -h -p -a ":"

此处逐个添加slot不太现实,所以采用shell中的范围表示法来添加:

```
redis-cli -h <node-ip> -p <node-port> -a "<your-username>:<your-password>" CLUSTER ADDSLOTS <slot1> [slot2] ... [slotN]
```

* `node-ip`:要分配哈希槽的Redis Cluster节点的IP 地址
* `node-port`:要分配哈希槽的Redis Cluster节点的端口
* `<slot1> [slot2] ... [slotN]`:要分配给该节点的哈希槽的编号

例如:想要为IP为192.168.1.10、端口为7000的节点分配哈希槽1到5000,可以按如下写法:

```
redis-cli -h 192.168.1.10 -p 7000 CLUSTER ADDSLOTS {1..5000}
```

* step1. 分配哈希槽给3个主节点

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 6379 --user cluster_user --pass cluster_password CLUSTER ADDSLOTS {0..5460}
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
OK
```

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 6380 --user cluster_user --pass cluster_password CLUSTER ADDSLOTS {5461..10922}
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
OK
```

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 6381 --user cluster_user --pass cluster_password CLUSTER ADDSLOTS {10923..16383}
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
OK
```

* step2. 查看当前集群情况

```
(base) root@192 ~ % redis-cli -p 6379
127.0.0.1:6379> AUTH cluster_user cluster_password
OK
127.0.0.1:6379> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:5
cluster_my_epoch:1
cluster_stats_messages_ping_sent:1490
cluster_stats_messages_pong_sent:1586
cluster_stats_messages_meet_sent:5
cluster_stats_messages_sent:3081
cluster_stats_messages_ping_received:1586
cluster_stats_messages_pong_received:1495
cluster_stats_messages_received:3081
```

可以看到,此时集群状态为正常,且已经有16384个哈希槽被分配到了该集群中.

### f. 设置从节点

* step1. 查看节点ID
* `CLUSTER REPLICATE nodeID`:设置当前节点为指定节点的从节点

命令`CLUSTER NODES`可用于查看节点ID

```
127.0.0.1:6379> CLUSTER NODES
dda36b27b4e0ed367b3267a565ed327fcda1f0e8 127.0.0.1:6380@16380 master - 0 1692281228000 0 connected 5461-10922
6983a0e89974c9fc1714061e7d3773cf614bd5bf 127.0.0.1:26380@36380 master - 0 1692281229652 4 connected
5415f296bb4de9a01f30f7e15a91845096ff5265 127.0.0.1:26379@36379 master - 0 1692281227000 3 connected
11e718918a9c689ed80bca73e3c24d9f2bb34102 127.0.0.1:6381@16381 master - 0 1692281227000 2 connected 10923-16383
482dc2ec344c3b2997b5bd6063c48d5f1a222014 127.0.0.1:26381@36381 master - 0 1692281228644 5 connected
c182e10b7f26e1289f6aa4fd41ac707ec3319508 127.0.0.1:6379@16379 myself,master - 0 1692281228000 1 connected 0-5460
```

其中第1列即为节点ID

即:

* clusterMaster1的节点ID为:c182e10b7f26e1289f6aa4fd41ac707ec3319508
* clusterMaster2的节点ID为:dda36b27b4e0ed367b3267a565ed327fcda1f0e8
* clusterMaster2的节点ID为:11e718918a9c689ed80bca73e3c24d9f2bb34102
* step2. 设置clusterSlave1节点为clusterMaster1的从节点

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26379
127.0.0.1:26379> AUTH cluster_user cluster_password
OK
127.0.0.1:26379> ACL WHOAMI
"cluster_user"
127.0.0.1:26379> CLUSTER REPLICATE c182e10b7f26e1289f6aa4fd41ac707ec3319508
OK
```

* step3. 设置clusterSlave2节点为clusterMaster2的从节点

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26380
127.0.0.1:26380> AUTH cluster_user cluster_password
OK
127.0.0.1:26380> ACL WHOAMI
"cluster_user"
127.0.0.1:26380> CLUSTER REPLICATE dda36b27b4e0ed367b3267a565ed327fcda1f0e8
OK
```

* step4. 设置clusterSlave3节点为clusterMaster3的从节点

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26381
127.0.0.1:26381> AUTH cluster_user cluster_password
OK
127.0.0.1:26381> ACL WHOAMI
"cluster_user"
127.0.0.1:26381> CLUSTER REPLICATE 11e718918a9c689ed80bca73e3c24d9f2bb34102
OK
```

* step5. 在任一节点上查看集群节点情况

```
127.0.0.1:26381> CLUSTER NODES 
5415f296bb4de9a01f30f7e15a91845096ff5265 127.0.0.1:26379@36379 slave c182e10b7f26e1289f6aa4fd41ac707ec3319508 0 1692281820368 1 connected
6983a0e89974c9fc1714061e7d3773cf614bd5bf 127.0.0.1:26380@36380 slave dda36b27b4e0ed367b3267a565ed327fcda1f0e8 0 1692281820000 0 connected
dda36b27b4e0ed367b3267a565ed327fcda1f0e8 127.0.0.1:6380@16380 master - 0 1692281819364 0 connected 5461-10922
11e718918a9c689ed80bca73e3c24d9f2bb34102 127.0.0.1:6381@16381 master - 0 1692281821526 2 connected 10923-16383
482dc2ec344c3b2997b5bd6063c48d5f1a222014 127.0.0.1:26381@36381 myself,slave 11e718918a9c689ed80bca73e3c24d9f2bb34102 0 1692281819000 2 connected
c182e10b7f26e1289f6aa4fd41ac707ec3319508 127.0.0.1:6379@16379 master - 0 1692281819000 1 connected 0-5460
```

可以看到,目前是3主3从的状态了

## 7.3.3 在cluster集群中读写数据

在任一从节点上执行写入操作,触发写入错误:

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26379
127.0.0.1:26379> AUTH cluster_user cluster_password
OK
127.0.0.1:26379> SET name 'Peter'
(error) MOVED 5798 127.0.0.1:6380
```

可以看到,其实并不是写入操作失败了,报错信息表明写入的键被移动到了`127.0.0.1:6380`的低5798个哈希槽中.在操作中,用户希望是透明地进行数据的读写操作,而不希望看到此类的读写错误

为了达到这个效果,需要在`redis-cli`命令后加入`-c`参数,以实现互联的效果:

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26379 -c --user cluster_user --pass cluster_password
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:26379> SET name 'Peter'
-> Redirected to slot [5798] located at 127.0.0.1:6380
OK
```

注意:在redis cluster的集群下,必须在连接时使用`--user`和`--pass`指定用户名密码,才能在执行命令时不报错.当然,集群中各节点的用户名密码应相同.否则报错如下:

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26379 -c
127.0.0.1:26379> AUTH cluster_user cluster_password
OK
127.0.0.1:26379> SET name 'Peter'
-> Redirected to slot [5798] located at 127.0.0.1:6380
(error) NOAUTH Authentication required.
(0.50s)
```

读取:

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26380 -c --user cluster_user --pass cluster_password
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:26380> GET name
-> Redirected to slot [5798] located at 127.0.0.1:6380
"Peter"
```

## 7.3.4 模拟扩容和数据迁移动作

扩容1主1从:

* step1. 编写clusterMasterNew\.conf文件

```
(base) root@192 section7-3 % cat clusterMasterNew.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 6385

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterMasterNew.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-6385.conf
```

* step2. 启动clusterMasterNew节点

```
(base) root@192 section7-3 % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterMasterNew.conf 
```

* step3. 编写clusterSlaveNew\.conf文件

```
(base) root@192 section7-3 % cat clusterSlaveNew.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user cluster_user on >cluster_password ~* &* +@all

# 指定端口
port 26385

# 指定文件写入的目录
dir "/StudyRedisBaseOnDocker/conf/chapter7/section7-3/logs"

# 指定日志文件名
logfile "clusterSlaveNew.log"

# 指定开启cluster集群模式
cluster-enabled yes

# 指定自动生成的cluster集群相关配置文件的文件名
cluster-config-file nodes-26385.conf
```

* step4. 启动clusterSlaveNew节点

```
(base) root@192 section7-3 % redis-server /StudyRedisBaseOnDocker/conf/chapter7/section7-3/clusterSlaveNew.conf
```

* step5. 将clusterMasterNew节点和clusterSlaveNew节点加入集群

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 6379       
127.0.0.1:6379> AUTH cluster_user cluster_password
OK
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6385
OK
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 26385
OK
```

* step6. 在clusterSlaveNew节点设置其对应的主节点

查看节点ID:

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 26385
127.0.0.1:26385> AUTH cluster_user cluster_password
OK
127.0.0.1:26385> CLUSTER NODES
5415f296bb4de9a01f30f7e15a91845096ff5265 127.0.0.1:26379@36379 slave c182e10b7f26e1289f6aa4fd41ac707ec3319508 0 1692283204043 1 connected
b65229a1d68aabf96ad488fe4115814167afbeec 127.0.0.1:6385@16385 master - 0 1692283203000 6 connected
6983a0e89974c9fc1714061e7d3773cf614bd5bf 127.0.0.1:26380@36380 slave dda36b27b4e0ed367b3267a565ed327fcda1f0e8 0 1692283199000 0 connected
482dc2ec344c3b2997b5bd6063c48d5f1a222014 127.0.0.1:26381@36381 slave 11e718918a9c689ed80bca73e3c24d9f2bb34102 0 1692283202000 2 connected
dda36b27b4e0ed367b3267a565ed327fcda1f0e8 127.0.0.1:6380@16380 master - 0 1692283203029 0 connected 5461-10922
11e718918a9c689ed80bca73e3c24d9f2bb34102 127.0.0.1:6381@16381 master - 0 1692283201000 2 connected 10923-16383
c182e10b7f26e1289f6aa4fd41ac707ec3319508 127.0.0.1:6379@16379 master - 0 1692283201000 1 connected 0-5460
cee8de80721938454744c1604188da8db22c841e 127.0.0.1:26385@36385 myself,master - 0 1692283200000 7 connected
```

可以看到,`b65229a1d68aabf96ad488fe4115814167afbeec`为clusterMasterNew的节点ID

设置主从:

```
127.0.0.1:26385> CLUSTER REPLICATE b65229a1d68aabf96ad488fe4115814167afbeec
OK
```

查看设置结果:

```
127.0.0.1:26385> CLUSTER NODES
5415f296bb4de9a01f30f7e15a91845096ff5265 127.0.0.1:26379@36379 slave c182e10b7f26e1289f6aa4fd41ac707ec3319508 0 1692283243000 1 connected
b65229a1d68aabf96ad488fe4115814167afbeec 127.0.0.1:6385@16385 master - 0 1692283241000 6 connected
6983a0e89974c9fc1714061e7d3773cf614bd5bf 127.0.0.1:26380@36380 slave dda36b27b4e0ed367b3267a565ed327fcda1f0e8 0 1692283243261 0 connected
482dc2ec344c3b2997b5bd6063c48d5f1a222014 127.0.0.1:26381@36381 slave 11e718918a9c689ed80bca73e3c24d9f2bb34102 0 1692283244267 2 connected
dda36b27b4e0ed367b3267a565ed327fcda1f0e8 127.0.0.1:6380@16380 master - 0 1692283245908 0 connected 5461-10922
11e718918a9c689ed80bca73e3c24d9f2bb34102 127.0.0.1:6381@16381 master - 0 1692283243000 2 connected 10923-16383
c182e10b7f26e1289f6aa4fd41ac707ec3319508 127.0.0.1:6379@16379 master - 0 1692283244000 1 connected 0-5460
cee8de80721938454744c1604188da8db22c841e 127.0.0.1:26385@36385 myself,slave b65229a1d68aabf96ad488fe4115814167afbeec 0 1692283240000 6 connected
```

* step7. 重新分片

```
(base) root@192 ~ % redis-cli --cluster reshard 127.0.0.1:6379 --cluster-from c182e10b7f26e1289f6aa4fd41ac707ec3319508,dda36b27b4e0ed367b3267a565ed327fcda1f0e8,11e718918a9c689ed80bca73e3c24d9f2bb34102 --cluster-to b65229a1d68aabf96ad488fe4115814167afbeec --cluster-slots 1024 --user cluster_user --pass cluster_password
```

其中:

* `127.0.0.1:6379`:指定执行重新分配哈希槽命令的redis服务器
* `--cluster-from`:指定要迁出哈希槽的节点ID,多个节点ID由`,`分割
* `--cluster-to`:指定要迁入的节点ID
* `--cluster-slots`:指定要迁移的哈希槽数量.此处的数量是指总数,而非是每个节点上的哈希槽数量

执行结果:

```
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: c182e10b7f26e1289f6aa4fd41ac707ec3319508 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: cee8de80721938454744c1604188da8db22c841e 127.0.0.1:26385
   slots: (0 slots) slave
   replicates b65229a1d68aabf96ad488fe4115814167afbeec
M: dda36b27b4e0ed367b3267a565ed327fcda1f0e8 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 6983a0e89974c9fc1714061e7d3773cf614bd5bf 127.0.0.1:26380
   slots: (0 slots) slave
   replicates dda36b27b4e0ed367b3267a565ed327fcda1f0e8
S: 5415f296bb4de9a01f30f7e15a91845096ff5265 127.0.0.1:26379
   slots: (0 slots) slave
   replicates c182e10b7f26e1289f6aa4fd41ac707ec3319508
M: 11e718918a9c689ed80bca73e3c24d9f2bb34102 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 482dc2ec344c3b2997b5bd6063c48d5f1a222014 127.0.0.1:26381
   slots: (0 slots) slave
   replicates 11e718918a9c689ed80bca73e3c24d9f2bb34102
M: b65229a1d68aabf96ad488fe4115814167afbeec 127.0.0.1:6385
   slots: (0 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
```

确认迁移后:

```
	Moving slot 5461 from dda36b27b4e0ed367b3267a565ed327fcda1f0e8
    Moving slot 5462 from dda36b27b4e0ed367b3267a565ed327fcda1f0e8
    ...
    Moving slot 0 from c182e10b7f26e1289f6aa4fd41ac707ec3319508
    Moving slot 1 from c182e10b7f26e1289f6aa4fd41ac707ec3319508
    ...
    Moving slot 10923 from 11e718918a9c689ed80bca73e3c24d9f2bb34102
    Moving slot 10924 from 11e718918a9c689ed80bca73e3c24d9f2bb34102
    ...
```

* step8. 查看集群情况

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 6380
127.0.0.1:6380> AUTH cluster_user cluster_password
OK
127.0.0.1:6380> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:8
cluster_size:4
cluster_current_epoch:8
cluster_my_epoch:0
cluster_stats_messages_ping_sent:4858
cluster_stats_messages_pong_sent:4420
cluster_stats_messages_sent:9278
cluster_stats_messages_ping_received:4419
cluster_stats_messages_pong_received:5195
cluster_stats_messages_meet_received:1
cluster_stats_messages_received:9615
```

* `cluster_size:4`:表示集群大小.由于当前该cluster集群包含了4个主节点和4个从节点,而哈希槽是分散在了4个主节点上,所以集群大小为4

查看节点情况:

```
(base) root@192 ~ % redis-cli -h 127.0.0.1 -p 6385
127.0.0.1:6385> AUTH cluster_user cluster_password
OK
127.0.0.1:6385> CLUSTER NODES
011cf3137a08205c6459dbf2290451fa8c8d309b 127.0.0.1:6380@16380 master - 0 1692286241000 1 connected 6140-10922
4a805531f0b36880faf746a9e0d6796643ce4c99 127.0.0.1:26385@36385 slave fa5c791946ce5cee2cbde002f5a7e2fe13e70f55 0 1692286241000 7 connected
17b7a1ce43fa31ac5849bad7fc812614fc15abbd 127.0.0.1:26381@36381 slave 26f4ec73c65682d8db5e77ec3b37132f2bcdd99c 1692286243529 1692286239000 2 connected
fa5c791946ce5cee2cbde002f5a7e2fe13e70f55 127.0.0.1:6385@16385 myself,master - 0 1692286236000 7 connected 0-689 5461-6139 10923-11611
77c23e662bcb6219564e1b30509d2611a54cb029 127.0.0.1:6379@16379 master - 1692286244535 1692286240000 4 connected 690-5460
26f4ec73c65682d8db5e77ec3b37132f2bcdd99c 127.0.0.1:6381@16381 master - 0 1692286242521 2 connected 11612-16383
2c00aa4baa17f1c4a25cdd2b4ff9084419a7cd09 127.0.0.1:26379@36379 slave 77c23e662bcb6219564e1b30509d2611a54cb029 0 1692286241000 4 connected
7edf9234b5524b3a8ef3442667bf8646fef12b67 127.0.0.1:26380@36380 slave 011cf3137a08205c6459dbf2290451fa8c8d309b 0 1692286241520 1 connected
```

## 7.3.5 cluster集群的常用配置参数

* `cluster-enabled`:表示Redis节点是否支持cluster集群.取值是`yes`表示支持集群;取值是`no`表示以普通Redis服务器节点的方式启动
* `cluster-config-file`:指定cluster集群配置文件的名称.该配置文件不是由使用者创建或更改的,而是在Redis服务器第一次以cluster节点身份启动时自动生 成的.在该文件中保存了cluster集群里本节点和其他节点的信息和关联方式
* `cluster-require-full-coverage`:表示当cluster集群中有节点失效时,该集群是否继续对外提供写服务.出于1容错性考虑,建议该参数值设置为`no`.若设置为`yes`,则集群中有节点失效时,该集群只能提供读服务
* `cluster-node-timeout`:用于设置cluster集群中节点的最长不可用时间(单位:毫秒).如果主节点的不可用时间超过该参数指定的值,那么会向对应的从节点进行故障转移动作
* `cluster-migration-barrier`:用于设置主节点的最小从节点数量.假设该值设置为1，当某主节点的从节点个数小于1时,就会从其他从节点个数大于1的主节点那边调剂一个从节点过来.这样做的目的是避免出现不包含从节点的主节点,因为一旦出现这种情况,当主节点失效后,就无法再用从节点进行故障恢复的动作.也就是说,合理地设置该参数能提升cluster集群系统的可靠性


# 第8章 GO整合MySQL与Redis


# 8.1 GO通过redigo读写Redis

## 8.1.1 以go mod方式引入redigo包

* step1. 编写redis.conf文件

```
(base) yanglei@yuanhong section8-1 % cat redis.conf 
# 指定default用户的密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user default on >default_password ~* &* +@all

# 指定用户名和密码 允许执行所有命令 允许访问所有key 允许访问所有频道
user redis_user on >redis_password ~* &* +@all

# 指定端口
port 6379
```

* step2. 启动redis-server

```
(base) yanglei@yuanhong ~ % redis-server /Users/yanglei/Desktop/StudyRedisBaseOnDocker/conf/chapter8/section8-1/redis.conf
```

* step3. 使用go mod初始化项目

```
(base) yanglei@yuanhong operateRedis % pwd
/Users/yanglei/Desktop/StudyRedisBaseOnDocker/code/chapter8/section8-1/operateRedis
(base) yanglei@yuanhong operateRedis % go mod init operateRedis
go: creating new go.mod: module operateRedis
```

* step4. 引入redigo包

编写`main.go`如下:

`/Users/yanglei/Desktop/StudyRedisBaseOnDocker/code/chapter8/section8-1/operateRedis/main.go`:

```go
package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

func main() {
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatalf("Could not connect: %v\n", err)
	} else {
		log.Println("Connected")
	}
	defer conn.Close()
}
```

执行`go mod tidy`

* step5. 运行

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 13:48:03 Connected
```

* step6. 认证并确认连接
* `PING`:该命令用于测试与Redis服务器的连接是否仍然活跃.若链接活跃则Redis服务器会回复`PONG`

修改`main.go`如下:

```go
package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

func main() {
	// 连接到Redis
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatalf("Could not connect: %v\n", err)
		return
	}

	// 认证
	_, err = conn.Do("AUTH", "redis_user", "redis_password")
	if err != nil {
		log.Fatalf("Could not authenticate: %v\n", err)
		return
	}

	// 探活
	reply, err := redis.String(conn.Do("PING"))
	if err != nil {
		log.Fatalf("Could not ping: %v\n", err)
		return
	}

	log.Printf("PING Response = %s\n", reply)

	defer conn.Close()
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:00:22 PING Response = PONG
```

* step7. 封装连接

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
└── main.go

1 directory, 4 files
```

`conn/redis.go`:

```go
package conn

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

type Conf struct {
	NetWork  string // NetWork 网络类型
	Address  string // Address Redis地址 格式: ip:port
	User     string // User 用户名
	Password string // Password 密码
}

// NewConn 根据给定的配置 创建Redis连接
func NewConn(conf *Conf) (conn redis.Conn, err error) {
	// 连接到Redis
	conn, err = redis.Dial(conf.NetWork, conf.Address)
	if err != nil {
		log.Fatalf("Could not connect: %v\n", err)
		return
	}

	if conf.User != "" || conf.Password != "" {
		err = auth(conf, conn)
		if err != nil {
			log.Fatalf("Could not authenticate: %v\n", err)
			return
		}
	}

	err = ping(conn)
	if err != nil {
		log.Fatalf("Could not ping: %v\n", err)
		return
	}

	return
}

// auth 根据给定的配置和连接 进行认证
func auth(conf *Conf, conn redis.Conn) (err error) {
	_, err = conn.Do("AUTH", conf.User, conf.Password)
	return err
}

// ping 根据给定的连接 进行探活
func ping(conn redis.Conn) (err error) {
	_, err = conn.Do("PING")
	return err
}
```

`main.go`:

```go
package main

import (
	"log"
	"operateRedis/conn"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	_, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	log.Print("Connect to Redis success!")
}
```

* step8. 测试

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:33:15 Connect to Redis success!
```

## 8.1.2 通过redigo读写Redis字符串

### 写入字符串

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    └── set.go

2 directories, 5 files
```

`operate/set.go`:

```go
package operate

import "github.com/gomodule/redigo/redis"

func Set(conn redis.Conn, key string, value string) (reply string, err error) {
	return redis.String(conn.Do("SET", key, value))
}
```

`main.go`:

```go
package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Set(redisConn, "name", "Peter")
	if err != nil {
		panic(err)
	}
	log.Printf("Set command reply: %s\n", reply)
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:44:31 Set command reply: OK
```

### 读取字符串

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./       
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── get.go
    └── set.go

2 directories, 6 files
```

`operate/get.go`:

```go
package operate

import "github.com/gomodule/redigo/redis"

func Get(conn redis.Conn, key string) (reply string, err error) {
	return redis.String(conn.Do("GET", key))
}
```

`main.go`:

```go
package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Get(redisConn, "name")
	if err != nil {
		panic(err)
	}
	log.Printf("Get command reply: %s\n", reply)
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:49:10 Get command reply: Peter
```

## 8.1.3 操作各种Redis命令

### `DEL`指令

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── get.go
    └── set.go

2 directories, 7 files
```

`operate/del.go`:

```go
package operate

import "github.com/gomodule/redigo/redis"

func Del(conn redis.Conn, key string) (reply int, err error) {
	return redis.Int(conn.Do("DEL", key))
}
```

`main.go`:

```go
package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Del(redisConn, "name")
	if err != nil {
		panic(err)
	}
	log.Printf("Del command reply: %d\n", reply)
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:53:03 Del command reply: 1
```

### `KEYS`指令

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── get.go
    ├── keys.go
    └── set.go

2 directories, 8 files
```

`operate/keys.go`:

```go
package operate

import "github.com/gomodule/redigo/redis"

func Keys(conn redis.Conn, pattern string) (reply []string, err error) {
	return redis.Strings(conn.Do("KEYS", pattern))
}
```

`main.go`:

```go
package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	replies, err := operate.Keys(redisConn, "*a*")
	if err != nil {
		panic(err)
	}

	for _, reply := range replies {
		log.Printf("Keys command reply: %s\n", reply)
	}
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 14:57:57 Keys command reply: repeat
2023/08/18 14:57:57 Keys command reply: rename
```

注:此处这2个key是我通过`redis-cli`事先添加进去的

### `EXISTS`指令

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    └── set.go

2 directories, 9 files
```

`operate/exists.go`:

```go
package operate

import "github.com/gomodule/redigo/redis"

func Exists(conn redis.Conn, key string) (reply bool, err error) {
	return redis.Bool(conn.Do("EXISTS", key))
}
```

`main.go`:

```go
package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	reply, err := operate.Exists(redisConn, "name")
	if err != nil {
		panic(err)
	}
	log.Printf("name exists: %v\n", reply)

	reply, err = operate.Exists(redisConn, "rename")
	if err != nil {
		panic(err)
	}
	log.Printf("rename exists: %v\n", reply)
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/18 15:46:27 name exists: false
2023/08/18 15:46:27 rename exists: true
```

也能通过该包使用其他Redis命令

## 8.1.4 以事务的方式操作Redis

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    ├── set.go
    └── transaction.go

2 directories, 10 files
```

`operate/transaction.go`:

```go

package operate

import (
	"errors"
	"github.com/gomodule/redigo/redis"
)

func Transaction(conn redis.Conn) (replies []string, err error) {
	// 1. Watch
	_, err = redis.String(conn.Do("WATCH", "counter"))
	if err != nil {
		return nil, err
	}

	// 若counter的值大于10 则不执行事务
	// 此处假定10为counter的原值
	counter, _ := redis.Int(conn.Do("GET", "counter"))
	if counter > 10 {
		conn.Do("UNWATCH")
		return nil, errors.New("counter value is too high. Not continuing the transaction")
	}

	// 2. Multi
	err = conn.Send("MULTI")
	if err != nil {
		return nil, err
	}

	// 3. Exec
	err = conn.Send("SET", "transaction_key_1", "transaction_value_1")
	if err != nil {
		conn.Do("DISCARD")
		return nil, err
	}

	err = conn.Send("SET", "transaction_key_2", "transaction_value_2")
	if err != nil {
		conn.Do("DISCARD")
		return nil, err
	}

	replies, err = redis.Strings(conn.Do("EXEC"))
	if err != nil {
		conn.Do("DISCARD")
		return nil, err
	}

	return replies, nil
}
```

`main.go`:

```go
package main

import (
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	redisConn, err := conn.NewConn(&conf)
	if err != nil {
		panic(err)
	}

	replies, err := operate.Transaction(redisConn)
	if err != nil {
		panic(err)
	}

	for _, reply := range replies {
		log.Printf("Transaction reply: %v\n", reply)
	}
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/21 16:49:20 Transaction reply: OK
2023/08/21 16:49:20 Transaction reply: OK
```

## 8.1.5 redis连接池

如果在项目中有多个GO客户端需要连接并操作Redis对象,那么每一次访问Redis都需要经历`创建连接 -> 打开Redis并操作 -> 释放连接`的步骤.连接和关闭数据库的操作比较浪费资源,如果频繁操作,会影响Redis甚至整个系统的性能,所以这种场景可以用`redigo`连接池来管理Redis的连接.

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   ├── pool.go
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    ├── set.go
    └── transaction.go

2 directories, 11 files
```

其中,`conn/pool.go`如下:

```go
package conn

import (
	"github.com/gomodule/redigo/redis"
	"time"
)

type PoolConf struct {
	// MaxIdle 最大空闲连接数
	MaxIdle int
	// MaxActive 最大连接数，0表示无限制
	MaxActive int
	// IdleTimeout 空闲连接超时时间
	IdleTimeout time.Duration
	// Conf Redis连接配置
	Conf Conf
}

var Pool *redis.Pool

func NewPool(poolConf PoolConf) {
	Pool = &redis.Pool{
		MaxIdle:     poolConf.MaxIdle,
		MaxActive:   poolConf.MaxActive,
		IdleTimeout: poolConf.IdleTimeout,
		Dial: func() (redis.Conn, error) {
			return redis.Dial(poolConf.Conf.NetWork, poolConf.Conf.Address)
		},
	}
}

// GetConnFromPool 从连接池中获取一个经过认证后的连接
func GetConnFromPool(poolConf PoolConf) (conn redis.Conn, err error) {
	conn = Pool.Get()
	err = auth(&poolConf.Conf, conn)
	if err != nil {
		return nil, err
	}

	return conn, nil
}

// CloseConnToPool 将该连接返回到连接池中
func CloseConnToPool(conn redis.Conn) error {
	// 关闭一个从连接池中取出的连接时,使用conn.Close()方法
	// 并不会让该连接真正的关闭,而是将该连接返回到连接池中,以便后续复用
	return conn.Close()
}
```

`main.go`:

```go
package main

import (
	"fmt"
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 使用该连接进行操作
	reply, err := operate.Set(redisConn, "name", "Peter")
	if err != nil {
		log.Fatalf("set error: %v\n", err)
	}

	fmt.Printf("set reply: %v\n", reply)
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go 
set reply: OK
```

## 8.1.6 用管道的方式提升操作性能

在单个客户端里,如果要读写大量数据,那么可以采用管道(pipeline)的方式.比如要一次性地执行20次的读写,那么每条命令都需要发送到Redis服务器,而每条命令的执行结果都需要返回给客户端.如果采用管道的方式,那么这20条命令会以批量的方式一次性地发送到服务器,而结果也会一次性地返回到客户端.

换言之,在大数据操作的场景里,通过管道的方式能大量节省"传输命令和结果的时间".

工程结构如下:

```
(base) yanglei@yuanhong operateRedis % tree ./
./
├── conn
│   ├── pool.go
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
└── operate
    ├── del.go
    ├── exists.go
    ├── get.go
    ├── keys.go
    ├── pipeline.go
    ├── set.go
    └── transaction.go

2 directories, 12 files
```

其中,`operate/pipeline.go`如下:

```go
package operate

import "github.com/gomodule/redigo/redis"

// Pipeline 以管道方式一次性发送多个命令
func Pipeline(conn redis.Conn) (operateNum int, err error) {
	operateNum = 0
	err = conn.Send("SET", "name", "Peter")
	if err != nil {
		return 0, err
	}
	operateNum++

	err = conn.Send("GET", "name")
	if err != nil {
		return 0, err
	}
	operateNum++

	err = conn.Send("SET", "age", "18")
	if err != nil {
		return 0, err
	}
	operateNum++

	err = conn.Send("GET", "age")
	if err != nil {
		return 0, err
	}
	operateNum++

	// 将缓冲区的命令写入到连接中
	err = conn.Flush()
	if err != nil {
		return 0, err
	}

	return operateNum, nil
}
```

`main.go`如下:

```go
package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
	"operateRedis/conn"
	"operateRedis/operate"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 使用该连接进行操作
	operateNum, err := operate.Pipeline(redisConn)
	if err != nil {
		log.Fatalf("pipeline error: %v\n", err)
	}

	// 读取响应
	for i := 0; i < operateNum; i++ {
		reply, err := redis.String(redisConn.Receive())
		if err != nil {
			log.Fatalf("receive error: %v\n", err)
		}

		log.Printf("reply: %v\n", reply)
	}
}
```

运行结果:

```
(base) yanglei@yuanhong operateRedis % go run main.go
2023/08/21 16:43:47 reply: OK
2023/08/21 16:43:47 reply: Peter
2023/08/21 16:43:47 reply: OK
2023/08/21 16:43:47 reply: 18
```


# 8.2 Go与各种Redis数据类型

## 8.2.1 读写列表类对象

工程结构如下:

```
(base) yanglei@yuanhong redisDataType % tree ./
./
├── conn
│   ├── pool.go
│   └── redis.go
├── dataType
│   └── list
│       └── list.go
├── go.mod
├── go.sum
└── main.go

3 directories, 6 files
```

其中`conn/`目录下的文件和上一小节的完全相同.

`dataType/list/list.go`:

```go
package list

import (
	"github.com/gomodule/redigo/redis"
)

func LPush(conn redis.Conn, key string, values ...string) (int, error) {
	length := 0

	for _, value := range values {
		err := conn.Send("LPUSH", key, value)
		if err != nil {
			return 0, err
		}
	}
	err := conn.Flush()
	if err != nil {
		return 0, err
	}

	for i := 0; i < len(values); i++ {
		reply, _ := redis.Int(conn.Receive())
		length = reply
	}

	return length, nil
}

func RPush(conn redis.Conn, key string, values ...string) (int, error) {
	length := 0

	for _, value := range values {
		err := conn.Send("RPUSH", key, value)
		if err != nil {
			return 0, err
		}
	}
	err := conn.Flush()
	if err != nil {
		return 0, err
	}

	for i := 0; i < len(values); i++ {
		reply, _ := redis.Int(conn.Receive())
		length = reply
	}

	return length, nil
}

func LPop(conn redis.Conn, key string) (string, error) {
	return redis.String(conn.Do("LPOP", key))
}

func RPop(conn redis.Conn, key string) (string, error) {
	return redis.String(conn.Do("RPOP", key))
}

func LLen(conn redis.Conn, key string) (int, error) {
	return redis.Int(conn.Do("LLEN", key))
}

func LRange(conn redis.Conn, key string, start int, end int) ([]string, error) {
	return redis.Strings(conn.Do("LRANGE", key, start, end))
}

func LTrim(conn redis.Conn, key string, start int, end int) (string, error) {
	return redis.String(conn.Do("LTRIM", key, start, end))
}

func LSet(conn redis.Conn, key string, index int, value string) (string, error) {
	return redis.String(conn.Do("LSET", key, index, value))
}

func LIndex(conn redis.Conn, key string, index int) (string, error) {
	return redis.String(conn.Do("LINDEX", key, index))
}
```

`main.go`:

```go
package main

import (
	"fmt"
	"log"
	"redisDataType/conn"
	"redisDataType/dataType/list"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 用LPUSH命令将元素插入到列表头部
	values := []string{"3", "2", "1"}
	length, err := list.LPush(redisConn, "myList", values...)
	if err != nil {
		log.Fatalf("LPUSH error: %v\n", err)
	}
	fmt.Printf("after LPUSH, length of list =  %d\n", length)

	// 查看列表中的元素
	elements, err := list.LRange(redisConn, "myList", 0, -1)
	if err != nil {
		log.Fatalf("LRANGE error: %v\n", err)
	}
	fmt.Printf("after LPUSH, LRANGE: %v\n", elements)

	// 用RPUSH命令将元素插入到列表尾部
	values = []string{"4", "5", "6"}
	length, err = list.RPush(redisConn, "myList", values...)
	if err != nil {
		log.Fatalf("RPUSH error: %v\n", err)
	}
	fmt.Printf("after RPUSH, length of list =  %d\n", length)

	// 查看列表中的元素
	elements, err = list.LRange(redisConn, "myList", 0, -1)
	if err != nil {
		log.Fatalf("LRANGE error: %v\n", err)
	}
	fmt.Printf("after RPUSH, LRANGE: %v\n", elements)

	// 用LPOP命令从列表头部弹出一个元素
	element, err := list.LPop(redisConn, "myList")
	if err != nil {
		log.Fatalf("LPOP error: %v\n", err)
	}
	fmt.Printf("the element which operated by LPOP =  %s\n", element)

	// 查看列表中的元素
	elements, err = list.LRange(redisConn, "myList", 0, -1)
	if err != nil {
		log.Fatalf("LRANGE error: %v\n", err)
	}
	fmt.Printf("after LPOP, LRANGE: %v\n", elements)
}
```

运行结果如下:

```
(base) yanglei@yuanhong redisDataType % go run main.go
after LPUSH, length of list =  3
after LPUSH, LRANGE: [1 2 3]
after RPUSH, length of list =  6
after RPUSH, LRANGE: [1 2 3 4 5 6]
the element which operated by LPOP =  1
after LPOP, LRANGE: [2 3 4 5 6]
```

## 8.2.2 读写哈希表类对象

工程结构如下:

```
(base) yanglei@yuanhong redisDataType % tree ./       
./
├── conn
│   ├── pool.go
│   └── redis.go
├── dataType
│   ├── hash
│   │   └── hash.go
│   └── list
│       └── list.go
├── go.mod
├── go.sum
└── main.go

4 directories, 7 files
```

`dataType/hash/hash.go`:

```go
package hash

import (
	"github.com/gomodule/redigo/redis"
)

func HSet(conn redis.Conn, key string, hashes map[string]string) (int, error) {
	_, err := conn.Do("HMSET", redis.Args{}.Add(key).AddFlat(hashes)...)
	if err != nil {
		return 0, err
	}

	length, err := HLen(conn, key)
	if err != nil {
		// TODO: 此处获取长度失败不应该返回0
		return 0, err
	}

	return length, nil
}

func HLen(conn redis.Conn, key string) (int, error) {
	return redis.Int(conn.Do("HLEN", key))
}

func HGet(conn redis.Conn, key string, field string) (string, error) {
	return redis.String(conn.Do("HGET", key, field))
}

func HGetAll(conn redis.Conn, key string) (map[string]string, error) {
	return redis.StringMap(conn.Do("HGETALL", key))
}

func HExists(conn redis.Conn, key string, field string) (bool, error) {
	return redis.Bool(conn.Do("HEXISTS", key, field))
}

func HDel(conn redis.Conn, key string, fields ...string) (int, error) {
	args := redis.Args{}.Add(key).AddFlat(fields)
	return redis.Int(conn.Do("HDEL", args...))
}
```

`main.go`:

```go
package main

import (
	"fmt"
	"log"
	"redisDataType/conn"
	"redisDataType/dataType/hash"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 向哈希表添加字段
	hashMap := map[string]string{
		"field1": "value1",
		"field2": "value2",
	}
	length, err := hash.HSet(redisConn, "hash", hashMap)
	if err != nil {
		log.Fatalf("HSET error: %v\n", err)
	}
	fmt.Printf("after HSET, the length of hash is %d\n", length)

	// 从哈希表中获取字段
	value, err := hash.HGet(redisConn, "hash", "field1")
	if err != nil {
		log.Fatalf("HGET error: %v\n", err)
	}
	fmt.Printf("key = %s, field = %s, value = %s \n", "hash", "field1", value)

	// 获取哈希表的长度
	length, err = hash.HLen(redisConn, "hash")
	if err != nil {
		log.Fatalf("HLEN error: %v\n", err)
	}
	fmt.Printf("the length of hash is %d\n", length)

	// 获取哈希表的所有字段和值
	hashMapResult, err := hash.HGetAll(redisConn, "hash")
	if err != nil {
		log.Fatalf("HGETALL error: %v\n", err)
	}
	fmt.Printf("the result of HGETALL is %v\n", hashMapResult)

	// 判断哈希表中是否存在某个字段
	exists, err := hash.HExists(redisConn, "hash", "field1")
	if err != nil {
		log.Fatalf("HEXISTS error: %v\n", err)
	}
	fmt.Printf("the field1 of hash exists: %v\n", exists)

	exists, err = hash.HExists(redisConn, "hash", "field3")
	if err != nil {
		log.Fatalf("HEXISTS error: %v\n", err)
	}
	fmt.Printf("the field3 of hash exists: %v\n", exists)

	// 删除哈希表中的字段
	deletedNum, err := hash.HDel(redisConn, "hash", "field1", "field2", "field3")
	if err != nil {
		log.Fatalf("HDEL error: %v\n", err)
	}
	fmt.Printf("the number of deleted fields is %d\n", deletedNum)
}
```

```
(base) yanglei@yuanhong redisDataType % go run main.go
after HSET, the length of hashes is 2
key = hashes, field = field1, value = value1 
the length of hashes is 2
the result of HGETALL is map[field1:value1 field2:value2]
the field1 of hashes exists: true
the field3 of hashes exists: false
the number of deleted fields is 2
```

## 8.2.3 读写集合类对象

工程结构如下:

```
(base) yanglei@yuanhong redisDataType % tree ./
./
├── conn
│   ├── pool.go
│   └── redis.go
├── dataType
│   ├── hash
│   │   └── hash.go
│   ├── list
│   │   └── list.go
│   └── set
│       └── set.go
├── go.mod
├── go.sum
└── main.go

5 directories, 8 files
```

`dataType/set/set.go`:

```go
package set

import "github.com/gomodule/redigo/redis"

func SAdd(conn redis.Conn, key string, members ...interface{}) (int, error) {
	args := redis.Args{}.Add(key).AddFlat(members)
	return redis.Int(conn.Do("SADD", args...))
}

func SIsMember(conn redis.Conn, key string, member interface{}) (bool, error) {
	return redis.Bool(conn.Do("SISMEMBER", key, member))
}

func SMembers(conn redis.Conn, key string) ([]string, error) {
	return redis.Strings(conn.Do("SMEMBERS", key))
}

// SRem 删除集合中的元素(根据给定的元素值删除) 返回删除的元素个数
func SRem(conn redis.Conn, key string, members ...interface{}) (int, error) {
	args := redis.Args{}.Add(key).AddFlat(members)
	return redis.Int(conn.Do("SREM", args...))
}

// SPop 随机删除并返回集合中的一个元素
func SPop(conn redis.Conn, key string) (string, error) {
	return redis.String(conn.Do("SPOP", key))
}
```

`main.go`:

```go
package main

import (
	"fmt"
	"log"
	"redisDataType/conn"
	"redisDataType/dataType/set"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 向集合中添加元素
	newMemberNum, err := set.SAdd(redisConn, "setKey", "setValue1", "setValue2", "setValue4", "setValue6")
	if err != nil {
		log.Fatalf("SADD error: %v\n", err)
	}
	fmt.Printf("new memeber num: %d\n", newMemberNum)

	newMemberNum, err = set.SAdd(redisConn, "setKey", "setValue2", "setValue3")
	if err != nil {
		log.Fatalf("SADD error: %v\n", err)
	}
	fmt.Printf("new memeber num: %d\n", newMemberNum)

	// 判断给定元素是否存在于集合中
	isMember, err := set.SIsMember(redisConn, "setKey", "setValue1")
	if err != nil {
		log.Fatalf("SISMEMBER error: %v\n", err)
	}
	fmt.Printf("setValue1 is member: %v\n", isMember)

	isMember, err = set.SIsMember(redisConn, "setKey", "setValue5")
	if err != nil {
		log.Fatalf("SISMEMBER error: %v\n", err)
	}
	fmt.Printf("setValue5 is member: %v\n", isMember)

	// 获取集合的所有元素
	members, err := set.SMembers(redisConn, "setKey")
	if err != nil {
		log.Fatalf("SMEMBERS error: %v\n", err)
	}
	fmt.Printf("members: %v\n", members)

	// 根据给定的元素值删除元素
	delSets := []interface{}{"setValue1", "setValue2", "setValue5"}
	delNum, err := set.SRem(redisConn, "setKey", delSets...)
	if err != nil {
		log.Fatalf("SREM error: %v\n", err)
	}
	fmt.Printf("delete num by SREM: %d\n", delNum)

	// 随机删除并返回集合中的一个元素
	popMember, err := set.SPop(redisConn, "setKey")
	if err != nil {
		log.Fatalf("SPOP error: %v\n", err)
	}
	fmt.Printf("pop member: %s\n", popMember)
}
```

运行结果如下:

```
(base) yanglei@yuanhong redisDataType % go run main.go
new memeber num: 4
new memeber num: 1
setValue1 is member: true
setValue5 is member: false
members: [setValue2 setValue4 setValue1 setValue6 setValue3]
delete num by SREM: 2
pop member: setValue4
```

## 8.2.4 读写有序集合类对象

工程结构如下:

```
(base) yanglei@yuanhong redisDataType % tree ./
./
├── conn
│   ├── pool.go
│   └── redis.go
├── dataType
│   ├── hash
│   │   └── hash.go
│   ├── list
│   │   └── list.go
│   ├── set
│   │   └── set.go
│   └── sortedSet
│       └── sortedSet.go
├── go.mod
├── go.sum
└── main.go

6 directories, 9 files
```

`dataType/sortedSet/sortedSet.go`:

```go
package sortedSet

import (
	"github.com/gomodule/redigo/redis"
)

func ZAdd(conn redis.Conn, key string, scoreMap map[int]interface{}) (int, error) {
	args := redis.Args{}.Add(key).AddFlat(scoreMap)
	return redis.Int(conn.Do("ZADD", args...))
}

func ZCard(conn redis.Conn, key string) (int, error) {
	return redis.Int(conn.Do("ZCARD", key))
}

// ZRange 根据索引获取有序集合中的元素(按照分数从小到大排序)
func ZRange(conn redis.Conn, key string, start int, stop int) ([]string, error) {
	return redis.Strings(conn.Do("ZRANGE", key, start, stop))
}

// ZRevRange 根据索引获取有序集合中的元素(按照分数从大到小排序)
func ZRevRange(conn redis.Conn, key string, start int, stop int) ([]string, error) {
	return redis.Strings(conn.Do("ZREVRANGE", key, start, stop))
}

func ZScore(conn redis.Conn, key string, member string) (float64, error) {
	return redis.Float64(conn.Do("ZSCORE", key, member))
}

func ZRem(conn redis.Conn, key string, members ...string) (int, error) {
	args := redis.Args{}.Add(key).AddFlat(members)
	return redis.Int(conn.Do("ZREM", args...))
}
```

`main.go`:

```go
package main

import (
	"fmt"
	"log"
	"redisDataType/conn"
	"redisDataType/dataType/sortedSet"
	"time"
)

func main() {
	conf := conn.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	poolConf := conn.PoolConf{
		MaxIdle:     10,
		MaxActive:   100,
		IdleTimeout: time.Hour,
		Conf:        conf,
	}

	// 初始化连接池
	conn.NewPool(poolConf)

	// 从连接池中获取一个连接
	redisConn, err := conn.GetConnFromPool(poolConf)
	if err != nil {
		log.Fatalf("get conn from pool error: %v\n", err)
	}

	// 将该连接返回到连接池中
	defer conn.CloseConnToPool(redisConn)

	// 添加元素到有序集合中
	scoreMap := map[int]interface{}{
		1: "one",
		3: "three",
	}
	addNum, err := sortedSet.ZAdd(redisConn, "myZSet", scoreMap)
	if err != nil {
		log.Fatalf("ZADD error: %v\n", err)
	}
	fmt.Printf("addNum: %d\n", addNum)

	scoreMap = map[int]interface{}{
		3: "three",
		5: "five",
	}
	addNum, err = sortedSet.ZAdd(redisConn, "myZSet", scoreMap)
	if err != nil {
		log.Fatalf("ZADD error: %v\n", err)
	}
	fmt.Printf("addNum: %d\n", addNum)

	// 获取有序集合中的元素数量
	cardNum, err := sortedSet.ZCard(redisConn, "myZSet")
	if err != nil {
		log.Fatalf("ZCARD error: %v\n", err)
	}
	fmt.Printf("cardNum: %d\n", cardNum)

	// 根据分数的索引获取元素
	// 按分数从小到大排序
	members, err := sortedSet.ZRange(redisConn, "myZSet", 0, 1)
	if err != nil {
		log.Fatalf("ZRANGE error: %v\n", err)
	}
	fmt.Printf("ZRANGE members: %v\n", members)
	// 按分数从大到小排序
	members, err = sortedSet.ZRevRange(redisConn, "myZSet", 0, 1)
	if err != nil {
		log.Fatalf("ZREVRANGE error: %v\n", err)
	}
	fmt.Printf("ZREVRANGE members: %v\n", members)

	// 获取指定成员的分数
	score, err := sortedSet.ZScore(redisConn, "myZSet", "three")
	if err != nil {
		log.Fatalf("ZSCORE error: %v\n", err)
	}
	fmt.Printf("ZSCORE score: %.1f\n", score)

	// 删除指定成员
	deletedMembers := []string{"three", "five", "seven"}
	delNum, err := sortedSet.ZRem(redisConn, "myZSet", deletedMembers...)
	if err != nil {
		log.Fatalf("ZREM error: %v\n", err)
	}
	fmt.Printf("ZREM delNum: %d\n", delNum)
}
```

运行结果:

```
(base) yanglei@yuanhong redisDataType % go run main.go 
addNum: 2
addNum: 1
cardNum: 3
ZRANGE members: [one three]
ZREVRANGE members: [five three]
ZSCORE score: 3.0
ZREM delNum: 2
```

## 8.2.5 操作地理位置数据


# 8.3 Redis与MySQL的整合

## 8.3.1 通过Docker安装MySQL开发环境

此处我直接用的本地MySQL.DDL语句如下:

```SQL
CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `score` float DEFAULT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

DML语句如下:

```SQL
INSERT INTO `student` (`id`, `name`, `age`, `score`, `created_at`, `updated_at`)
VALUES
	(1,'Peter',18,100,'2023-08-23 14:10:16','2023-08-23 14:10:16'),
	(2,'Tom',17,98,'2023-08-23 14:10:27','2023-08-23 14:10:27'),
	(3,'John',17,99,'2023-08-23 14:10:38','2023-08-23 14:10:38');
```

## 8.3.2 通过GORM连接并操作MySQL数据库

本例中使用GIN + GORM + REDIGO这3个库来完成演示

### 8.3.2.1 初始化连接

工程结构如下:

```
(base) yanglei@yuanhong mysqlAndRedis % tree ./
./
├── cache
│   ├── conf.go
│   └── conn.go
├── db
│   ├── conf.go
│   └── conn.go
├── go.mod
├── go.sum
└── main.go

2 directories, 7 files
```

`cache/conf.go`:

```go
package cache

// Conf redis相关配置
type Conf struct {
	// NetWork 网络类型
	NetWork string
	// Address Redis地址 格式: ip:port
	Address string
	// User 用户名
	User string
	// Password 密码
	Password string
}
```

`cache/conn.go`:

```go
package cache

import (
	"github.com/gomodule/redigo/redis"
)

var Conn redis.Conn

// Connect 根据给定的配置 创建Redis连接
func Connect(conf Conf) (err error) {
	if Conn != nil {
		return nil
	}

	// 连接到Redis
	Conn, err = redis.Dial(conf.NetWork, conf.Address)
	if err != nil {
		return err
	}

	if conf.User != "" || conf.Password != "" {
		err = auth(conf, Conn)
		if err != nil {
			return err
		}
	}

	err = ping(Conn)
	if err != nil {
		return err
	}

	return nil
}

// auth 根据给定的配置和连接 进行认证
func auth(conf Conf, conn redis.Conn) (err error) {
	_, err = conn.Do("AUTH", conf.User, conf.Password)
	return err
}

// ping 根据给定的连接 进行探活
func ping(conn redis.Conn) (err error) {
	_, err = conn.Do("PING")
	return err
}
```

`db/conf.go`:

```go
package db

// Conf 数据库相关配置
type Conf struct {
	// Domain 数据库服务器IP地址
	Domain string
	// Port 数据库端口
	Port string
	// User 用户名
	User string
	// Password 密码
	Password string
	// Name 数据库名
	Name string
}
```

`db/conn.go`:

```go
package db

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/schema"
)

var Conn *gorm.DB

// Connect 创建连接MySQL的句柄
func Connect(conf Conf) (err error) {
	if Conn != nil {
		return
	}

	dsn := fillConnArgs(conf)

	Conn, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		// 禁用表名复数
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
	})
	if err != nil {
		return err
	}

	err = ping()
	if err != nil {
		return err
	}
	
	return nil
}

// fillConnArgs 根据配置拼接连接数据库的必要信息
func fillConnArgs(conf Conf) (args string) {
	return conf.User + ":" + conf.Password + "@tcp(" + conf.Domain +
		":" + conf.Port + ")/" + conf.Name + "?charset=utf8&parseTime=True&loc=Local"
}

// ping 测试数据库连接是否正常
func ping() (err error) {
	sqlDB, err := Conn.DB()
	if err != nil {
		return err
	}

	err = sqlDB.Ping()
	if err != nil {
		return err
	}

	return nil
}
```

`main.go`:

```go
package main

import (
	"log"
	"mysqlAndRedis/cache"
	"mysqlAndRedis/db"
)

func main() {
	mysqlConf := db.Conf{
		Domain:   "127.0.0.1",
		Port:     "3306",
		User:     "root",
		Password: "123456",
		Name:     "redisStudy",
	}
	err := db.Connect(mysqlConf)
	if err != nil {
		panic("connect mysql failed:" + err.Error())
	}

	redisConf := cache.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	err = cache.Connect(redisConf)
	if err != nil {
		panic("connect redis failed:" + err.Error())
	}
	
	// 进程结束前关闭连接
	sqlDB, err := db.Conn.DB()
	defer sqlDB.Close()
	defer cache.Conn.Close()

	log.Printf("connect redis and mysql success\n")
}
```

运行结果如下:

```
(base) yanglei@yuanhong mysqlAndRedis % go run main.go
2023/08/23 15:06:52 connect redis and mysql success
```

### 8.3.2.2 引入web框架

工程结构如下:

```
(base) yanglei@yuanhong mysqlAndRedis % tree ./
./
├── cache
│   ├── conf.go
│   └── conn.go
├── controller
│   └── student.go
├── db
│   ├── conf.go
│   └── conn.go
├── go.mod
├── go.sum
├── main.go
└── resp
    └── response.go

4 directories, 9 files
```

`controller/student.go`如下:

```go
package controller

import (
	"github.com/gin-gonic/gin"
	"mysqlAndRedis/resp"
	"net/http"
)

func GetStudentById(c *gin.Context) {
	data := map[string]interface{}{
		"id":    1,
		"name":  "Peter",
		"age":   18,
		"score": 100,
	}

	response := resp.Response{
		Code:    200,
		Message: "success",
		Data:    []interface{}{data},
	}

	c.JSON(http.StatusOK, response)
}
```

`resp/response.go`如下:

```go
package resp

type Response struct {
	Code    int
	Message string
	Data    []interface{}
}
```

`main.go`如下:

```go
package main

import (
	"github.com/gin-gonic/gin"
	"mysqlAndRedis/cache"
	"mysqlAndRedis/controller"
	"mysqlAndRedis/db"
)

func main() {
	mysqlConf := db.Conf{
		Domain:   "127.0.0.1",
		Port:     "3306",
		User:     "root",
		Password: "123456",
		Name:     "redisStudy",
	}
	err := db.Connect(mysqlConf)
	if err != nil {
		panic("connect mysql failed:" + err.Error())
	}

	redisConf := cache.Conf{
		NetWork:  "tcp",
		Address:  "localhost:6379",
		User:     "redis_user",
		Password: "redis_password",
	}

	err = cache.Connect(redisConf)
	if err != nil {
		panic("connect redis failed:" + err.Error())
	}

	// 进程结束前关闭连接
	sqlDB, err := db.Conn.DB()
	defer sqlDB.Close()
	defer cache.Conn.Close()

	// 确认连接成功后开启web服务
	r := gin.Default()
	r.POST("/student/getById", controller.GetStudentById)
	r.Run("0.0.0.0:8085")
}
```

运行结果如下:

![测试访问](/files/vH3fQFqv5pbTjX0kRb84)

## 8.3.3 引入Redis做缓存

![应用程序与MySQL和Redis之间的关系](/files/6OpPbBXUIWQAeJ5Q12VF)

本例中,我们使用Redis中的list类型缓存MySQL中的数据.其中:

* key的命名规则为:`Stu + Id`.例如:`Stu1`,`Stu2`,`Stu3`...
* value的结构为:
  * `Stu[0]`:`id`列
  * `Stu[1]`:`name`列
  * `Stu[2]`:`age`列
  * `Stu[3]`:`score`列

整体思路如下:

* step1. 根据id到Redis中查询,查询到则直接返回cache中的数据
* step2. 未查询到则去MySQL中查询
* step3. 将从MySQL中查询到的数据存入Redis

工程结构如下:

```
(base) yanglei@yuanhong mysqlAndRedis % tree ./
./
├── biz
│   └── student.go
├── cache
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── controller
│   └── student.go
├── db
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── go.mod
├── go.sum
├── main.go
├── request
│   └── student
│       └── getStudentById.go
└── resp
    └── response.go

7 directories, 13 files
```

`cache/student.go`:

```go
package cache

import (
	"github.com/gomodule/redigo/redis"
	"strconv"
)

const StudentKeyPrefix = "Stu"

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
}

func (s *Student) FindById(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Values(Conn.Do("LRANGE", key, 0, -1))
	if err != nil {
		return err
	}

	if len(reply) == 0 {
		return nil
	}

	s.Id, err = redis.Int(reply[0], nil)
	if err != nil {
		return err
	}

	s.Name, err = redis.String(reply[1], nil)
	if err != nil {
		return err
	}

	s.Age, err = redis.Int(reply[2], nil)
	if err != nil {
		return err
	}

	s.Score, err = redis.Float64(reply[3], nil)
	if err != nil {
		return err
	}

	return nil
}

func (s *Student) SaveById(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("RPUSH", key, s.Id, s.Name, s.Age, s.Score)
	if err != nil {
		return err
	}

	return nil
}
```

`db/student.go`:

```go
package db

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
}

func (s *Student) FindById(id int) error {
	return Conn.Where("id = ?", id).First(s).Error
}
```

`biz/student.go`:

```go
package biz

import (
	"mysqlAndRedis/cache"
	"mysqlAndRedis/db"
)

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
}

func (s *Student) GetById(id int) error {
	// 从缓存中获取
	cacheStu := &cache.Student{}
	err := cacheStu.FindById(id)
	if err != nil {
		return err
	}

	if cacheStu.Id != 0 {
		s.Id = cacheStu.Id
		s.Name = cacheStu.Name
		s.Age = cacheStu.Age
		s.Score = cacheStu.Score
		return nil
	}

	// 从数据库中获取
	dbStu := &db.Student{}
	err = dbStu.FindById(id)
	if err != nil {
		return err
	}

	s.Id = dbStu.Id
	s.Name = dbStu.Name
	s.Age = dbStu.Age
	s.Score = dbStu.Score

	// 写入缓存
	cacheStu.Id = dbStu.Id
	cacheStu.Name = dbStu.Name
	cacheStu.Age = dbStu.Age
	cacheStu.Score = dbStu.Score
	err = cacheStu.SaveById(id)
	if err != nil {
		return err
	}

	return nil
}
```

`request/student/getStudentById.go`:

```go
package student

type GetStudentByIdParam struct {
	Id *int `json:"id" binding:"required,gte=0"`
}
```

`controller/student.go`:

```go
package controller

import (
	"github.com/gin-gonic/gin"
	"mysqlAndRedis/biz"
	req "mysqlAndRedis/request/student"
	"mysqlAndRedis/resp"
	"net/http"
)

func GetStudentById(c *gin.Context) {
	param := req.GetStudentByIdParam{}
	response := &resp.Response{}

	err := c.ShouldBindJSON(&param)
	if err != nil {
		response.Code = 10001
		response.Message = "bind param failed: " + err.Error()
		response.Data = []interface{}{}
		c.JSON(http.StatusOK, response)
		return
	}

	id := *param.Id
	studentBiz := &biz.Student{}
	err = studentBiz.GetById(id)
	if err != nil {
		response.Code = 10002
		response.Message = "get student by id failed: " + err.Error()
		response.Data = []interface{}{}
		c.JSON(http.StatusOK, response)
		return
	}

	response.Code = 200
	response.Message = "success"
	response.Data = []interface{}{studentBiz}
	c.JSON(http.StatusOK, response)
	return
}
```

运行结果:

![测试MySQL与Redis联动](/files/dWqrKVeD5BkTWzS1mdB8)

在Redis中查询:

```
(base) yanglei@yuanhong ~ % redis-cli
127.0.0.1:6379> AUTH redis_user redis_password
OK
127.0.0.1:6379> LRANGE Stu3 0 -1
1) "3"
2) "John"
3) "17"
4) "99"
```

## 8.3.4 模拟缓存穿透现象

上文中的程序已经实现了缓存数据的功能.但是还存在一个问题:假如接收到一个数据库中不存在的id,例如`id=4`.那么按上文中的代码,每次还是会去MySQL中查询,进而导致这些请求都从Redis缓存层"穿透"到MySQL数据库.这样一来MySQL还是需要应对高并发的压力.避免缓存穿透的解决方案:**缓存不存在的数据**

工程结构如下:

```
(base) yanglei@yuanhong mysqlAndRedis % tree ./
./
├── biz
│   └── student.go
├── cache
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── controller
│   └── student.go
├── db
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── go.mod
├── go.sum
├── main.go
├── request
│   └── student
│       └── getStudentById.go
└── resp
    └── response.go

7 directories, 13 files
```

`cache/student.go`:

```go
package cache

import (
	"github.com/gomodule/redigo/redis"
	"strconv"
)

const StudentKeyPrefix = "Stu"

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
	Exist bool
}

func (s *Student) FindById(id int) (err error) {
	// 判断键是否存在
	s.Exist, err = s.exists(id)
	if err != nil {
		return err
	}

	// 键不存在则直接返回
	if !s.Exist {
		return nil
	}

	// 确认键存在,则从redis中读取(有可能读到的是一个0值)
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Values(Conn.Do("LRANGE", key, 0, -1))
	if err != nil {
		return err
	}

	s.Id, err = redis.Int(reply[0], nil)
	if err != nil {
		return err
	}

	s.Name, err = redis.String(reply[1], nil)
	if err != nil {
		return err
	}

	s.Age, err = redis.Int(reply[2], nil)
	if err != nil {
		return err
	}

	s.Score, err = redis.Float64(reply[3], nil)
	if err != nil {
		return err
	}

	return nil
}

func (s *Student) SaveById(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("RPUSH", key, s.Id, s.Name, s.Age, s.Score)
	if err != nil {
		return err
	}

	return nil
}

func (s *Student) exists(id int) (bool, error) {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Int(Conn.Do("EXISTS", key))
	if err != nil {
		return false, err
	}

	return reply == 1, nil
}
```

`biz/student.go`:

```go
package biz

import (
	"errors"
	"gorm.io/gorm"
	"mysqlAndRedis/cache"
	"mysqlAndRedis/db"
)

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
}

func (s *Student) GetById(id int) error {
	// 从缓存中获取
	cacheStu := &cache.Student{}
	err := cacheStu.FindById(id)
	if err != nil {
		return err
	}

	// 如果缓存中存在,则直接返回(虽然有可能返回零值)
	if cacheStu.Exist {
		s.Id = cacheStu.Id
		s.Name = cacheStu.Name
		s.Age = cacheStu.Age
		s.Score = cacheStu.Score
		return nil
	}

	// 从数据库中获取
	dbStu := &db.Student{}
	err = dbStu.FindById(id)
	// 如果数据库中不存在 则同样将对应的键写入Redis(此时存储的是一个零值)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}

	s.Id = dbStu.Id
	s.Name = dbStu.Name
	s.Age = dbStu.Age
	s.Score = dbStu.Score

	// 写入缓存
	cacheStu.Id = dbStu.Id
	cacheStu.Name = dbStu.Name
	cacheStu.Age = dbStu.Age
	cacheStu.Score = dbStu.Score
	err = cacheStu.SaveById(id)
	if err != nil {
		return err
	}

	return nil
}
```

`controller/student.go`:

```go
package controller

import (
	"github.com/gin-gonic/gin"
	"mysqlAndRedis/biz"
	req "mysqlAndRedis/request/student"
	"mysqlAndRedis/resp"
	"net/http"
)

func GetStudentById(c *gin.Context) {
	param := req.GetStudentByIdParam{}
	response := &resp.Response{}

	err := c.ShouldBindJSON(&param)
	if err != nil {
		response.Code = 10001
		response.Message = "bind param failed: " + err.Error()
		response.Data = []interface{}{}
		c.JSON(http.StatusOK, response)
		return
	}

	id := *param.Id
	studentBiz := &biz.Student{}
	err = studentBiz.GetById(id)

	// 查询出错
	if err != nil {
		response.Code = 10002
		response.Message = "get student by id failed: " + err.Error()
		response.Data = []interface{}{}
		c.JSON(http.StatusOK, response)
		return
	}

	// 没查到数据
	if studentBiz.Id == 0 {
		response.Code = 10003
		response.Message = "student not found"
		response.Data = []interface{}{}
		c.JSON(http.StatusOK, response)
		return
	}

	response.Code = 200
	response.Message = "success"
	response.Data = []interface{}{studentBiz}
	c.JSON(http.StatusOK, response)
	return
}
```

运行结果:

![测试访问MySQL中不存在的数据](/files/mIM3GRusAEFNepZoy75U)

在Redis中查询:

```
(base) yanglei@yuanhong ~ % redis-cli
127.0.0.1:6379> AUTH redis_user redis_password
OK
127.0.0.1:6379> LRANGE Stu8 0 -1
1) "0"
2) ""
3) "0"
4) "0"
```

## 8.3.5 模拟内存使用不当的场景

在之前的程序中,由于没有设置超时时间,因此所有的key都将永久存在于Redis中.持续这样发展下去,一定会遇到内存OOM的时刻.**因此,在使用Redis的场景中,需要合理设置缓存数据的超时时间.这不是一个可选项,而是必选项**.

此处我的实现思路为:

* 写入时设置过期时间
* 读取时重置过期时间
* 若某个key在过期时间内都未被读取,则任由其到达过期时间后被淘汰

工程结构如下:

```
(base) yanglei@yuanhong mysqlAndRedis % tree ./
./
├── biz
│   └── student.go
├── cache
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── controller
│   └── student.go
├── db
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── go.mod
├── go.sum
├── main.go
├── request
│   └── student
│       └── getStudentById.go
└── resp
    └── response.go

7 directories, 13 files
```

`cache/student.go`:

```go
package cache

import (
	"github.com/gomodule/redigo/redis"
	"strconv"
)

const StudentKeyPrefix = "Stu"

const ExpireSecond = 3600

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
	Exist bool
}

func (s *Student) FindById(id int) (err error) {
	// 判断键是否存在
	s.Exist, err = s.exists(id)
	if err != nil {
		return err
	}

	// 键不存在则直接返回
	if !s.Exist {
		return nil
	}

	// 确认键存在,则从redis中读取(有可能读到的是一个0值)
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Values(Conn.Do("LRANGE", key, 0, -1))
	if err != nil {
		return err
	}

	s.Id, err = redis.Int(reply[0], nil)
	if err != nil {
		return err
	}

	s.Name, err = redis.String(reply[1], nil)
	if err != nil {
		return err
	}

	s.Age, err = redis.Int(reply[2], nil)
	if err != nil {
		return err
	}

	s.Score, err = redis.Float64(reply[3], nil)
	if err != nil {
		return err
	}

	// 若读取了该key 则重置过期时间
	// 此处忽略错误
	_ = s.setExpireTime(id)

	return nil
}

func (s *Student) SaveById(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("RPUSH", key, s.Id, s.Name, s.Age, s.Score)
	if err != nil {
		return err
	}

	err = s.setExpireTime(id)
	if err != nil {
		return err
	}

	return nil
}

func (s *Student) exists(id int) (bool, error) {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Int(Conn.Do("EXISTS", key))
	if err != nil {
		return false, err
	}

	return reply == 1, nil
}

func (s *Student) setExpireTime(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("EXPIRE", key, ExpireSecond)
	return err
}
```

其他文件均未改变

连续访问2次后,在Redis中查看指定key的生命周期:

```
(base) yanglei@yuanhong ~ % redis-cli
127.0.0.1:6379> AUTH redis_user redis_password
OK
127.0.0.1:6379> TTL Stu1
(integer) 3595
127.0.0.1:6379> TTL Stu1
(integer) 3597
```


# 8.4 Redis缓存实战分析

## 8.4.1 缓存不存在的键,以防穿透

代码已经实现.见[8.3.4 模拟缓存穿透现象](https://github.com/rayallen20/StudyRedisBaseOnDocker/blob/master/note/%E7%AC%AC8%E7%AB%A0%20GO%E6%95%B4%E5%90%88MySQL%E4%B8%8ERedis/8.3%20Redis%E4%B8%8EMySQL%E7%9A%84%E6%95%B4%E5%90%88.md#834-%E6%A8%A1%E6%8B%9F%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E7%8E%B0%E8%B1%A1)

## 8.4.2 合理设置超时时间,以防内存溢出

代码已经实现.见[8.3.5 模拟内存使用不当的场景](https://github.com/rayallen20/StudyRedisBaseOnDocker/blob/master/note/%E7%AC%AC8%E7%AB%A0%20GO%E6%95%B4%E5%90%88MySQL%E4%B8%8ERedis/8.3%20Redis%E4%B8%8EMySQL%E7%9A%84%E6%95%B4%E5%90%88.md#835-%E6%A8%A1%E6%8B%9F%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%BD%93%E7%9A%84%E5%9C%BA%E6%99%AF)

## 8.4.3 超时时间外加随机数,以防穿透

我们在之前设置的所有超时时间均为1小时(3600秒).假设我们在某一时刻批量添加了几千个缓存数据,那么按照之前程序设置的超时时间,在1小时之后,这几千个key会同时失效.那么对这批数据的请求会被同时发送到MySQL(当然我们是假定会有对这几千个key的请求),那么MySQL同样有可能崩溃.

解决方法:**设置超时时间的数值采用`整数 + 随机数`的方式**

在8.3小节的代码基础上修改:

工程结构如下:

```
(base) yanglei@yuanhong mysqlAndRedis % tree ./
./
├── biz
│   └── student.go
├── cache
│   ├── conf.go
│   ├── conn.go
│   ├── genExpireSecond.go
│   └── student.go
├── controller
│   └── student.go
├── db
│   ├── conf.go
│   ├── conn.go
│   └── student.go
├── go.mod
├── go.sum
├── lib
│   └── randInt.go
├── main.go
├── request
│   └── student
│       └── getStudentById.go
└── resp
    └── response.go

8 directories, 15 files
```

`lib/randInt.go`:

```go
package lib

import (
	"math/rand"
	"time"
)

func GenRandInt(ceiling int) int {
	rand.Seed(time.Now().UnixNano())
	return rand.Intn(ceiling)
}
```

`cache/genExpireSecond.go`:

```go
package cache

import "mysqlAndRedis/lib"

const ExpireSecond = 3600

const CeilSecond = 60

func genExpireSecond() int {
	return ExpireSecond + lib.GenRandInt(CeilSecond)
}
```

`cache/student.go`:

```go
package cache

import (
	"github.com/gomodule/redigo/redis"
	"strconv"
)

const StudentKeyPrefix = "Stu"

type Student struct {
	Id    int
	Name  string
	Age   int
	Score float64
	Exist bool
}

func (s *Student) FindById(id int) (err error) {
	// 判断键是否存在
	s.Exist, err = s.exists(id)
	if err != nil {
		return err
	}

	// 键不存在则直接返回
	if !s.Exist {
		return nil
	}

	// 确认键存在,则从redis中读取(有可能读到的是一个0值)
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Values(Conn.Do("LRANGE", key, 0, -1))
	if err != nil {
		return err
	}

	s.Id, err = redis.Int(reply[0], nil)
	if err != nil {
		return err
	}

	s.Name, err = redis.String(reply[1], nil)
	if err != nil {
		return err
	}

	s.Age, err = redis.Int(reply[2], nil)
	if err != nil {
		return err
	}

	s.Score, err = redis.Float64(reply[3], nil)
	if err != nil {
		return err
	}

	// 若读取了该key 则重置过期时间
	// 此处忽略错误
	_ = s.setExpireTime(id)

	return nil
}

func (s *Student) SaveById(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("RPUSH", key, s.Id, s.Name, s.Age, s.Score)
	if err != nil {
		return err
	}

	err = s.setExpireTime(id)
	if err != nil {
		return err
	}

	return nil
}

func (s *Student) exists(id int) (bool, error) {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	reply, err := redis.Int(Conn.Do("EXISTS", key))
	if err != nil {
		return false, err
	}

	return reply == 1, nil
}

func (s *Student) setExpireTime(id int) error {
	idStr := strconv.Itoa(id)
	key := StudentKeyPrefix + idStr
	_, err := Conn.Do("EXPIRE", key, genExpireSecond())
	return err
}
```

运行后查询`id = 1`和`id = 2`的数据:

```
(base) yanglei@yuanhong ~ % redis-cli
127.0.0.1:6379> AUTH redis_user redis_password
OK
127.0.0.1:6379> TTL Stu1
(integer) 3610
127.0.0.1:6379> TTL Stu2
(integer) 3643
```

可以看到,各个键的生存时间都有所区别了


# 第9章 Redis应用场景与案例实现


# 9.1 Redis消息队列实战

在实际项目中,模块间可以通过消息队列(Message Queue,MQ)来交互数据,比如订单模块在处理好一个订单后可以把该订单对象放入消息队列,而记账模块则可以从该消息中取出订单对象继续执行

## 9.1.1 消息队列与Redis消息订阅发布模式

如下图示,订单模块和记账模块通过MQ交互数据.在MQ中,以FIFO的方式存储订单对象.即:订单模块在队列头部插入订单,记账模块从队列头部获取订单并处理

![消息队列效果图](/files/wtcxyNkqcztRnJgJWnmb)

用MQ来交互数据的好处是"解耦".在刚才的例子中,订单模块和记账模块间在交互数据时无需考虑对方的业务细节.在实际项目中,一般会用到一些成熟的MQ中间件,例如Kafka和RabbitMQ等,而通过Redis的消息订阅和发布模式也能实现MQ的效果

Redis的消息订阅和发布模式是一种消息的通信模式,其中发布者(publisher)可以向指定的频道(channel)发送消息,消息发送后订阅该频道的订阅者(subscriber)能收到消息.如下图示:

![Redis消息订阅发布模式效果图](/files/zqgpR2UtOvgZt9n2ZPWz)

其中,每个channel中都包含1个消息队列,当publisher向特定的channel发送多个消息后,这些消息会以队列的形式存储,并被订阅该channel的subscriber处理

这样一来,publisher在发送消息后无需关注有多少个subscriber,也无需关注subscriber处理消息的细节.反之亦然.subscriber也无需关注publisher的细节.也就是说,通过这套消息订阅和发布模式,可以最大限度地解耦模块间的交互动作

## 9.1.2 消息订阅发布的命令和流程

![Redis通信模块效果图](/files/j2W9feMKSHgro0YiyO3t)

如上图示,存储消息的channel在redisPublisher实例中.当连接redisPublisher实例的客户端创建MQChannel后,连接redisSub1和redisSub2实例的**客户端**会订阅该channel,这样当channel中有消息时,2个客户端能够自动接收消息

* step1. 创建redisPublisher实例并创建channel

编写配置文件:

```
(base) yanglei@yuanhong section9-1 % cat redisPublisher.conf 
# 指定端口
port 6379
```

启动服务:

```
(base) yanglei@yuanhong ~ % redis-server /Users/yanglei/Desktop/StudyRedisBaseOnDocker/conf/chapter9/section9-1/redisPublisher.conf
```

使用客户端连接该实例,并发布消息:

```
(base) yanglei@yuanhong section9-1 % redis-cli
127.0.0.1:6379> PUBLISH myChannel myMessage
(integer) 0
```

* `PUBLISH channel message`
  * 功能: 向指定的频道发送消息.若指定的频道不存在,则会创建一个.返回值表示接收到信息的订阅者数量
  * `channel`:频道名
  * `message`:待发送的消息
* step2. 创建redisSub1客户端,并订阅频道

```
(base) yanglei@yuanhong section9-1 % redis-cli
127.0.0.1:6379> SUBSCRIBE myChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myChannel"
3) (integer) 1
```

* `SUBSCRIBE channel [channel ...]`
  * 功能:订阅给定的一个或多个频道的信息
  * `channel`:给定的channel名
* step3. 在连接redisPublisher的客户端中发布消息

```
127.0.0.1:6379> PUBLISH myChannel Hello
(integer) 1
```

* step4. 观察redisSub1客户端的订阅结果

```
(base) yanglei@yuanhong section9-1 % redis-cli
127.0.0.1:6379> SUBSCRIBE myChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myChannel"
3) (integer) 1
1) "message"
2) "myChannel"
3) "Hello"
```

可以看到,客户端成功订阅了`myChannel`频道的消息.因此其他客户端向该channel发送消息,则该客户端立刻就能收到

* step5. 创建redisSub2客户端,并订阅频道

```
(base) yanglei@yuanhong ~ % redis-cli
127.0.0.1:6379> SUBSCRIBE myChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myChannel"
3) (integer) 1
```

* step6. 在step1的客户端中发布2条消息

```
(base) yanglei@yuanhong section9-1 % redis-cli
127.0.0.1:6379> PUBLISH myChannel Redis
(integer) 2
127.0.0.1:6379> PUBLISH myChannel "Info for MQ"
```

* step7. 观察redisSub1客户端

```
(base) yanglei@yuanhong section9-1 % redis-cli
127.0.0.1:6379> SUBSCRIBE myChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myChannel"
3) (integer) 1
1) "message"
2) "myChannel"
3) "Redis"
1) "message"
2) "myChannel"
3) "Info for MQ"
```

* step8. 观察redisSub2客户端

```
(base) yanglei@yuanhong ~ % redis-cli
127.0.0.1:6379> SUBSCRIBE myChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "myChannel"
3) (integer) 1
1) "message"
2) "myChannel"
3) "Redis"
1) "message"
2) "myChannel"
3) "Info for MQ"
```

## 9.1.3 消息订阅发布的相关命令汇总

* `PSUBSCRIBE pattern [pattern ...]`
  * 功能:订阅一个或多个符合给定模式的频道
* `UNSUBSCRIBE channel [channel ...]`
  * 退订给定的一个或多个频道

```
127.0.0.1:6379> unSUBSCRIBE myChannel
1) "unsubscribe"
2) "myChannel"
3) (integer) 0
```

## 9.1.4 GO与消息队列的实战范例

由于Pub/Sub并不是常用功能,故本例将使用比较简单的项目结构来演示

### 发布者端

```go
package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

func main() {
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	mqMessages := []string{"MQMessage1", "MQMessage2", "MQMessage3", "exit"}

	for _, message := range mqMessages {
		reply, err := conn.Do("PUBLISH", "MQChannel", message)
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("reply: %v", reply)
	}
}
```

### 订阅者端

```go
package main

import (
	"github.com/gomodule/redigo/redis"
	"log"
)

func main() {
	conn, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	psc := redis.PubSubConn{Conn: conn}
	// 订阅 MQChannel 频道
	err = psc.Subscribe("MQChannel")
	if err != nil {
		log.Fatal(err)
	}

	for {
		switch v := psc.Receive().(type) {
		case redis.Message:
			log.Printf("Received message from channel %s: %s\n", v.Channel, v.Data)
			if string(v.Data) == "exit" {
				err = psc.Unsubscribe("MQChannel")
				if err != nil {
					log.Fatal(err)
				}
				return
			}

		case redis.Subscription:
			log.Printf("%s: %s %d\n", v.Kind, v.Channel, v.Count)
			// 当取消订阅后 退出循环
			if v.Count == 0 {
				return
			}

		case error:
			log.Fatal(v)
		}
	}
}
```

### 运行

* step1. 运行订阅者

```
(base) yanglei@yuanhong subscriber % go run main.go 
2023/08/28 10:57:08 subscribe: MQChannel 1
```

此时由于发布者尚未发送消息,故订阅者hang在了死循环中

* step2. 运行发布者

```
(base) yanglei@yuanhong publisher % go run main.go 
2023/08/28 10:57:38 reply: 1
2023/08/28 10:57:38 reply: 1
2023/08/28 10:57:38 reply: 1
2023/08/28 10:57:38 reply: 1
```

* step3. 观察订阅者

```
(base) yanglei@yuanhong subscriber % go run main.go 
2023/08/28 10:57:08 subscribe: MQChannel 1
2023/08/28 10:57:38 Received message from channel MQChannel: MQMessage1
2023/08/28 10:57:38 Received message from channel MQChannel: MQMessage2
2023/08/28 10:57:38 Received message from channel MQChannel: MQMessage3
2023/08/28 10:57:38 Received message from channel MQChannel: exit
```

可以看到,订阅者在收到消息后,可以完成跳出死循环的功能


# 9.2 Go实战Redis分布式锁

在一台主机的多线程场景里,为了保护某个对象在同一时刻只能被一个线程访问,可以用锁机制.即线程只有在获取该对象锁资源的前提下才能访问,在访问完以后需要立即释放锁,以便其他线程继续使用该对象.

再把问题扩展一下,如果访问同一对象的线程来自分布式系统里的多台主机,那么用来确保访问唯一性的锁就叫分布式锁.也就是说,如果多个线程要竞争同一个资源,就需要用到分布式锁,本节将讲述基于Redis分布式锁的相关实战技巧.

## 9.2.1 观察分布式锁的特性

在某支付系统里有这样一个需求:一个操作需要先读取存放在其他主机上某账户的余额,读到后在本机内存里进行加值操作,再把更新后的余额写回对方主机上.但是,在读数据到回写数据的这个时间段里,分布式系统里的其他主机也有可能会读写该余额数据,具体的效果如下图示:

![分布式场景下的读写数据](/files/YuyxkalWaFahK5wtFJ6D)

我们假定这个分布式锁工作在一个分布式系统重的高并发场景下,因此除了应当具备"加锁"和"解锁"这2个功能外,还应具备如下两大特性:

1. 需要有"限时等待"的特性.即使加锁的主机系统崩溃导致无法再发出"解锁"指令,加在这个余额上的分布式锁也应该在一定时间后自动解锁
2. 需要确保解锁和加锁的主机必须唯一.例如:主机A发出"锁余额"的指令,同时发出"10s后解锁"指令,但是10s后主机A没有执行完操作余额的指令,此时锁应当自动释放,并且主机B获得锁

![分布式场景下解错锁的情况](/files/kVscoH6bd2dTn9Vaq16g)

如上图示,这种"解开其他主机加的锁"的情况在分布式场景中需要避免,也就是说,需要确保解锁和加锁的主机是一致的,否则不予解锁

## 9.2.2 加锁与解锁的Redis命令分析

使用`SET`和`DEL`命令来加锁和解锁.

* `SET key value [EX seconds|PX millseconds] [NX|XX] [KEEPTTL]`

其中`NX`参数表示当key不存在时才进行设置值操作.若多个线程要用分布式锁竞争同一个资源,那么这些线程可以先通过`SET flag 1 EX 60 NX`命令向名为flag的key中设置值,由于加入了`NX`参数,因此只能有1个线程设置成功,相当于这个线程抢占到了分布式锁

此外,在该命令中,使用`EX`参数指定了flag键的生存时间,所以即使抢占到分布式锁的机器因为故障而无法发起`DEL`命令实现解锁时,该flag键能够在到达生存时间后自动被删除,这样该线程对资源的占有就会被自动释放,以供其他线程继续抢占

占有资源的线程在使用完毕后通过`DEL flag`命令来删除键,从而实现解锁的动作,但是在通过`DEL`命令解锁时需要确认加锁和解锁的是同一台机器或同一个线程,避免误解锁操作

## 9.2.3 基于Go的Redis分布式锁

注:此处实现的是一个最简版的分布式锁.以多个goroutine来模拟上文中的多个线程,打印了获取锁的情况.

工程结构如下:

```
(base) yanglei@yuanhong distributeLock % tree ./
./
├── go.mod
├── go.sum
├── lock
│   └── lock.go
└── main.go

1 directory, 4 files
```

`lock/lock.go`:

```go
package lock

import (
	"github.com/gomodule/redigo/redis"
	"math/rand"
)

type DistributeLock struct {
	Key   string
	value int
	TTL   int
	Conn  redis.Conn
}

// Acquire 获取锁
func (l *DistributeLock) Acquire() bool {
	result, err := redis.String(l.Conn.Do("SET", l.Key, l.value, "NX", "EX", l.TTL))
	if err != nil {
		return false
	}

	return result == "OK"
}

// Release 释放锁
func (l *DistributeLock) Release() bool {
	// KEY存在 且 VALUE和设置时的值相同,才能删除
	luaScript := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
	return redis.call("DEL", KEYS[1])
else
	return 0
end
`
	script := redis.NewScript(1, luaScript)
	result, err := redis.Int(script.Do(l.Conn, l.Key, l.value))
	if err != nil {
		return false
	}

	return result == 1
}

// GenValue 生成随机值作为锁的value
func (l *DistributeLock) GenValue() {
	l.value = rand.Int()
}
```

`main.go`:

```go
package main

import (
	"distributeLock/lock"
	"fmt"
	"github.com/gomodule/redigo/redis"
	"log"
	"sync"
	"time"
)

func main() {
	// 创建锁1
	conn1, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		log.Fatalf("conn error: %v\n", err)
	}
	distributeLock1 := lock.DistributeLock{
		Key:  "flag",
		TTL:  60,
		Conn: conn1,
	}
	distributeLock1.GenValue()
	defer conn1.Close()

	// 创建锁2
	conn2, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		log.Fatalf("conn error: %v\n", err)
	}
	distributeLock2 := lock.DistributeLock{
		Key:  "flag",
		TTL:  60,
		Conn: conn2,
	}
	distributeLock2.GenValue()
	defer conn2.Close()

	// 创建锁3
	conn3, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		log.Fatalf("conn error: %v\n", err)
	}
	distributeLock3 := lock.DistributeLock{
		Key:  "flag",
		TTL:  60,
		Conn: conn3,
	}
	distributeLock3.GenValue()
	defer conn3.Close()

	// 启动3个goroutine
	wg := &sync.WaitGroup{}
	wg.Add(3)
	go retrieveLock(1, wg, distributeLock1)
	go retrieveLock(2, wg, distributeLock2)
	go retrieveLock(3, wg, distributeLock3)
	wg.Wait()
}

func retrieveLock(i int, wg *sync.WaitGroup, distributeLock lock.DistributeLock) {
	haveRetrieve := false

	for !haveRetrieve {
		// 获取锁
		if distributeLock.Acquire() {
			fmt.Printf("goroutine %d acquire lock success\n", i)
			haveRetrieve = true
		} else {
			fmt.Printf("goroutine %d acquire lock failed, retry soon\n", i)
			time.Sleep(2 * time.Second)
			continue
		}

		// 以等待5s作为假定的业务处理时间
		fmt.Printf("goroutine %d is processing\n", i)
		time.Sleep(5 * time.Second)

		// 释放锁
		if distributeLock.Release() {
			fmt.Printf("goroutine %d release lock success\n", i)
		} else {
			fmt.Printf("goroutine %d release lock failed\n", i)
		}
	}

	wg.Done()
}
```

运行结果:

```
(base) yanglei@yuanhong distributeLock % go run main.go
goroutine 1 acquire lock success
goroutine 1 is processing
goroutine 3 acquire lock failed, retry soon
goroutine 2 acquire lock failed, retry soon
goroutine 2 acquire lock failed, retry soon
goroutine 3 acquire lock failed, retry soon
goroutine 2 acquire lock failed, retry soon
goroutine 3 acquire lock failed, retry soon
goroutine 1 release lock success
goroutine 3 acquire lock success
goroutine 3 is processing
goroutine 2 acquire lock failed, retry soon
goroutine 2 acquire lock failed, retry soon
goroutine 2 acquire lock failed, retry soon
goroutine 3 release lock success
goroutine 2 acquire lock success
goroutine 2 is processing
goroutine 2 release lock success
```

注:

1. 此处我尝试过用连接池管理`distributeLock.Conn`,但是在执行`SET`命令时报错,故使用给每个goroutine手动创建连接的方式执行
2. 使用`distributeLock.GenValue()`方法设置value是为了避免3个goroutine手动设置相同的value
3. 这个最简版的分布式锁,没有实现可重入的功能


