参考:
http://doc.redisfans.com/index.html
https://mp.weixin.qq.com/s/QSyvSV2BYcNiidP3WpSeCw
Redis
,Remote Dictionary Server
,使用C
语言编写的非关系型键值对存储数据库。
特性:
Redis
是单线程的,避免线程切换开销及多线程的竞争问题。单线程是指网络请求使用一个线程来处理,Redis
运行时不止有一个线程,如数据持久化时会开启另一个线程处理。String
、Hash
、List
、Set
、Zset
等Redis
还支持对几个操作合并后的原子性执行Redis
支持RDB
和AOF
两种持久化机制,可以有效地避免数据丢失问题。Memcached和Redis的区别:
MemCached
数据结构单一,仅用来缓存数据,而Redis
支持多种数据类型MemCached
不支持数据持久化IO
模型不同,Memcached
是多线程非阻塞IO
复用,Redis
使用单线程的IO
复用模型Memcached
使用预分配的内存池的方式,Redis
使用现场申请内存的方式来存储数据Redis
提供主从同步机制和cluster
集群部署能力,能够提供高可用服务。Memcached
没有提供原生的集群模式,需要依靠客户端实现往集群中分片写入数据。String
: 最常用的一种数据类型,String
类型的值可以是字符串、数字或者二进制,但值最大不能超过512MB
Hash
:一个键值对集合
Set
:无序去重的集合。Set
提供了交集、并集等方法
List
:有序可重复的集合,底层是依赖双向链表实现的。
SortedSet
:有序Set
。内部维护了一个score
的参数来实现。适用于排行榜和带权重的消息队列等场景。
特殊的数据类型:
1、Bitmap
:位图,可以认为是一个以位为单位数组,数组中的每个单元只能存0
或者1
,数组的下标在Bitmap
中叫做偏移量。Bitmap
的长度与集合中元素个数无关,而是与基数的上限有关。
2、Hyperloglog
。HyperLogLog
是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。典型的使用场景是统计独立访客。
3、Geospatial
:主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。
Redis
原子性的执行一系列命令
事务执行流程:
MULTI
开启一个事务EXEC
按顺序执行所有命令WATCH
命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行(类似于乐观锁)。可使用UNWATCH
取消监控
Redis
事务不保证原子性,即事务中的所有命令不一定都成功,其中一条命令出错不会影响其他命令的执行
即将数据保存到磁盘的方式,保证服务崩溃后不会丢失数据
Redis
支持RDB
和AOF
两种持久化方式
在启用AOF
和RDB
持久性的情况下,Redis
重新启动AOF
文件将用于重建原始数据集,因为它保证数据是最完整的。
默认的持久化方案
触发方式:
SAVE
或BGSAVE
命令
SAVE
命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot
)以RDB
文件的形式保存到硬盘BGSAVE
在后台异步(Asynchronously)保存当前数据库的数据到磁盘。BGSAVE
命令执行之后立即返回 OK
,然后Redis fork
出一个新子进程,原来的Redis
进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出SAVE 100 10
,100秒内至少有10个键被修改则进行快照BGSAVE
生成 RDB 文件并发送给从节点。shutdown
命令时,如果没有开启AOF
持久化功能则自动执行BGSAVE
。优点:
RDB
文件恢复数据比AOF
快BGSAVE
使用单独子进程进行持久化,主进程不会进行任何IO
操作缺点:
AOF(Append Only File)
:AOF
持久化记录服务器接收到的每个写操作,在服务器启动时再次执行,重建原始数据集。命令使用与Redis
协议本身相同的格式以仅附加方式记录。当日志变得太大时,Redis
能够在后台重写日志
通过
appendonly
参数启用:appendonly yes
日志数据会先写道缓存中,然后可配置如下参数确定何时将缓存中数据写入到磁盘上:
appendfsync always # 每次写入aof文件都会执行同步,最安全最慢
appendfsync everysec # 每秒同步既保证性能也保证安全,建议配置
appendfsync no # 由操作系统决定何时进行同步操作
优点:
AOF
以append-only
的模式写入,所以没有磁盘寻址的开销,写入性能非常高缺点:
AOF
文件通常比相同数据集的等效RDB
文件大基于内存:使用内存存储数据,没有磁盘IO上的开销,读写速度非常快
单线程实现(Redis 6.0
以前):Reids
使用单个线程处理请求,避免多个线程之间线程切换和锁资源竞争的开销
IO
多路复用:Redis
采用IO
多路复用技术。Redis
使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O
上浪费过多的时间
高效的数据结构:Redis
每种数据类型底层都做了优化
考虑单线程的原因:
I/O
处理多个客户端的连接请求Redis
服务大多数操作的性能瓶颈都不是CPU
,而是网络传输,即网络I/O
重要特性:
Redis
使用异步复制。 从Redis 2.8
开始, 从服务器会以每秒一次的频率向主服务器报告复制流(replication stream)
的处理进度。
一个主服务器可以有多个从服务器。
从服务器也可以有自己的从服务器, 多个从服务器之间可以构成一个图状结构。
复制功能不会阻塞主服务器,即使有一个或多个从服务器正在进行初次同步, 主服务器也可以继续处理命令请求。
复制功能也不会阻塞从服务器: 只要在 redis.conf
文件中进行了相应的设置, 即使从服务器正在进行初次同步, 服务器也可以使用旧版本的数据集来处理命令查询。
不过, 在从服务器删除旧版本数据集并载入新版本数据集的那段时间内, 连接请求会被阻塞。
你还可以配置从服务器, 让它在与主服务器之间的连接断开时, 向客户端发送一个错误。
复制功能可以单纯地用于数据冗余(data redundancy)
, 也可以通过让多个从服务器处理只读命令请求来提升扩展性(scalability)
: 比如说, 繁重的SORT
命令可以交给附属节点去运行。
可以通过复制功能来让主服务器免于执行持久化操作: 只要关闭主服务器的持久化功能, 然后由从服务器去执行持久化操作即可。
复制过程:
当建立一个从服务器时, 从服务器都将向主服务器发送一个
SYNC
命令。接到
SYNC
命令的主服务器将开始执行BGSAVE
, 并在保存操作执行期间, 将所有新执行的写入命令都保存到一个缓冲区里面。当
BGSAVE
执行完毕后, 主服务器将执行保存操作所得的.rdb
文件发送给从服务器, 从服务器接收这个.rdb
文件, 并将文件中的数据载入到内存中。之后主服务器会以
Redis
命令协议的格式, 将写命令缓冲区中积累的所有内容都发送给从服务器。你可以通过
telnet
命令来亲自验证这个同步过程: 首先连上一个正在处理命令请求的Redis
服务器, 然后向它发送SYNC
命令, 过一阵子, 你将看到telnet
会话(session)
接收到服务器发来的大段数据(.rdb
文件), 之后还会看到, 所有在服务器执行过的写命令, 都会重新发送到telnet
会话来。即使有多个从服务器同时向主服务器发送
SYNC
, 主服务器也只需执行一次BGSAVE
命令, 就可以处理所有这些从服务器的同步请求。从服务器可以在主从服务器之间的连接断开时进行自动重连, 在
Redis 2.8
版本之前, 断线之后重连的从服务器总要执行一次完整重同步(full resynchronization)
操作, 但是从Redis 2.8
版本开始, 从服务器可以根据主服务器的情况来选择执行完整重同步还是部分重同步(partial resynchronization)
。
部分重同步:
从 Redis 2.8 开始, 在网络连接短暂性失效之后, 主从服务器可以尝试继续执行原有的复制进程
(process)
, 而不一定要执行完整重同步操作。这个特性需要主服务器为被发送的复制流创建一个内存缓冲区
(in-memory backlog)
, 并且主服务器和所有从服务器之间都记录一个复制偏移量(replication offset)
和一个主服务器ID (master run id)
, 当出现网络连接断开时, 从服务器会重新连接, 并且向主服务器请求继续执行原来的复制进程:
- 如果从服务器记录的主服务器
ID
和当前要连接的主服务器的ID
相同, 并且从服务器记录的偏移量所指定的数据仍然保存在主服务器的复制流缓冲区里面, 那么主服务器会向从服务器发送断线时缺失的那部分数据, 然后复制工作可以继续执行。- 否则的话, 从服务器就要执行完整重同步操作。
Redis 2.8
的这个部分重同步特性会用到一个新增的PSYNC
内部命令, 而Redis 2.8
以前的旧版本只有SYNC
命令, 不过, 只要从服务器是Redis 2.8
或以上的版本, 它就会根据主服务器的版本来决定到底是使用PSYNC
还是SYNC
:
- 如果主服务器是
Redis 2.8
或以上版本,那么从服务器使用PSYNC
命令来进行同步。- 如果主服务器是
Redis 2.8
之前的版本,那么从服务器使用SYNC
命令来进行同步。
主从复制存在不能自动故障转移、达不到高可用的问题。哨兵模式解决了这些问题。通过哨兵机制可以自动切换主从节点。
客户端连接
Redis
的时候,先连接哨兵,哨兵会告诉客户端Redis主节点的地址,然后客户端连接上Redis
主节点并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。
Redis
的Sentinel
中关于下线(down)
有两个不同的概念:
(Subjectively Down, 简称 SDOWN)
指的是单个Sentinel
实例对服务器做出的下线判断。(Objectively Down, 简称 ODOWN)
指的是多个Sentinel
实例在对同一个服务器做出SDOWN
判断, 并且通过 SENTINEL is-master-down-by-addr
命令互相交流之后, 得出的服务器下线判断。 (一个Sentinel
可以通过向另一个Sentinel
发送 SENTINEL is-master-down-by-addr
命令来询问对方是否认为给定的服务器已下线。)每个Sentinel都需要定期执行的任务:
Sentinel
以每秒钟一次的频率向它所知的主服务器、从服务器以及其他Sentinel
实例发送一个PING
命令。(instance)
距离最后一次有效回复PING
命令的时间超过down-after-milliseconds
选项所指定的值, 那么这个实例会被Sentinel
标记为主观下线。 一个有效回复可以是: +PONG
、 -LOADING
或者 -MASTERDOWN
。Sentinel
要以每秒一次的频率确认主服务器的确进入了主观下线状态。Sentinel
(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。Sentinel
会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO
命令。 当一个主服务器被Sentinel
标记为客观下线时,Sentinel
向下线主服务器的所有从服务器发送INFO
命令的频率会从10
秒一次改为每秒一次。Sentinel
同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向Sentinel
的INFO
命令返回有效回复时, 主服务器的主观下线状态就会被移除。leader
,负责故障转移的工作。leader
会推选出某个表现良好的从节点成为新的主节点,然后通知其他从节点更新主节点信息。哨兵模式解决了主从复制不能自动故障转移、达不到高可用的问题,但还是存在主节点的写能力、容量受限于单机配置的问题。而
cluster
模式实现了Redis
的分布式存储,每个节点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题。
Redis cluster
集群节点最小配置6
个节点以上(3
主3
从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis cluster
采用虚拟槽分区,所有的键根据哈希函数映射到0~16383
个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。
哈希槽是如何映射到Redis
实例上的:
Redis
集群使用数据分片(sharding)
而非一致性哈希(consistency hashing)
来实现: 一个Redis
集群包含16384
个哈希槽(hash slot)
, 数据库中的每个键都属于这16384
个哈希槽的其中一个, 集群使用公式CRC16(key) % 16384
来计算键key
属于哪个槽, 其中CRC16(key)
语句用于计算键key
的CRC16
校验和 。集群中的每个节点负责处理一部分哈希槽。 举个例子, 一个集群可以有三个哈希槽, 其中:
- 节点 A 负责处理
0
号至5500
号哈希槽。- 节点 B 负责处理
5501
号至11000
号哈希槽。- 节点 C 负责处理
11001
号至16384
号哈希槽。这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。 比如说:
- 如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。
- 与此类似, 如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。
因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞, 所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线。
优点:
slot
存储分布在多个节点,节点间数据共享,可动态调整数据分布(failover)
,节点之间通过gossip
协议交换状态信息,用投票机制完成Slave
到Master
的角色转换。缺点:
(pipeline)
。key
在同一节点上的事务操作,当多个key
分布于不同的节点上时无法使用事务功能。key
作为数据分区的最小粒度,不能将一个很大的键值对象如hash
、list
等映射到不同的节点。Redis
可以支持到16
个数据库,集群模式下只能使用1
个数据库空间。key
时,如果发现key
已经过期,那么会将key
删除。key
,每次清理会依次遍历所有DB
,从db
随机取出20
个key
,如果过期就删除,如果其中有5
个key
过期,那么就继续对这个db
进行清理,否则开始清理下一个db
。Redis
有最大内存的限制,通过maxmemory
参数可以设置最大内存,当使用的内存超过了设置的最大内存,就要进行内存释放, 在进行内存释放的时候,会按照配置的淘汰策略清理内存当Redis
的内存超过最大允许的内存之后,Redis
会触发内存淘汰策略,删除一些不常用的数据,以保证Redis
服务器正常运行。
Redisv4.0前提供 6 种数据淘汰策略:
LRU
(Least Recently Used
),最近最少使用。利用LRU
算法移除设置了过期时间的key
key
Redisv4.0后增加以下两种:
LFU
,Least Frequently Used
,最不常用,从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。key
。内存淘汰策略可以通过配置文件来修改,相应的配置项是maxmemory-policy
,默认配置是noeviction
。
key
的当前值,如果没有超出限制再执行INCR
增1
,如果key
不存在,使用redis
的事务初始化key
和过期时间。setnx(或set nx)
命令实现先删除缓存再更新数据库
进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。
存在的问题:删除缓存数据之后,更新数据库完成之前,这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。
先更新数据库再删除缓存
进行更新操作时,先更新MySQL
,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。
存在的问题:更新MySQL
和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。
异步更新缓存
数据库的更新操作完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis
自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。
缓存穿透是指查询一个不存在的数据,由于缓存是不命中时被动写的,如果从DB
查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB
去查询,失去了缓存的意义。在流量大时,可能DB
就挂掉了。
bitmap
中,查询不存在的数据会被这个bitmap
拦截掉,从而避免了对DB
的查询压力。布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1
。查询时,将元素通过散列函数映射之后会得到k
个点,如果这些点有任何一个0
,则被检元素一定不在,直接返回;如果都是1
,则查询元素很可能存在,就会去查询Redis
和数据库。
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB
,DB
瞬时压力过重挂掉。
解决方法:在原有的失效时间基础上增加一个随机值,使得过期时间分散一些。
缓存击穿:大量的请求同时查询一个key
时,此时这个key
正好失效了,就会导致大量的请求都落到数据库。缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key。
解决方法:加分布式锁,第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms
然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。
Redis
客户端执行一条命令分4
个过程:发送命令、命令排队、命令执行、返回结果。使用pipeline
可以批量请求,批量返回结果,执行速度比逐条执行要快。
使用pipeline
组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline
命令完成。
原生批命令(mset和mget)
与pipeline
对比:
pipeline
是非原子性。pipeline
命令中途异常退出,之前执行成功的命令不会回滚。pipeline
支持多命令。Redis
通过LUA
脚本创建具有原子性的命令:当lua
脚本命令正在运行的时候,不会有其他脚本或 Redis
命令被执行,实现组合命令的原子操作
作用:
Lua脚本在Redis
中是原子执行的,执行过程中间不会插入其他命令。
Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
令完成。
原生批命令(mset和mget)
与pipeline
对比:
pipeline
是非原子性。pipeline
命令中途异常退出,之前执行成功的命令不会回滚。pipeline
支持多命令。