# redis入门指南

## 第一章 简介

字典形式存储，数据结构无法嵌套。更贴近程序。

数据存在内存中，需要持久化，异步写入硬盘。常作为缓存、队列。

所有操作都是原子的。

可以设置key的生存时间、最大内存、阻塞式读取、发布/订阅模式

### 多数据库

select命令更换数据库，数字编码，无法命名。默认链接0号。

不支持访问密码，全访问/不访问

数据库之间不完全隔离，更像是命名空间。最好放相关的数据，如生产/测试

不同应用应该用不同的redis实例来存储。

### 存储结构

不同数据结构，有不同命令前缀，不能混着用

换言之不同前缀的set，也就声明了不同数据结构

* 字符串string
* 散列hash - H
* 列表list - L
* 集合set - S
* 有序集合zset - Z

### key命名

`对象类型:对象ID:对象属性`，多个单词用`.`分隔

* keys \* 遍历存的所有的key

## 第二章 数据类型

### 字符串

最多512m

* set key value

  get key
* incr key 递增
* exists key
* del key \[key...]

### hash

key是字典名，里面有字段field和字段值value

字段值只能是字符串。

![image-20191119105402225](https://tva1.sinaimg.cn/large/006y8mN6gy1g935txgqmpj31800jm41d.jpg)

* hset key field value 插入/更新

  hget key field
* hmset key field value \[key value…] 多存多取

  hmget ...
* hgetall key
* hexists key field
* hsetnx key field value 不存在则赋值
* hincrby key field increment 增加数字
* hdel key field \[field ...]
* hlen key 数量

### 列表

可以向列表两端添加元素，或获取某个片段。

内部用双向链表，所以越接近两端，访问速度越快，中间或索引访问很慢。

更适合新鲜事、排名、记录日志。

* lpush key value \[value...] rpush key value \[value...]
* lpop key

  rpop key
* llen key
* lrange key start stop 支持负索引，最右是-1，lrange key 0 -1就是获取全部
* lrem key count value 删除前count个值为value的元素，返回实际删除个数

  count>0，从左；<0从右，=0，删除所有
* lindex key index 获取索引值
* ltrim key start end 只保留选中片段
* linsert key before/after pivot value 从左往右在第一个pivot前/后插入value
* rpoplpush source destination source列表右边弹出，destination左边插入 可以做一个执行堆栈

### 集合

每个元素不同，且没有顺序。可以并、交、差。

底层用key为空的hash。

适合文章标签之类的

* sadd key member \[member…] 已存在会忽略，返回增加了多少元素

  srem key member \[member...]
* smembers key 获取所有元素

  scard key 元素个数
* sismember key member 判断是否在集合中
* spop key 随机弹出一个元素

#### 运算

* sdiff key \[key...]

  差，A-B
* sinter key \[key...]

  交，A∩B
* sunion key \[key...]

  并，A∪B

### 有序集合

集合每个元素都关联一个分数，不同元素可以分数相同。

底层是hash表+跳跃表，读取中间位置也很快

> 跳跃表：每个节点随机有多少个节点，来链接后面
>
> ![image.png](https://tva1.sinaimg.cn/large/006y8mN6gy1g93awilx09j310o04j3yt.jpg)

可以方便的调整某个元素位置，但比列表更占内存。

* zadd key score member \[score member ...]
* zscore key member 获得元素分数
* zrange key start stop \[withscores] 获得排名在某范围为元素，含两端

  有withscores会带上分数
* zincrby key increment member 增加某个元素分数
* zcard key 获得集合元素数量

  zcount key min max 指定分数范围内元素个数

## 第四章 进阶

### 4.1 事务multi/exec

一组命令的集合，要么都执行，要么不执行。

而且保证执行过程中，无法被其他命令插入

但执行失败了无法回滚，得手动收拾烂摊子。

```
multi
...  // 命令
exec
```

如果中间命令加入成功，会返回queued

#### 错误

* 语法错误

  返回错误，然后后面命令就不执行了
* 运行错误

  其他命令照样执行（前后）

#### 锁：watch

就是监听一个值，如果被修改了，之后的事物就不会再进行了。

然后要手动回滚+重新运行函数

可以在事务一半手动unwatch放开

### 4.2 过期时间expire

* expire key seconds

精确到秒

pexpire key ms，精确到毫秒

* ttl key

  查询还有多久会被删除 被删了之后返回-2，如果没有设置过期时间，返回-1
* persist key 取消过期时间

#### 最大可用内存

maxmemory

超过了会lru，最近最少使用，来删掉不用的

#### 访问频率限制

列表，记录用户最近10次访问时间。如果最早的访问在1分钟内，就报错。

### 4.3 排序sort

有序集合适合场景，是大数据排序。

sort key

集合、列表、有序集合

* sort key alpha，根据字母排序

没有的话就转成双精度浮点数来排序

* sort key desc 降序
* sort key by field->value

field里面必须有`*`，会根据`*`的值来排序。

`sort tag by post:*->time`

在其他地方，或者没有`*`，都不能排序

* … get field->value

排序后返回field里面的\*，对应的内容

`sort tag by post:*->time get post:*->title`

id是#

* … store ${value} 来把结果存到某个变量里

#### 性能优化

复杂度是`O(n+mlog(m))`，m是要返回元素个数，n是要排序的元素个数

* 待排序要少
* 取出来的要少
* 数据量大，用store缓存结果

### 4.4 消息通知

#### 任务队列

用独立线程来放队列，队列两边是生产者和消费者

* 生产者producer，给任务的人。把任务放进任务队列
* 消费者consumer，做任务的人。从队列读任务并执行

可以有多个消费者

* brpop queue 0，没有元素就会一直阻塞，就不用空数组还拼命读了

  0是超时时间，没新的元素，就会永远堵塞

#### 优先级队列

blpop key \[key…] timeout

同时检测多个键，所有键都没有元素就堵塞。

不然从左到右读取。因此优先的队列可以放在左边

#### 发布/订阅

发布：publish channel message

订阅：subscribe channel \[channel...]

订阅者无法获得订阅之前的消息

### 4.5 管道pipelining

为了减少网络传输，最好一口气发多条命令，然后一次性把结果返回

### 4.6 空间优化

数量少的时候，用效率更低，但占用空间更少的数据结构。到了一定数量再换回原本数据结构。

转换过程不可逆，数量减少了依然保持原本的数据结构。

#### 字符串

* 小于64位有符号整数，用long类型。

  能节约一半空间

sdshdr类型，包括了字符串长度、buf剩余空间、内容

* redisObject

启动时会预先建立10000个数字，作为共享对象。就10000内只用存指针

即存小数字不用额外内存。

* 键值内容不超过39字节，用redis\_encoding-embstr编码

  超过了用redis\_encoding\_raw编码

#### 散列类型

* key/value/总数，少于hash-max-ziplist-entires

用redis\_encoding\_ziplist存

* 有一个超过了，就用redis\_encoding\_ht

**redis\_encoding\_ziplist**

* 存前一个元素大小，方便倒序找
* 编码类型、元素大小、是否数字类型（更加省空间）
* 实际内容

实际存，就是一个列表，field、value并列。要找的话从头一个个找

#### 列表类型

* redis\_encoding\_linkedlist，双向链表
* redis\_encoding\_ziplist
* redis\_encoding\_quicklist

  将长列表变成若干链表组织的ziplist

#### 集合类型

小于set-max-intset-entires用redis\_encoding\_intset，多了用redis\_encoding\_ht

* redis\_encoding\_intset

有序存储，二分查找。

#### 有序集合

小了用ziplist，不然是skiplist

* redis\_encoding\_skiplist

用hash和跳表来存储。

hash表，存元素值和元素分数

跳表，存分数到元素值的映射

## 第六章 脚本

可以用Lua语言，写脚本，然后传到redis中执行

把逻辑用脚本实现，程序就更容易升级和扩展

## 第七章 持久化

* 缓存穿透：查一个数据库里面不存在的东西，找不到，于是就不断请求redis，再不断请求数据库，导致数据库宕机

可以返回一个空，就每次查询，redis直接返回查不到

* 缓存雪崩：很多缓存同一时间失效，然后redis同时请求数据库一堆内容，导致数据库宕机

每个缓存失效时间要设置的不同，不能同时失效

redis支持两种持久化：

* RDB：根据规则，定时把内存数据存在硬盘上
* AOF：记录执行了哪些命令

一般两者一起用

### RDB

根据规则生成快照（snapshotting），记录修改了哪些地方

异常退出，就只会丢失最后一次快照后更改的内容。

#### 规则

* 直接执行save，同步存

  bgsave，异步存
* 配置文件中规定更新时间

save 900 1，900s内有一次以上改动，就更新。

多条规则是`或`关系

* flushall，刷新。清除数据库所有数据，执行快照
* 主从模式，复制时快照

#### 快照原理

1. fork()复制当前进程副本
2. 子进程开始把内存中的数据写入硬盘中的临时文件

   这是经过压缩的数据，占用空间会小于内存大小
3. 写完所有数据，才把临时文件替换旧dump.rdb文件。

   换言之，任何时刻rdb文件都是完整的

写时复制：父子进程共享同一片内存，父进程要修改某一部分，操作系统就复制这部分数据给他用。

因此fork时内存不会变大一倍，因此可以申请超过可用内存的空间。

如果拍快照的时候疯狂写入，那么就会导致内存占用明显变大，导致内存溢出。

换言之快照存的是，fork的那一刻的数据

### AOF

把每一条redis命令记录进appendonly.aof文件。需要手动开启。

aof纯文本，记录客户端发送的原始通信协议内容。

但是会有冗余命令，因此每当达到一定条件，自动重写aof文件

重写过程只和内存中数据有关，和之前aof文件无关。

redis启动时，会逐个执行aof文件命令，把硬盘数据加载到内存中。

#### 同步硬盘

写入硬盘，是写入了硬盘的缓存，一般操作系统30s会同步一次进入硬盘。

也可以主动要求系统，把缓存内容同步到硬盘中。

一般使用默认值，每秒同步一次即可。

## 第八章 集群

* 避免单点故障
* 要扩容

### 8.1复制

主从结构。主master数据库，可以读写，主要是读；从slave数据库，只读。

主数据改动会同步给从。一主可有多从，一从只能有一主。主从关系可更改。

从也可以为主，下面跟从数据库。

适合读多写少的场景。

info replictaion来查看信息

#### 同步

一开始的同步，是主数据库异步在后台保存快照，即RDB持久化。同时把接收到的命令缓存起来。

然后复制给从数据库，从数据库从快照和命令缓存恢复。

同步过程中，请求数据的命令都回复错误。即不让读数据。

此后主数据库，就同步修改命令，给从数据库

**乐观复制**

允许主从之间有数据不同步，但最终会同步。

复制数据是异步的，因此会有数据不一致的时间窗口。

**无硬盘复制**

* 不论有没有开rdb，快照都会拍。那么下次启动，主数据库就会以快照恢复数据。

如果没开rdb，就可能恢复了错误时间点的数据

* 每次和数据库同步，redis都会执行一次快照，导致性能降低

无硬盘复制，初始化时，不存硬盘，直接通过网络发送给从数据库，绕开硬盘性能瓶颈。

**增量复制**

从数据库断开连接，之后连接上主，如果再快照走一遍，就很浪费。

* 存主数据库的运行id，每个实例对应一个id，重启生成新的
* 主数据库记录每个命令，放在一个积压队列里面，记录偏移量（指针）
* 从数据库接收主数据库命令，也要记录命令的id（偏移量）

重连后

* 判断运行id是否和自己的一致，之前是不是和自己在同步的
* 命令偏移量比对一下，把增量命令发给从

如果不满足，就全部同步

挤压队列就是固定长度的循环队列。越长允许断线时间越长。

### 8.2哨兵

主挂了怎么办？哨兵自动化监控+故障恢复

如果主挂了，就选一个从当主，如果主再连上，就自动变成从。

一般一个节点一个哨兵盯着。哨兵环境尽可能模拟数据库环境。

* 定期info获取当前数据库的信息。

  有新节点自动发现，也能获取从数据库列表，然后同样方式建立连接
* 定期和别的哨兵沟通，分享已有信息、主数据库的配置版本

  有新哨兵就能这样发现。
* ping其他哨兵和数据库，监控有没有停止服务。

#### 故障处理

1. 如果被ping的对方超时没有，就认为其 主观下线。

即从这个哨兵的视角看来它是下线了

1. 然后就把消息同步给其他哨兵。当认为 主观下线的人超过一半，就认为 客观下线了
2. 选举领头哨兵节点，进行故障恢复

**选举方法**

* 发现客观下线的节点，告诉每个哨兵，选自己当节点
* 如果哨兵还没选过人，就选他
* 如果投票结果没有超过半数的，每个人参选人随机等一段时间，让别人投自己

**恢复方法**

停止服务的主数据库，从数据库当中挑选一个来当新的主

* 人为设置优先级高的当选
* 命令偏移量大，即最接近主数据库数据的当选
* 都一样就看id小的当选

### 8.3 集群

每个数据库拥有全部数据，那么存储容量就会受制于内存最小的数据库。

水瓶扩容，就是客户端分片，整个数据分布在n个节点中。【有点像哈希表】

可以预分片，先规划好哪些存哪里。

#### Cluster

比客户端分片性能更优、更方便

* 只能用默认0号数据库
* 一个集群至少3个主数据库
* 每个节点都有自己的运行ID，是集群中的身份证。每个节点都完全独立。

#### 初始化

* redis-trib.rb，以客户端形式ping所有节点
* 向每个节点发送cluster meet命令，告诉其他节点的ip和port
* 分配主从节点，主从不在一个ip，主尽量不在一个ip，容灾能力。
* 主数据库分配插槽，哪些键归哪些节点负责

#### 分配插槽

没规定的话，插槽连续。

设定键的时候，可以用`{}`包裹标签，这样就会被分配到同一个插槽当中了。同一个节点，才能支持多健的批量操作。

#### 节点增加

给新节点发送，集群中任意一个节点的ip和port。相当于找个人引荐

其他人都能顺着找到。

#### 找不到就跳转

类似于路由，这里找不到就跳转，重新定向。比如在同步数据之类的时候。

不过这样就增加了请求次数，因此可以加个缓存。

#### 故障恢复

定期抽取5节点，选择最久没ping的ping一下

* 一段时间内没回复，人为疑似下线（主观下线）
* 如果整个集群一半都认为下线，就标记为下线，启动故障恢复。
* 下线的从数据库里面，找一个从，变成主。也是用哨兵的那个算法

## 第九章 管理

### 9.1 安全

没有多少安全工作。需要运行在“可信环境”中。redis默认接受任何地址发来的请求。

生产环境中也不能外界直接连接到redis服务器，需要应用程序进行中转。

#### 密码

配置密码，每次链接都要发送密码。

但redis性能高且密码错误后不会主动延迟。所以可以穷举密码，一秒尝试十几万个。

主数据库设置了密码，从数据库要配置相应密码，就能自动认证。

#### 通信协议

* 简单协议，就是用空格断句，平常命令行敲的。
* 统一请求回复，AOF和主从复制的统一请求协议

返回值是一样的

### 9.3第三方管理工具

* 耗时命令日志slow log
* monitor，调试和纠错，监控所有命令
* rdbtools，快照文件分析，到处json数据、占用内存情况等。
