本文详细讲解了关于Redis的知识点。

概述

什么是Redis

Redis是基于C语言编写的、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

主要特性

整理了Redis的7种特性,有:

  • 速度快
    处理速度非常快,每秒能执行约11万集合,每秒约81000+记录。

    速度快的原因是:

    • 基于C语言实现,效率高;
    • 数据存储在内存中,读取速度快;(主要)
    • 单线程模型,避免线程上下文切换和竞态消耗;
    • 使用了多路I/O复用模型

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

注意:

  • 单线程:
    Redis内部使用文件事件处理器FileEventHandler,这个处理器是单线程的,所以Redis也被叫做单线程的模型。它采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来进行处理。

  • 非单线程:
    Redis内部也有许多多线程操作,如fysnc file descriptor和close file descritor操作时会有独立的线程来操作。
    持久化使用RDB时,手动使用bgsave命令触发时,会调用系统的fork函数创建子进程后台处理。即 Redis 使用操作系统的fork多进程 COW(Copy On Write) 机制来实现快照持久化。
    持久化AOF时的瘦身操作,fork子线程进行命令合并;

  • 可持久化
    支持持久化,即使机器宕机或断电也不会丢失数据。
    因为数据保存在内存中,对数据的更新将会异步的保存到磁盘上。

    主要有三种持久化方式:

    • 快照:一种半持久模式,不时的将数据集以异步的方式从内存以RDB格式写入硬盘;
    • AOF可追加文件:将数据集的修改操作追加记录;
    • 快照和AOF混合使用
  • 多种数据结构
    5种常见的数据结构:字符串(String)、散列哈希(Hash)、列表(List)、集合(Set)和有序集合(Sort Set)。
    其他的还有位图(BitMaps)、HyperLogLog(超小内存的唯一值计数)、GEO(地理信息定位)。

  • 功能丰富
    支持发布订阅、Lua脚本(原子性的操作)、事务、pipeline管道操作;

  • 主从复制
    支持主从同步,确保Master和Slave之间的数据同步。可以将数据复制到任意数量的从服务器,而从服务器也是可以关联其他从服务器的主服务器。

由于完全实现了发布订阅机制,使得从Slave在任何地方同步数据时,就可以订阅一个频道并接收Master完整的发布记录。

  • 高可用、分布式、集群模式
    支持集群模式,Sentinel哨兵机制支持高可用。

  • 支持多种编程语言,使用简单
    如Java、Python等热门语言,都提供了API可以使用。

应用场景

缓存系统

如JetCache中Local使用Caffeine,Remote使用Redis。

为什么用Redis做缓存?

主要是基于高性能高并发两个方面考虑,才使用缓存。
缓存分为本地缓存和远程缓存。本地缓存的特点是轻量且快速,生命周期随着JVM的销毁而结束,如果多实例或多机器的情况下,每个实例或机器都需要保存一份本地缓存,浪费空间而且不具备一致性。
远程缓存也可称为分布式缓存,如Redis或MemCached等,使用远程缓存多个实例或机器可共用一份缓存,能够保证一致性,但需要Redis自身保持高可用。

而Redis是单线程的,基于内存的数据库,支持持久化和高可用,数据结构丰富,多用于缓存系统。

Redis与MemCached的区别
  • Redis支持丰富的数据结构,而MemCached支持简单的String类型(新增了二进制类型);
  • Redis支持数据持久化,而MemCached是数据全部存在内存中;
  • Redis支持集群模式,而MemCached没有原生集群模式,需要依靠客户端来实现往集群中分片写入数据;
  • Redis使用单线程的IO多路复用模型。MemCached是多线程的,非阻塞IO复用的网络模型。

消息队列系统

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。

还可以基于List结构的消息队列:lpush + brpop = message queue 阻塞式先进先出

计数器

increment方法实现。

  • 1、计数器:记录用户个人主页的访问量
    可以使用
    1
    2
    3
    4
    5
    6
    incr userId:pageView
    ````
    单线程无竞争的,来记录每个用户每个页面的访问量
    - 2、计数器:记录网站每个用户某页的访问量
    ```redis
    hincrby user pageView count

排行榜功能

有序集合Zset里面的元素是唯一的,有序的,按分数从小到大排序。
如:

1
zadd key score1 element1 score2 element2 ...

score可以为:时间戳、销量、关注人数等

社交网络

如社交网络应用中的点赞数、粉丝数、关注数等。
可以将点赞用户存在set集合中,scard获取其大小。

实时系统过滤器

实现过滤功能,如布隆过滤器。

安装部署

四种安装方式

具体安装部署方式可参考官网或其他文章,比较简单。

  • 单机模式:
  • 主从模式:
  • Sentinel哨兵模式;
  • Cluster集群模式:

可执行文件

基于Redis 5.0.5版本:

  • redis-server 服务器
  • redis-cli 命令行客户端,连接服务端
  • redis-benchmark 基准和性能测试
  • redis-check-aof AOF文件修复工具
  • redis-check-rdb RDB文件修复工具
  • redis-sentinel 启动哨兵节点

启动

三种启动方式

  • 简单启动:
    直接执行redis-server (默认ip为127.0.0.1/localhost,port为6379)

  • 动态参数启动:

    1
    redis-server --port 6380
  • 配置文件启动(推荐):

    1
    redis-server redis.conf

验证

1
2
3
ps -ef | grep redis 查看pid进程;
netstat -antp | grep redis 查看端口是否Listening;
redis-cli -h ip -p port ping 客户端连接ping测试是否返回PONG

开机启动

1
systemctl enable redis.service

配置参数

线上修改配置

如果已经启动服务,修改配置需重启,影响线上服务;
可以使用客户端命令修改(不会修改配置文件,临时生效,重启后恢复原样)。
如下案例:

1
2
3
4
5
6
127.0.0.1:6379> CONFIG GET appendonly
1) "appendonly"
2) "no"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> CONFIG SET appendonly yes

配置文件详解

基于Redis 5.0.5版本:

网络 Network
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
################################## NETWORK #####################################
# ip绑定,默认127.0.0.1。多个逗号分割
# 注意:0.0.0.0 仅在测试环境下,生成环境需绑定具体的ip
bind 127.0.0.1

# 是否开启保护模式,默认yes。
# 要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问
protected-mode yes

# redis对外端口,默认6379,当单机多实例时需指定不同端口
port 6379

# 确定了TCP连接中已完成队列(完成三次握手之后)的长度。
# 当然此值必须不大于Linux系统定义的/proc/sys/net/core/somaxconn值,默认是511,而Linux的默认参数值是128。
# 当系统并发量大并且客户端速度缓慢的时候,可以将这二个参数一起参考设定。
# 该内核参数默认值一般是128,对于负载很大的服务程序来说大大的不够。一般会将它修改为2048或者更大。
# 在/etc/sysctl.conf中添加:net.core.somaxconn = 2048,然后在终端中执行sysctl -p。
tcp-backlog 511

# 设置客户端空闲超过timeout,服务端会断开连接,为0则服务端不会主动断开连接,不能小于0。
timeout 0

# 如果设置不为0,就使用配置tcp的SO_KEEPALIVE值。
# 使用keepalive有两个好处:
# - 检测挂掉的对端。
# - 降低中间设备出问题而导致网络看似连接却已经与对端端口的问题。
# 在Linux内核中,设置了keepalive,redis会定时给对端发送ack。检测到对端关闭需要两倍的设置值。
tcp-keepalive 300
通用配置 Gennral
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
################################# GENERAL #####################################

# 是否以守护进程(no/yes)的方式启动,
# 默认为no,不是作为守护进程运行的,
# 如果你想让它在后台运行,你就把它改成 yes。
# 当redis作为守护进程运行的时候,它会写一个 pid 到 /var/run/redis.pid 文件里面
daemonize no

# 可以通过upstart和systemd管理Redis守护进程:
# - supervised no - 没有监督互动;
# - supervised upstart - 通过将Redis置于SIGSTOP模式来启动信号;
# - supervised systemd - signal systemd将READY = 1写入$ NOTIFY_SOCKET;
# - supervised auto - 检测upstart或systemd方法基于 UPSTART_JOB或NOTIFY_SOCKET环境变量;
supervised no

# 配置PID文件路径,当redis作为守护进程运行的时候,它会把 pid 默认写到 /var/redis/run/redis_6379.pid 文件里面
pidfile /var/run/redis_6379.pid

# 定义日志级别:
# - debug(记录大量日志信息,适用于开发、测试阶段)
# - verbose(较多日志信息)
# - notice(适量日志信息,使用于生产环境)
# - warning(仅有部分重要、关键信息才会被记录)
loglevel notice

# Redis系统日志,文件名。
# 日志文件的位置,当指定为空字符串时,为标准输出;
# 如果redis已守护进程模式运行,那么日志将会输出到/dev/null
logfile ""

# 设置数据库的数目。默认的数据库是DB 0 。
# 可以在每个连接上使用select <dbid> 命令选择一个不同的数据库。
databases 16

# 是否总是显示logo
always-show-logo yes
快照 SnapShotting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
################################ SNAPSHOTTING  ################################

# 存 DB 到磁盘:格式:save <间隔时间(秒)> <写入次数>
# 根据给定的时间间隔和写入次数将数据保存到磁盘。
# 下面的例子的意思是:
# - 900 秒内如果至少有 1 个 key 的值变化,则保存
# - 300 秒内如果至少有 10 个 key 的值变化,则保存
# - 60 秒内如果至少有 10000 个 key 的值变化,则保存
# 注意:你可以注释掉所有的 save 行来停用保存功能。
# 也可以直接一个空字符串来实现停用:save ""
save 900 1
save 300 10
save 60 10000

# 如果用户开启了RDB快照功能,那么在redis持久化数据到磁盘时如果出现失败,默认情况下,redis会停止接受所有的写请求。
# 可以让用户很明确的知道内存中的数据和磁盘上的数据已经存在不一致了。
# 如果redis不顾这种不一致,一意孤行的继续接收写请求,就可能会引起一些灾难性的后果。
# 如果下一次RDB持久化成功,redis会自动恢复接受写请求。
# 如果不在乎这种数据不一致或者有其他的手段发现和控制这种不一致的话,可以关闭这个功能,
# 以便在快照写入失败时,也能确保redis继续接受新的写请求。
stop-writes-on-bgsave-error yes

# 对于存储到磁盘中的快照,可以设置是否进行压缩存储。
# 如果是的话,redis会采用LZF算法进行压缩。
# 如果你不想消耗CPU来进行压缩的话, 可以设置为关闭此功能,但是存储在磁盘上的快照会比较大
rdbcompression yes

# 在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,
# 如果希望获取到最大的性能提升,可以关闭此功能。
rdbchecksum yes

# 设置快照的文件名
dbfilename dump.rdb

# Redis工作目录, 设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名
dir ./
主从复制 Replication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
################################# REPLICATION #################################

# 当设置本机为从服务器时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
# replicaof <masterip> <masterport>

# 当master服务设置了密码保护时,从服务器连接master的密码
# masterauth <master-password>

# 当主从连接中断,或者主从复制建立期间,是否允许从服务器对外提供服务。
# 默认为yes,即允许对外提供服务,但有可能读到脏数据。
# slave 可能会有两种表现:
# 1) 如果为 yes ,slave 仍然会应答客户端请求,但返回的数据可能是过时, 或者数据可能是空的在第一次同步的时候
# 2) 如果为 no ,在你执行除了 info he salveof 之外的其他命令时,slave 都将返回一个 "SYNC with master in progress" 的错误
replica-serve-stale-data yes

# 配置一个 slave 实体是否接受写入操作。
# 通过写入操作来存储一些短暂的数据对于一个 slave 实例来说可能是有用的,
# 因为相对从 master 重新同步数而言,把数据写入到 slave 会更容易被删除。
# 但是如果客户端因为一个错误的配置写入,也可能会导致一些问题。
# 从 redis 2.6 版起,默认 slaves 都是只读的。
replica-read-only yes

# 主从数据复制是否使用无硬盘复制功能。即是否使用socket方式复制数据。
# 目前redis复制提供两种方式,disk和socket。
# 如果新的slave连上来或者重连的slave无法部分同步,就会执行全量同步,master会生成rdb文件。
# 有2种方式:
# - disk方式是master创建一个新的进程把rdb文件保存到磁盘,再把磁盘上的rdb文件传递给slave。
# - socket是master创建一个新的进程,直接把rdb文件以socket的方式发给slave。
# disk方式的时候,当一个rdb保存的过程中,多个slave都能共享这个rdb文件。
# socket的方式就的一个个slave顺序复制。
# 在磁盘速度缓慢,网速快的情况下推荐用socket方式。
repl-diskless-sync no

# 当使用socket复制数据启用的时候,socket复制的延迟时间,如果设置成0表示禁用,默认值是5s。
repl-diskless-sync-delay 5

# 从节点根据指定的时间间隔向主节点发起ping请求
# repl-ping-replica-period 10

# 复制连接超时时间。
# 需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时
# repl-timeout 60

# 是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。默认是no,即使用tcp nodelay。
# 如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。但是这也可能带来数据的延迟。
# 默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes。
repl-disable-tcp-nodelay no

# 复制缓冲区大小, 默认是1mb
# 当从节点在一段时间内断开连接时,主节点会收集数据到backlog这个缓冲区,
# 因此当一个从节点想要重新连接时,通常不需要完全的重新同步,但是部分的重新同步就足够了,只是通过在断开连接的时候传递数据的一部分。
# repl-backlog-size 1mb

# 单位s。当主节点不再联系从节点,则释放backlog(内存)
# repl-backlog-ttl 3600

# 从节点优先级。
# 当master不可用,Sentinel会根据slave的优先级选举一个master。
# 最低的优先级的slave,当选master。
# 而配置成0,永远不会被选举
replica-priority 100

# 当健康的slave的个数小于N,mater就禁止写入
# min-replicas-to-write 3
# 延迟小于min-slaves-max-lag秒的slave才认为是健康的slave
# min-replicas-max-lag 10
安全 Security
1
2
3
4
5
6
7
8
9
10
11
12
13
################################## SECURITY ###################################

# requirepass配置可以让用户使用AUTH命令来认证密码,才能使用其他命令。
# 这让redis可以使用在不受信任的网络中。为了保持向后的兼容性,可以注释该命令,因为大部分用户也不需要认证。
# 使用requirepass的时候需要注意,因为redis太快了,每秒可以认证15w次密码,简单的密码很容易被攻破,所以最好使用一个更复杂的密码。注意只有密码没有用户名。
# requirepass foobared

# 把危险的命令给修改成其他名称。
# 比如CONFIG命令可以重命名为一个很难被猜到的命令,这样用户不能使用,而内部工具还能接着使用。
# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52

# 设置成一个空的值,可以禁止一个命令
# rename-command CONFIG ""
客户端 Clients
1
2
3
4
5
6
7
################################### CLIENTS ####################################

# 设置能连上redis的最大客户端连接数量。
# 默认是10000个客户端连接。
# 由于redis不区分连接是客户端连接还是内部打开文件或者和slave连接等,所以maxclients最小建议设置到32。
# 如果超过了maxclients,redis会给新的连接发送max number of clients reached,并关闭连接。
# maxclients 10000
内存管理 Memory Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
############################## MEMORY MANAGEMENT ################################

# redis配置的最大内存容量。
# 当内存满了,需要配合maxmemory-policy策略进行处理。
# 注意slave的输出缓冲区是不计算在maxmemory内的。
# 所以为了防止主机内存使用完,建议设置的maxmemory需要更小一些。
# maxmemory <bytes>

# 驱逐策略:内存容量超过maxmemory后的处理策略。
# LRU算法只是预测最近被访问的数据将来最有可能被访问到。我们可以转变思路,采用一种LFU(Least Frequently Used)算法,也就是最频繁被访问的数据将来最有可能被访问到。

# - volatile-lru:利用LRU算法移除设置过过期时间的key。
## LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
# - volatile-random:随机移除设置过过期时间的key。
# - volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL)
# - allkeys-lru:利用LRU算法移除任何key。
# - allkeys-random:随机移除任何key。
# - noeviction:不移除任何key,只是返回一个写错误。
# - volatile-lfu:从已经设置过期时间的数据中,挑选最不经常使用的数据淘汰。
# - allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
# maxmemory-policy noeviction

# lru检测的样本数。
# 使用lru或者ttl淘汰算法,从需要淘汰的列表中随机选择sample个key,选出闲置时间最长的key移除。
# maxmemory-samples 5

# 从 Redis 5 开始,默认情况下,replica 节点会忽略 maxmemory 设置(除非在发生 failover 后,此节点被提升为 master 节点)。
# 这意味着只有 master 才会执行过期删除策略,并且 master 在删除键之后会对 replica 发送 DEL 命令。
# replica-ignore-maxmemory yes
惰性/异步删除 LazyFree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
############################# LAZY FREEING ####################################

# Redis 有两种方式删除键。
# - 一种是使用如 DEL 这样的命令进行的同步删除。
# 同步删除意味着删除过程中服务端会停止处理新进来的命令。
# 若要删除的 key 关联了一个小的 object 删除耗时会很短。
# 若要删除的 key 管理了一个很大的 object,比如此对象有上百万个元素,服务端会阻塞相同长一段时间(甚至超过一秒)。

# - Redis 5.0.5 同时提供了一种非阻塞的方式用于删除。
# 比如 UNLINK(非阻塞的 DEL)以及用于 FLUSHALL 和 FLUSHDB 的 ASYNC 选项,这些命令能在后台回收内存。
# 这些命令能在常数时间内执行完毕。其他线程会在后台尽快回收内存。
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
AOF持久化 Append Only Mode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
############################## APPEND ONLY MODE ###############################

# 默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。
# 但是redis如果中途宕机,会导致可能有几分钟的数据丢失。
# Append Only File是另一种持久化方式,可以提供更好的持久化特性。
# Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件,每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。

# 禁用了appendonly功能,这样的风险是一旦redis实例crash,重启后只能恢复到最近1次快照(即bgsave产生的rdb文件),可能会丢失很长时间的数据。
# appendonly可以实现准实时刷盘,默认每1s将数据追加到磁盘文件,也可以配置成每次修改都刷盘,当redis crash时最大限度的保证数据完整性。
appendonly no

# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"

# aof持久化策略的配置:
# - no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
# - always表示每次写入都执行fsync,以保证数据同步到磁盘。
# - everysec表示每秒执行一次fsync,可能会导致丢失这1s数据
# appendfsync always
appendfsync everysec
# appendfsync no

# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间。
# 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,
# 默认为no,建议yes。Linux的默认fsync策略是30秒。可能丢失30秒数据。
# 如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。
no-appendfsync-on-rewrite no

# aof自动重写配置。
# 当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写。
# 即当aof文件增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。
# 当前AOF文件大小是上次日志重写得到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
# 设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写
auto-aof-rewrite-min-size 64mb

# 是否redis在启动时可以加载被截断的AOF文件
aof-load-truncated yes

# 混合持久化:RDB快照和AOF。
# 具体参考内部原理中的持久化之混合持久化
aof-use-rdb-preamble yes
Lua脚本 LUA SCRIPTING
1
2
3
4
5
################################ LUA SCRIPTING  ###############################

# 设置lua脚本的最大运行时间,单位为毫秒
# 如果此值设置为0或负数,则既不会有报错也不会有时间限制。
lua-time-limit 5000
集群 Redis Cluster
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
################################ REDIS CLUSTER  ###############################

# 集群开关,默认是注释掉掉,不开启集群模式
# cluster-enabled yes

# 集群配置文件的名称,每个节点都有一个集群相关的配置文件,持久化保存集群的信息。
# 这个文件并不需要手动配置,这个配置文件有Redis生成并更新,每个Redis集群节点需要一个单独的配置文件,请确保与实例运行的系统中配置文件名称不冲突
# cluster-config-file nodes-6379.conf

# 节点互连超时的阀值,集群节点超时毫秒数
# cluster-node-timeout 15000

# 在进行故障转移的时候,全部slave都会请求申请为master,但是有些slave可能与master断开连接一段时间了,导致数据过于陈旧,这样的slave不应该被提升为master。
# 该参数就是用来判断slave节点与master断线的时间是否过长。
# 判断方法是:
# 比较slave断开连接的时间和(node-timeout * slave-validity-factor) + repl-ping-slave-period
# 如果节点超时时间为三十秒, 并且slave-validity-factor为10,
# 假设默认的repl-ping-slave-period是10秒,即如果超过310秒slave将不会尝试进行故障转移
# cluster-replica-validity-factor 10

# master的slave数量大于该值,slave才能迁移到其他孤立master上。
# 如这个参数若被设为2,那么只有当一个主节点拥有2 个可工作的从节点时,它的一个从节点会尝试迁移。
# cluster-migration-barrier 1

# 默认情况下,集群全部的slot由节点负责,集群状态才为ok,才能提供服务。
# 设置为no,可以在slot没有全部分配的时候提供服务。
# 不建议打开该配置,这样会造成分区的时候,小分区的master一直在接受写请求,而造成很长时间数据不一致
# cluster-require-full-coverage yes

# 控制 master 发生故障时是否自动进行 failover。
# 当设置为 yes 后 master 发生故障时不会自动进行 failover,这时你可以进行手动的 failover 操作。
# cluster-replica-no-failover no
慢查询 SlowLog
1
2
3
4
5
6
7
8
9
10
11
12
################################## SLOW LOG ###################################
# (慢查询就是在日志中记录运行比较慢的SQL语句)

# slog log是用来记录redis运行中执行比较慢的命令耗时。
# 当命令的执行超过了指定时间,就记录在slow log中,slog log保存在内存中,所以没有IO操作。
# 执行时间比slowlog-log-slower-than大的请求记录到slowlog里面,单位是微秒,所以1000000就是1秒。
# 注意,负数时间会禁用慢查询日志,而0则会强制记录所有命令。
slowlog-log-slower-than 10000

# 慢查询日志长度。当一个新的命令被写进日志的时候,最老的那个记录会被删掉,这个长度没有限制。
# 只要有足够的内存就行,你可以通过 SLOWLOG RESET 来释放内存
slowlog-max-len 128
延迟监控 Latency Monitor
1
2
3
4
5
6
################################ LATENCY MONITOR ##############################

# 用来监控redis中执行比较缓慢的一些操作,用LATENCY打印redis实例在跑命令时的耗时图表。只记录大于等于下边设置的值的操作。
# 0的话,就是关闭监视。
# 默认延迟监控功能是关闭的,如果你需要打开,也可以通过CONFIG SET命令动态设置。
latency-monitor-threshold 0
订阅通知 Event Notification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
############################# EVENT NOTIFICATION ##############################

# Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
# Redis 客户端可以订阅任意数量的频道。
# 因为开启键空间通知功能需要消耗一些 CPU,所以在默认配置下,该功能处于关闭状态。

# > 注意:因为 Redis 目前的订阅与发布功能采取的是发送即忘(fire and forget)策略,所以如果你的程序需要可靠事件通知(reliable notification of events),
# > 么目前的键空间通知可能并不适合你:当订阅事件的客户端断线时,它会丢失所有在断线期间分发给它的事件。

# 对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件:键空间通知(key-space)和键事件通知(key-event)。
# > 当 del mykey 命令执行时:
# > - 键空间频道的订阅者将接收到被执行的事件的名字,在这个例子中,就是 del
# > - 键事件频道的订阅者将接收到被执行事件的键的名字,在这个例子中,就是 mykey

# 参数可以是以下字符的任意组合, 它指定了服务器该发送哪些类型的通知。
# 输入的参数中至少要有一个 K 或者 E,否则的话,不管其余的参数是什么,都不会有任何通知被分发。
# - K: 键空间通知,所有通知以 __keyspace@__ 为前缀
# - E: 键事件通知,所有通知以 __keyevent@__ 为前缀
# - g: DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知
# - $: 字符串命令的通知
# - l: 列表命令的通知
# - s: 集合命令的通知
# - h: 哈希命令的通知
# - z: 有序集合命令的通知
# - x: 过期事件:每当有过期键被删除时发送
# - e: 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
# - A: 参数 g$lshzxe 的别名
# 可参考:[Redis事件通知](https://www.cnblogs.com/tangxuliang/p/10659439.html)
# 如: notify-keyspace-events "Ex" 表示对过期事件进行通知发送;
# notify-keyspace-events "kx" 表示想监控某个 key 的失效事件。
# 将参数设为字符串 AKE 表示发送所有类型的通知。
notify-keyspace-events ""
高级配置 Advance Config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
############################# EVENT NOTIFICATION ##############################

# Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
# Redis 客户端可以订阅任意数量的频道。
# 因为开启键空间通知功能需要消耗一些 CPU,所以在默认配置下,该功能处于关闭状态。

# > 注意:因为 Redis 目前的订阅与发布功能采取的是发送即忘(fire and forget)策略,所以如果你的程序需要可靠事件通知(reliable notification of events),
# > 么目前的键空间通知可能并不适合你:当订阅事件的客户端断线时,它会丢失所有在断线期间分发给它的事件。

# 对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件:键空间通知(key-space)和键事件通知(key-event)。
# > 当 del mykey 命令执行时:
# > - 键空间频道的订阅者将接收到被执行的事件的名字,在这个例子中,就是 del
# > - 键事件频道的订阅者将接收到被执行事件的键的名字,在这个例子中,就是 mykey

# 参数可以是以下字符的任意组合, 它指定了服务器该发送哪些类型的通知。
# 输入的参数中至少要有一个 K 或者 E,否则的话,不管其余的参数是什么,都不会有任何通知被分发。
# - K: 键空间通知,所有通知以 __keyspace@__ 为前缀
# - E: 键事件通知,所有通知以 __keyevent@__ 为前缀
# - g: DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知
# - $: 字符串命令的通知
# - l: 列表命令的通知
# - s: 集合命令的通知
# - h: 哈希命令的通知
# - z: 有序集合命令的通知
# - x: 过期事件:每当有过期键被删除时发送
# - e: 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
# - A: 参数 g$lshzxe 的别名
# 可参考:[Redis事件通知](https://www.cnblogs.com/tangxuliang/p/10659439.html)
# 如: notify-keyspace-events "Ex" 表示对过期事件进行通知发送;
# notify-keyspace-events "kx" 表示想监控某个 key 的失效事件。
# 将参数设为字符串 AKE 表示发送所有类型的通知。
notify-keyspace-events ""

############################### ADVANCED CONFIG ###############################

# 数据量小于等于hash-max-ziplist-entries的用ziplist,大于hash-max-ziplist-entries用hash
# hash类型的数据结构在编码上可以使用ziplist和hashtable。
# ziplist的特点就是文件存储(以及内存存储)所需的空间较小,在内容较小时,性能和hashtable几乎一样。
# 因此redis对hash类型默认采取ziplist。如果hash中条目的条目个数或者value长度达到阀值,将会被重构为hashtable。
# 这个参数指的是ziplist中允许存储的最大条目个数,默认为512,建议为128
hash-max-ziplist-entries 512
# ziplist中允许条目value值最大字节数,默认为64,建议为1024
hash-max-ziplist-value 64

# 当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。
# 比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。
# 当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每个值含义如下:
# -5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
# -4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
# -3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
# -2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
# -1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
list-max-ziplist-size -2

# 表示一个quicklist两端不被压缩的节点个数。
# 注:这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。
# 实际上,一个quicklist节点上的ziplist,如果被压缩,就是整体被压缩的。
# 参数list-compress-depth的取值含义如下:
# 0: 是个特殊值,表示都不压缩。这是Redis的默认值。
# 1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
# 2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
# 3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
# 依此类推…
# 由于0是个特殊值,很容易看出quicklist的头节点和尾节点总是不被压缩的,以便于在表的两端进行快速存取。
list-compress-depth 0

# 数据量小于等于set-max-intset-entries用intset,大于set-max-intset-entries用set
set-max-intset-entries 512

# 数据量小于等于zset-max-ziplist-entries用ziplist,大于zset-max-ziplist-entries用zset
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

# value大小小于等于hll-sparse-max-bytes使用稀疏数据结构(sparse)
# 大于hll-sparse-max-bytes使用稠密的数据结构(dense),一个比16000大的value是几乎没用的,
# 建议的value大概为3000。如果对CPU要求不高,对空间要求较高的,建议设置到10000左右
hll-sparse-max-bytes 3000

# Streams macro node max size / items. The stream data structure is a radix
# tree of big nodes that encode multiple items inside. Using this configuration
# it is possible to configure how big a single node can be in bytes, and the
# maximum number of items it may contain before switching to a new node when
# appending new stream entries. If any of the following settings are set to
# zero, the limit is ignored, so for instance it is possible to set just a
# max entires limit by setting max-bytes to 0 and max-entries to the desired
# value.
stream-node-max-bytes 4096
stream-node-max-entries 100

# Redis将在每100毫秒时使用1毫秒的CPU时间来对redis的hash表进行重新hash,可以降低内存的使用。
# 当你的使用场景中,有非常严格的实时性需要,不能够接受Redis时不时的对请求有2毫秒的延迟的话,把这项配置为no。
# 如果没有这么严格的实时性要求,可以设置为yes,以便能够尽可能快的释放内存
activerehashing yes

# 对客户端输出缓冲进行限制可以强迫那些不从服务器读取数据的客户端断开连接,用来强制关闭传输缓慢的客户端。
# 对于normal client,第一个0表示取消hard limit,第二个0和第三个0表示取消soft limit,normal client默认取消限制,因为如果没有寻问,他们是不会接收数据的
# 对于slave client和MONITER client,如果client-output-buffer一旦超过256mb,又或者超过64mb持续60秒,那么服务器就会立即断开客户端连接。
# 对于pubsub client,如果client-output-buffer一旦超过32mb,又或者超过8mb持续60秒,那么服务器就会立即断开客户端连接。
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

# Client query buffers accumulate new commands. They are limited to a fixed
# amount by default in order to avoid that a protocol desynchronization (for
# instance due to a bug in the client) will lead to unbound memory usage in
# the query buffer. However you can configure it here if you have very special
# needs, such us huge multi/exec requests or alike.
#
# client-query-buffer-limit 1gb

# In the Redis protocol, bulk requests, that are, elements representing single
# strings, are normally limited ot 512 mb. However you can change this limit
# here.
#
# proto-max-bulk-len 512mb

# redis执行任务的频率为1s除以hz
hz 10

# 可选值为yes和no,分别代表开启动态hz和关闭动态hz。默认值为yes。
# 当动态hz开启时,您设置的hz参数的值,即configured_hz,将作为基线值,
# 而Redis服务中的实际hz值会在基线值的基础上根据已连接到Redis的客户端数量自动调整,
# 连接的客户端越多,实际hz值越高,Redis执行定期任务的频率就越高。
dynamic-hz yes

# 在aof重写的时候,如果打开了aof-rewrite-incremental-fsync开关,系统会每32MB执行一次fsync。
# 这对于把文件写入磁盘是有帮助的,可以避免过大的延迟峰值
aof-rewrite-incremental-fsync yes

# RDB自动触发策略是否启用,默认为yes
# RDB手动触发和自动触发:
# 1)自动触发:
# 如上面配置所示,按配置情况触发
# 2)手动触发:
# 连接redis后使用命令save、bgsave触发
# - save:会阻塞redis服务器,直到完成持久化
# - bgsave:会fork一个子进程,由子进程进行持久化。
rdb-save-incremental-fsync yes

# 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。
# lfu-log-factor 10
# 是一个以分钟为单位的数值,可以调整counter的减少速度
# lfu-decay-time 1

API相关

通用命令

除了Keys命令的时间复杂度为O(n)其他都为O(1)

  • keys pattern 列出符合pattern的key
    因为它是一个O(n)的操作且是单线程操作会阻塞其他命令。一般不在生产环境使用。

    建议使用方案:

    • 可以在热本从节点时,在从节点执行比较重的命令;
    • scan命令:以非阻塞的方式实现key值的查找,绝大多数情况下是可以替代keys命令的,可选性更强
  • SCAN cursor [MATCH pattern] [COUNT count]
    是一个基于游标的迭代器,count默认为10。
    这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程。

    scan 参数提供了三个参数:

    • 第一个是 cursor 整数值;
    • 第二个是 key 的正则模式;
    • 第三个是遍历的 limit hint。

    第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

  • dbsize 返回当前数据库的 key 的数量
    可以线上使用,因为redis有计数器会实时记录key的总数,时间复杂度为O(1)

  • exists key 是否存在key,存在1 不存在0

  • del key... 删除key,可以多个

  • expire key seconds 过期时间设置,key在n秒后过期

  • ttl key 查看key剩余的过期时间,返回负数-2说明key已经被删除

  • persist key 去掉key的过期时间,这时在执行ttl会返回-1,说明key存在,并没有过期时间

  • type key 返回 key 所储存的值的类型
    一般为string、hash、list、set、zset、none(key不存在)

  • select db 选择db,共16个db,默认0

  • MOVE key db 将当前数据库的 key 移动到给定的数据库 db 当中。

  • RENAME key newkey 修改 key 的名称

  • RENAMENX key newkey 仅当 newkey 不存在时,将 key 改名为 newkey 。

数据结构

5种常见类型

String:字符串类型

结构:key value。
Value的类型可以为字符、数值型、二进制、Json串。

注意:字符串类型的Value的大小不能大于512M;

内部编码
  • int

8个字节的长整型

如果一个字符串的内容可以转换为long,那么该字符串就会被转换成long类型,对象的ptr就会指向该long,并且对象类型也用int类型表示;

  • embstr

小于等于39个字节的字符串
如果字符串对象的长度小于39字节,就用embstr对象,否则使用传统的raw对象。

  • raw

大于39个字节的字符串

常见命令
  • get key O(1)操作

  • del key O(1)操作

  • set操作:O(1)操作
    set key value 不管key是否存在都设置
    setnx key value 当key不存在才设置
    set key value XX key存在才设置
    SETEX key seconds value 将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。如果 key 已经存在, SETEX命令将覆写旧值。
    PSETEX key milliseconds value 这个命令和 SETEX命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX命令那样,以秒为单位。
    set key value [expiration EX seconds|PX milliseconds] [NX|XX]

    • EX seconds : 将键的过期时间设置为 seconds 秒。
      执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value。
    • PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。
      执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
    • NX : 只在键不存在时, 才对键进行设置操作。
      执行 SET key value NX 的效果等同于执行 SETNX key value
    • XX : 只在键已经存在时, 才对键进行设置操作。
  • 批量操作:O(n)操作
    mget key1 key2 ...
    批量获取key,原子操作。

    n次get = n次网络时间 + n次命令时间
    (一般是网络时间比较耗时)

mset key1 v1 key2 v2 ...
批量设置key。

1次mget = 1次网络时间 + n次命令时间

MSETNX key value [key value ...]
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
即使只有一个给定 key 已存在, MSETNX也会拒绝执行所有给定 key 的设置操作。

  • O(N)操作, N 为要设置的 key 的数量。
  • 当所有 key 都成功设置,返回 1 。
    如果所有给定 key 都设置失败(至少有一个 key 已经存在),那么返回 0 。
  • 自增/减命令
    incr key
    O(1)操作。计数,key自增1,如key不存在,自增后get(key)=1
    decr key
    O(1)操作。key自减1,如key不存在,自减后get(key)=-1
    incrby key k
    O(1)操作。key自增k,如key不存在,自增后get(key)=k
    decr key k
    O(1)操作。key自减k,如key不存在,自减后get(key)=-k

  • 其他操作:
    getset key newValue
    O(1)操作,设置key的新值,返回旧值。
    append key value
    O(1)操作,将value追加到旧的value。
    strlen key
    O(1)操作,返回字符串的长度。
    incrbyfloat key 3.5
    O(1)操作,增加key对应的值为3.5
    getrange key start end
    O(1)操作,获取字符串下指定下标的值
    setrange key index value
    O(1)操作,设置指定下标所对应的值。

应用

1、计数器:记录用户个人主页的访问量

可以使用 incr userId:pageView (单线程无竞争的),来记录每个用户每个页面的访问量

Hash:哈希类型

结构:key field value。
即类似于Map结构。

内部编码
  • ziplist压缩链表
    当元素个数小于512 , 并且值的大小小于64个字节时 , 采用ziplist。

    • 当哈希类型中元素个数小于 hash-max-ziplist-entries 配置(默认 512 个),同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,
      Redis 会使用 ziplist 作为哈希的内部实现。
    • ziplist是一种压缩链表,它的好处是更能节省内存空间
      因为它所存储的内容都是在连续的内存区域当中的。
      当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。
      但当数据量过大时就ziplist就不是那么好用了。
      因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc
    • ziplist最大的优势就是存储的时候是连续的内存,可以极大的提升cpu的缓存命中率
  • hashtable哈希表
    当元素个数小于512 , 并且值的大小小于64个字节时 , 采用ziplist , 大于的时候采用hashtable。

常见命令
  • 常用命令:O(1)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    hget key field 获取hashkey对应的field的value
    hset key field value
    hdel key field
    hexists key field
    hlen key 获取hash key的field的数量

    hsetnx key field value 不存在设置,否则失败
    hincrby key field intCounter
    hincrbyfloat key field floatCounter
  • 批量操作:O(n)

    1
    2
    hmget key1 field1 field2...
    hmset key field1 value1 field2 value2...
  • 其他:O(n)

    1
    2
    3
    hgetall key  返回key下所有的field和value。由于单线程,要小心使用此命令,尽量用hmget代替
    hkeys key 返回key对应的所有field的field
    hvals key 返回key对应的所有field的value
应用
  • 1、计数器:记录网站每个用户某页的访问量
    1
    hincrby user:info pageView count
  • 2、缓存:缓存视频的基本信息
List:列表类型

结构:key elements。
有序的(插入顺序)、可重复的、可以左右两边插入弹出的。

内部编码
  • linklist双向链表

    linkedlist是一种双向链表。
    它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。
    当每增加一个node的时候,就需要重新malloc一块内存。

  • ziplist 压缩列表

常见命令
  • 增:

    1
    2
    3
    4
    5
    6
    7
    8
    rpush key value1 value2 ...
    O(1~n),从列表的右端插入

    lpush key value1 value2 ...
    O(1~n),从列表的左端插入

    linsert key before|after value newValue
    O(n),在list指定的value之前或后插入newValue
  • 删:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    lpop key
    O(1),从左弹出list中的一个元素

    rpop key
    O(1),从右弹出list中的一个元素

    lrem key count value
    O(n),根据count值,从list中删除所有等于value的元素
    count > 0:从左到右,删除最多count个。
    count < 0:从右到左,删除最多Math.abs(count)个。
    count = 0:删除所有满足条件的元素。

    ltrim key start end
    O(n),按照索引位置修剪list。保留范围內的元素

    blpop key timeout 和brpop key timeout
    O(1),阻塞删除,timeout是阻塞超时时间,为0表示永远不阻塞
  • 改:

    1
    2
    lset key index newValue
    O(n),修改指定位置的值为newValue
  • 查:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    lrange key start end (包含end)
    O(n),获取列表指定索引范围的元素。
    如:list有6个元素。
    索引从左:0~5;
    索引从右:-1~-6;

    lindex key index
    O(n),获取列表指定索引的元素

    llen key
    O(1),获取list长度
应用
  • 1、微博时间轴TimeLine:将关注用户的微博由新到旧排列。
    关注的人更新微博,使用lpush左侧即头部插入;
    使用lrang可以分页查询;

  • 2、实现栈:lpush + lpop = stack 先进后出

  • 3、实现队列:lpush + rpop = queue 先进先出

  • 4、实现有固定数量的列表:lpush + ltrim = capped collection

  • 5、消息队列:lpush + brpop = message queue 阻塞式先进先出

Set:集合类型

结构:key values。
元素是无序的、无重复元素、支持集合间的操作,如交/并/差集,

内部编码
  • intset
    intset是一个整数集合,里面存的为某种同一类型的整数。

    1
    2
    3
    #define INTSET_ENC_INT16 (sizeof(int16_t)) 
    #define INTSET_ENC_INT32 (sizeof(int32_t))
    #define INTSET_ENC_INT64 (sizeof(int64_t))

    当集合中的元素都是整数,并且集合中的元素个数小于 512 个时,Redis 会选用 intset 作为底层内部实现。

    intset是一个有序集合,查找元素的复杂度为O(logN),但插入时不一定为O(logN),因为有可能涉及到升级操作。
    比如当集合里全是int16_t型的整数,这时要插入一个int32_t,那么为了维持集合中数据类型的一致,
    那么所有的数据都会被转换成int32_t类型,涉及到内存的重新分配,这时插入的复杂度就为O(N)了。但是intset不支持降级操作。

  • hashtable 哈希表

常见命令
  • 集合内操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    sadd element
    O(1),添加元素,如存在则失败

    srem key element
    O(1) 删除

    scard key
    计算集合大小

    sismember key value
    判断vlaue是否在key中

    srandmember key count
    随机挑count个元素。不会破坏集合的数据

    spop key
    从集合中随机弹出一个元素

    smembers key
    获取集合所有元素,返回结果无序
    会造成阻塞,需注意使用,建议使用游标scan
  • 集合间操作:
    1
    2
    3
    4
    5
    sdiff key1 key2  求差集
    sinter key1 key2 求交集
    sunion key1 key2 求并集
    sdiff/sinter/suion + store destkey
    将差/交/并集结果保存到destkey中
应用
  • 1、微博抽奖系统:使用spop或srandmember随机选择一个或多个用户
  • 2、微博点赞、转发等:将点赞用户存在集合中,scard获取其大小
  • 3、标签:给用户添加标签/给标签添加用户
  • 4、共同关注/共同好友等功能:求交集
ZSet:有序集合类型

结构:key score value。
无重复元素、有序的、有元素+分值构成。
时间复杂度比集合类型有所增大。

内部编码
  • ziplist 压缩列表
  • skiplist 跳表

    它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。
    但它的实现比较简单,可以作为平衡树的替代品。
    可参考:
    随机数据结构:跳表(SkipList)

常见命令
  • 基本操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    zadd key score1 element1 score2 element2 ...
    O(logN),添加score和element

    zrem key element
    O(1) 删除

    zscore key element
    O(1) 返回元素score

    zincrby key incrScore element
    O(1),增加或减少元素的score

    zcard key
    O(1) 返回key中元素个数

    zrank key element
    获取某元素的排名(升序 从小到大)
  • 范围操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    zrange key start end [withscores]
    返回指定索引范围的升序元素,是否打印分数可选
    复杂度为O(log(n) + m) :
    n指有序集合中元素的个数;
    m指获取范围内的元素个数

    zrangebyscore key minScore maxCore [withscores]
    O(log(n) + m) ,指定分数范围,其余和上面zrange一样

    zcount key minScore maxScore
    O(log(n) + m) ,指定分数范围的个数

    zremrangebyrank key start end
    O(log(n) + m) ,删除指定排名內的升序元素

    zremrangebyscore key start end
    zremrangebyscore key minScore maxScore

    zrevrank/zrevrange/zrevrangebyscore
    排名从高到低
  • 集合操作:

    1
    2
    zinterstore/zunionstore
    集合间操作,交集/并集
应用

1、排行榜实现:
zadd key score1 element1 score2 element2 ...
score可以为:时间戳、销量、关注人数等

高级功能

慢查询 SlowLog

概念

慢查询顾名思义是将redis执行命令较慢的命令记录到慢查询队列中
慢查询是一个先进先出的队列,且队列是固定长度的,保存在内存中的

生命周期

Redis命令执行的完整生命周期:

1
client发送命令 -> Redis队列命令排队(单线程) -> 执行命令 -> 返回结果到client

慢查询发生在第3个阶段(执行命令);
客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素。

两个配置和三个命令
两个配置
  • slowlog-log-slower-than 10000
    sloglog是用来记录redis运行中执行比较慢的命令耗时。
    当命令的执行超过了指定时间,就记录在slowlog中(单位是微秒,所以1000000就是1秒),slowlog保存在内存中,所以没有IO操作。

    注意:负数时间会禁用慢查询日志,而0则会强制记录所有命令。

  • slowlog-max-len 128
    慢查询队列长度(记录多少条慢查询,默认128)
    一个新的命令满足慢查询条件时被插入到这个列表中。
    当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出。

三个命令
  • slowlog get [n]
    获取慢查询日志,参数n可以指定条数
    返回结果有6个部分组成:
    1、慢查询日志的唯一ID
    2、发生的时间戳
    3、命令耗时,单位微秒
    4、执行的命令和参数
    5、客户端网络套接字(ip: port)
    6、“”

  • slowlog len
    查询当前慢查询记录数

  • slowlog reset
    重置慢查询日志 (实际是对列表做清理操作)

运维经验
  • slowlog-max-len不要设置太小,通常1000左右
    线上建议调大。因为记录慢查询时Redis会对长命令做阶段操作,并不会占用大量内存,增大慢查询列表可以减缓慢查询被剔除的可能。

  • slowlog-log-slower-than不要设置太大,通常1000微秒(即1ms),根据时间QPS设置
    默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。

    由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间超过1毫秒以上,那么Redis最多可支撑QPS不到1000因此对于高QPS场景下的Redis建议设置为1毫秒。
    如:qps为10000的话,平均每个时间就为0.1ms,如超过1ms就会对qps造成影响,这样调小阈值慢查询才会被记录下来。

  • 注意Redis命令的生命周期。
    慢查询只记录命令的执行时间,并不包括命令排队和网络传输时间。
    因此客户端执行命令的时间会大于命令的实际执行时间,因为命令执行排队机制,慢查询会导致其他命令级联阻塞。
    因此客户端出现请求超时时,需要检查该时间点是否有对应的慢查询,从而分析是否为慢查询导致的命令级联阻塞。

  • 定期持久化慢查询
    由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多,队列满的情况下,可能会丢失部分慢查询命令。
    为了防止这种情况发生,可以定期执行slowlog get命令将慢查询日志持久化到其他存储中(例如:MySQL、ElasticSearch等),然后可以通过可视化工具进行查询。

管道 Pipeline

pipeline,即流水线。
1次pipeline(N条命令)= 1次网络时间 + N次命令时间
对于多个命令执行,不再同步等待每个命令的返回结果。我们会在一个统一的时间点来获取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
for (int i = 0; i < 100; i++) {
Pipeline pipeline = jedis.pipelined();
for (int j = i * 100; j < (i + 1) * 100; j++) {
pipeline.hset("hashKey", "field-" + j, "value-" + i);
}
// pipeline.sync() 表示一次性的异步发生,不关注执行结果
// pipeline.syncAndReturnAll() 程序会阻塞,等所有命令完成之后,返回一个list
// pipeline不适合组装特别多的命令,要进行命令的拆分
List<Object> list = pipeline.syncAndReturnAll();
List<String> setList = list.stream().map(Object::toString).collect(Collectors.toList());;
System.out.println(String.join(",", setList));
}
}
优点
  • 提高redis的读写能力。

    Redis其实是一个基于TCP协议的CS架构的内存数据库,所有的操作都是一个request一个response的同步操作
    redis每接收到一个命令就会处理一个命令,并同步返回结果。
    这样带来的问题就是,一个命令就会产生一次RTT(Round Time Trip,往返时间),这样的话必然会消耗大量的网络IO。

redis客户端执行一条命令分4个过程:发送命令-〉命令排队-〉命令执行-〉返回结果
这个过程称为RTT
mget和mset批量操作,有效节约了RTT,但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,需要消耗N次RTT ,这个时候需要pipeline来解决这个问题

需注意
  • Redis命令时间是微秒级别的,无瓶颈,也就是pipeline解决了Redis网络的瓶颈。
  • pipeline中每条命令要注意网络消耗
  • 使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。
  • pipeline每次只能作用在一个Redis节点;
与M操作(批处理)对比:
  • 原生批命令操作是原子的(一批命令 要么成功要么失败)。
    pipeline是非原子的,会将其中命令进行拆分的,但返回的结果是顺序的。
  • 原生批命令一命令多个key, 但pipeline支持多命令(存在事务),非原子性;
  • 原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成。

发布订阅 Pub/Sub

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。

角色
  • 发布者:Publisher
    基于客户端实现。

    注意:无法做消息堆积,即获取历史信息

如Pub发布了一条消息到Channel,新的Sub去订阅该Channel,是收不到之前的消息的

  • 订阅者:Subscribe
    基于客户端实现,可订阅多个频道。

  • 频道:Channel
    基于Server段实现。

API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
publish channel message
指定频道发布消息,返回订阅者数量

subscribe channel ...
可订阅一个或多个频道

unsubscribe channel ...
取消订阅一个或多个
由于Redis的订阅操作是阻塞式的,因此一旦客户端订阅了某个频道或模式,就将会一直处于订阅状态直到退出。在SUBSCRIBE,PSUBSCRIBE,UNSUBSCRIBE和PUNSUBSCRIBE命令中,其返回值都包含了该客户端当前订阅的频道和模式的数量,当这个数量变为0时,该客户端会自动退出订阅状态。

psubscribe/punsubscribe pattern ...
模式匹配。订阅/退订一个或多个符合给定模式的频道。

pubsub numsub channel ...
返回指定channel的订阅数量

pubsub numpat
列出被订阅模式的数量
对其他消息队列发布订阅的对比:
  • 其他MQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失;
  • 其他MQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。
  • 其他MQ支持多种消息协议。

位图 BitMap

概念

8bit = 1b = 0.001kb
bitmap就是通过最小的单位 bit来进行0或者1的设置,表示某个元素对应的值或者状态。

一个bit的值,只能是0或1;也就是说一个bit能存储的最多信息是2。
位数组是自动扩展的,如设置在某个offset超出来现有范围,就会自动将位数组进行0扩充。

位图并不是一种特殊的数据结构,其实本质上是二进制字符串,也就是byte数组。

可以使用普通的 get/set 直接获取和设置整个位图的内容;
也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。

Bitmaps 并不是实际的数据类型,而是定义在String类型上的一个面向字节操作的集合。
因为字符串是二进制安全的块,他们的最大长度是512M,最适合设置成2^32个不同字节。

优势
  • 基于最小的单位bit进行存储,所以非常省空间。
  • 设置时候时间复杂度O(1)、读取时候时间复杂度O(n),操作是非常快的
  • 二进制数据的存储,进行相关计算的时候非常快
  • byte二进制数组方便扩容
限制

redis中BitMap被限制在512MB之内,所以最大是2^32位。
建议每个key的位数都控制下,因为读取时候时间复杂度O(n),越大的串读的时间花销越多。

常用命令API
  • getbit key offset
    获取位图指定索引的值。
    长度超过补为0。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    127.0.0.1:6379> set hello redis
    OK
    127.0.0.1:6379>
    127.0.0.1:6379> getbit hello 0
    (integer) 0
    127.0.0.1:6379> getbit hello 1
    (integer) 1
    127.0.0.1:6379> getbit hello 2
    (integer) 1
    127.0.0.1:6379> getbit hello 3
    (integer) 1
    127.0.0.1:6379> getbit hello 4
    (integer) 0
  • setbit key offset value
    给位图指定索引设置值,返回该索引位置的原始值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    127.0.0.1:6379> get hello
    "redis"
    127.0.0.1:6379> getbit hello 3
    (integer) 1
    127.0.0.1:6379> setbit hello 3 0
    (integer) 1
    127.0.0.1:6379> get hello
    "bedis"
    127.0.0.1:6379>
  • bitcount key [start end]
    获取位图指定范围(start到end,单位为字节,如果不指定就是获取全部)位值为1的个数。

    1
    2
    3
    4
    127.0.0.1:6379> get hello
    "bedis"
    127.0.0.1:6379> bitcount hello
    (integer) 19
  • bitpos key targetBit [start] [end]
    计算位图指定范围(start到end,单位为字节,如果不指定就是获取全部)第一个偏移量对应的值等于targetBit的位置。
    查找指定范围内出现的第一个0或1。

  • bitop and|or|not|xor destkey key [key...]
    做多个bitmap的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存到destkey中。

    1
    2
    3
    4
    5
    6
    7
    8
    127.0.0.1:6379> set hello big
    OK
    127.0.0.1:6379> set world big
    OK
    127.0.0.1:6379> bitop and destkey hello world
    (integer) 3
    127.0.0.1:6379> get destkey
    "big"
  • bitfield命令
    已支持的命令列表:

    1
    2
    3
    4
    5
    - 支持的命令:GET <type> <offset> – 返回指定的位域
    - SET <type> <offset> <value> – 设置指定位域的值并返回它的原值
    - INCRBY <type> <offset> <increment> – 自增或自减(如果increment为负数)指定位域的值并返回它的新值
    还有一个命令通过设置溢出行为来改变调用INCRBY指令的后序操作:
    - OVERFLOW [WRAP|SAT|FAIL]

    示例:
    当需要一个整型时,有符号整型需在位数前加i,无符号在位数前加u。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    127.0.0.1:6379> get w
    "hello"
    127.0.0.1:6379>
    127.0.0.1:6379> BITFIELD w get u4 0
    1) (integer) 6
    127.0.0.1:6379> BITFIELD w get u3 2
    1) (integer) 5
    127.0.0.1:6379> BITFIELD w get i4 0
    1) (integer) 6
    127.0.0.1:6379> BITFIELD w get i3 2
    1) (integer) -3

    setbit和getbit指定的值都是单个位的,如果指定多个位,就需要pipeline来处理。但使用bitfield可以对指定位片段进行读写,但最多只能处理64个连续但位。
    如超过需使用多个子指令,bitfield可以一次执行多个子指令。

    1
    2
    3
    4
    5
    6
    一次执行多个子指令:
    127.0.0.1:6379> BITFIELD w get u4 0 get u3 2 get i4 0 get i3 2
    1) (integer) 6
    2) (integer) 5
    3) (integer) 6
    4) (integer) -3

    incrby,它用来对指定范围的位进行自增操作。
    既然提到自增,就有可能出现溢出。
    如果增加了正数,会出现上溢,如果增加的是负数,就会出现下溢出。
    Redis 默认的处理是折返。如果出现了溢出,就将溢出的符号位丢掉。
    如果是 8 位无符号数 255, 加 1 后就会溢出,会全部变零。如果是 8 位有符号数 127,加 1 后就会溢出变成 -128。

    溢出策略:
    bitfield 指令提供了溢出策略子指令 overflow,用户可以选择溢出行为。
    默认是折返 (wrap),还可以选择失败 (fail) 报错不执行,以及饱和截断 (sat),超过了范围就停留在最大 最小值。
    overflow 指令只影响接下来的第一条指令,这条指令执行完后溢出策略会变成默认 值折返 (wrap)。

应用
  • 1、UV 独立用户统计
    使用set和Bitmap(前提是用户的ID必须是整型)
    场景一:总共1亿用户,五千万独立用户。

    使用Set:

    • 每个userId占用空间:32位(假设userId用的是integer) ;
    • 需要存储的用户量:50,000,000;
    • 内存使用总量:32位 * 50,000,000=200MB
      使用BItMap:
    • 每个userId占用空间:1位;
    • 需要存储的用户量:100,000,000;
    • 内存使用总量:1位 * 100,000,000=12.5MB

场景二:总共1亿用户,若只有10万独立用户

使用Set

  • 每个userId占用空间:32位(假设userId用的是integer) ;
  • 需要存储的用户量:100,000;
  • 内存使用总量:32位 * 100,000=4MB
    使用BItMap:
  • 每个userId占用空间:1位;
  • 需要存储的用户量:100,000,000;
  • 内存使用总量:1位 * 100,000,000=12.5MB
  • 2、用户签到数据记录:

    1
    setbit key offset value

    key主要由用户id组成,设定一个初始时间,每加一天即对应用户的offset的加1。value=1为已签到
    取数据时,只需要计算时间段差的天数,然后

    1
    bitcount key [start end]
  • 3、用户在线状态:判断某用户是否在线?

    1
    setbit key offset value

    只需要一个key,然后用户id为偏移量offset,如果在线就设置为1,不在线就设置为0。
    3亿用户只需要大约36.6MB的空间:1位 * 3亿=3亿位/8/1000/1024=36.6M

  • 4、统计活跃用户:
    setbit key offset value 使用时间作为key,用户id为offset,如果当日活跃过就设置为1。
    通过bitop and|or|not|xor destkey key [key...]进行二进制计算,就可以算出在某段时间内用户的活跃情况。

注意
  • string类型最大长度为512M。
  • 注意setbit时的偏移量,当偏移量很大时,可能会有较大耗时。
  • 位图不是绝对的好,有时可能更浪费空间。(如UV统计时的第二种情况)。

HyperLogLog 基数统计

概念

HyperLogLog 是用来做基数统计的算法
基数统计即统计一个数据集中不重复元素的个数

提供不精确的去重计数方案,标准误差(即均方根误差,RMSE,Root mean squared error)为0.81%

API命令
1
2
3
4
5
6
7
8
PFADD key element [element ...]
添加指定元素到 HyperLogLog 中。

PFCOUNT key [key ...]
返回给定 HyperLogLog 的基数估算值。

PFMERGE destkey sourcekey [sourcekey ...]
将多个 HyperLogLog 合并为一个 HyperLogLog
优缺点

优点:

  • 在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
  • 每个HyperLogLog 键只需要花费12KB内存,就可以计算接近 2^64 个不同元素的基数。
    这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

缺点:

  • HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
  • HLL这个数据结构需要占据一定12k的存储空间,不适合统计单个用户相关的数据。
  • 为什么占用12K?
    因为Redis的HLL实现中用到了16384(2^14)个桶,每个桶的maxBits需要6个bits来存储,最大可以表示为maxBits=63。
    于是总内存为:16384 * 6 / 8 / 1024 = 12k字节
优化

Redis对HLL做了优化:

  • 在计数比较小时,存储空间采用稀疏矩阵存储,占用空间小;
  • 计数变大时,稀疏矩阵占用空间超过阈值时,会一次性转变为稠密矩阵,才会占用12K的空间。
  • 稀疏矩阵
    在矩阵中,若数值为0的元素数目远远多于非0元素的数目时,则称该矩阵为稀疏矩阵。

只存储在矩阵中极少数的非零元素,为此必须对每一个非零元素,保存它的下标和值
可以采用一个三元Trituple<row,column,value>来唯一地确定一个矩阵元素
因此,稀疏矩阵需要使用一个三元组数组(亦称为三元组表)来表示。

  • 稠密矩阵
    与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵。
应用
  • 1、用户的UV独立访问统计
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    127.0.0.1:6379> PFADD uv 1 2 3 4 3 4 2 1
    (integer) 1
    127.0.0.1:6379> PFCOUNT uv
    (integer) 4
    127.0.0.1:6379> PFADD uv_2 7 8 9 8 7 9
    (integer) 1
    127.0.0.1:6379> PFCOUNT uv_2
    (integer) 3
    127.0.0.1:6379> PFMERGE result uv uv_2
    OK
    127.0.0.1:6379> PFCOUNT result
    (integer) 7
    127.0.0.1:6379>

GEO 地理位置信息

GEO 地理信息定位:存储经纬度、计算两地距离、范围计算等。

原理

业界比较通用的地理位置距离排序算法是 GeoHash 算法

  • GeoHash算法
    GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近
    当我们想要计算「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行 了。

    映射算法实现

    • 1、它将整个地球看成一个二维平面,然后划分成了一系列正方形的方格,就好比围棋棋盘,所有的地图元素坐标都将放置于唯一的方格中。方格越小,坐标越精确。
    • 2、然后对这些方格进行整数编码,越是靠近的方格编码越是接近。

      最简单的方案就是切蛋糕法
      设想一个正方形的蛋糕摆在你面前,二刀下去均分 分成四块小正方形,这四个小正方形可以分别标记为 00,01,10,11 四个二进制整数。然后对每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用 4bit 的二进制整数予以表示。然后继续切去,正方形就会越来越小,二进制整数也会越来越长,精确度就会越来越高。

    • 3、编码之后,每个地图元素的坐标都将变成一个整数,通过这个整数可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程度就越小。
    • 4、GeoHash算法会继续对这个整数做一次 base32编码 (0-9,a-z去掉 a,i,l,o 四个字母) 变成一个字符串。

      在Redis里面,经纬度使用52位的整数进行编码,放进了zset里面,zset的 value 是元素的 key,score 是 GeoHash 的 52 位整数值。

    • 5、在使用 Redis 进行 Geo 查询时,它的内部结构实际上只是一个zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 ,通过将 score 还原成坐标值就可以得到元素的原始坐标。
API命令
  • geoadd key longitude latitude member [longitude latitude member...]
    将给定的空间元素(纬度、经度、名字)添加到指定的键里面。

    这些数据会以有序集合的形式被储存在键里面,
    从而使得georadius和georadiusbymember这样的命令可以在之后通过位置查询取得这些元素。
    geoadd命令以标准的x,y格式接受参数,所以用户必须先输入经度,然后再输入纬度。
    geoadd能够记录的坐标是有限的:非常接近两极的区域无法被索引的精确的坐标限制由EPSG:900913 / EPSG:3785 / OSGEO:41001 等坐标系统定义, 具体如下

  • 有效的经度介于-180-180度之间
  • 有效的纬度介于-85.05112878 度至 85.05112878 度之间。
    当用户尝试输入一个超出范围的经度或者纬度时,geoadd命令将返回一个错误。
1
2
127.0.0.1:6379> GEOADD cities:locations 116.28 39.55 beijing
(integer) 1
  • geopos key member [member...]
    从键里面返回所有给定位置元素的位置(经度和纬度)

    geopos命令返回一个数组。
    数组中的每个项都由两个元素组成:
    第一个元素为给定位置元素的经度,
    而第二个元素则为给定位置元素的纬度。
    当给定的位置元素不存在时,对应的数组项为空值.

1
2
3
4
5
6
7
127.0.0.1:6379> geopos cities:locations tianjin shijiazhuang
1) 1) "117.12000042200088501"
2) "39.0800000535766543"
2) 1) "114.29000169038772583"
2) "38.01999994251037407"
127.0.0.1:6379> geopos cities:locations nanjing
1) (nil)
  • geodist key member1 member2 [unit]
    计算出的距离会以双精度浮点数的形式被返回。如果给定的位置元素不存在,那么命令返回空值。

    指定单位的参数unit必须是以下单位的其中一个:
    m表示单位为米
    km表示单位为千米
    mi表示单位为英里
    ft表示单位为英尺
    如果用户没有显式地指定单位参数,那么geodist默认使用米作为单位。
    geodist命令在计算距离时会假设地球为完美的球形,在极限情况下,这一假设最大会造成0.5%的误差。

1
2
3
4
5
6
127.0.0.1:6379> geodist cities:locations tianjin shijiazhuang
"272929.6477"
127.0.0.1:6379> geodist cities:locations tianjin shijiazhuang km
"272.9296"
127.0.0.1:6379> geodist cities:locations tianjin nanjing km
(nil)
  • georadius key longitude latitude radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]
    以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素。

    范围可以使用以下其中一个单位:
    m 表示单位为米。
    km 表示单位为千米。
    mi 表示单位为英里。
    ft 表示单位为英尺。
    withdist:在返回位置元素的同时,将位置元素与中心之间的距离也一并返回.距离的单位和用户给定的范围单位保持一致。
    withcoord:将位置元素的经度和纬度也一并返回。
    withhash:以52位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中的作用不大。
    命令默认返回未排序的位置元素。
    通过以下两个参数,用户可以指定被返回位置元素的排序方式:

    asc:根据中心的位置,按照从近到远的方式返回位置元素
    desc:根据中心的位置,按照从远到近的方式返回位置元素。

在默认情况下,georadius命令会返回所有匹配的位置元。虽然用户可以使用count选项去获取N个匹配元素,但是因为命令在内部可能会需要对所有被匹配的元素进行处理,所以在对一个非常大的区域进行搜索时,即使只使用count选项去获取少量元素,

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> georadius cities:locations 117 39 200 km withdist
1) 1) "baoding"
2) "158.0144"
2) 1) "beijing"
2) "87.0941"
3) 1) "tianjin"
2) "13.6619"
4) 1) "tangshan"
2) "96.7842"
  • georadiusbymember key member radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]
    和georadius命令一样,都可以找出位于指定范围内的元素,
    但是georadiusbymember的中心点是由给定的位置元素决定的。
    而不是像georadius那样,使用输入的经度和纬度来决定中心点。

    1
    2
    3
    4
    5
    6
    7
    127.0.0.1:6379> georadiusbymember cities:locations tianjin 100 km
    1) "beijing"
    2) "tianjin"
    3) "tangshan"
    127.0.0.1:6379> georadiusbymember cities:locations beijing 100 km
    1) "beijing"
    2) "tianjin"
  • geohash key member [member...]
    使用geohash将二维经纬度转换为一维字符串,字符串越长表示位置更精确,两个字符串越相似表示距离越近。

    1
    2
    3
    4
    5
    6
    127.0.0.1:6379> geohash cities:locations tangshan baoding
    1) "wx5bj2um070"
    2) "wwcgp6x9580"
    127.0.0.1:6379> geohash cities:locations beijing tianjin
    1) "wx48ypbe2q0"
    2) "wwgq34k1tb0"
  • zrem
    GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除.

    1
    zrem cities:locations tianjin
应用
  • 1、查看附近的人、餐厅、公司等
    1
    2
    georadiusbymember company huoli 20 km count 3 asc
    范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身

事务 Transaction

  • MULTI
    标记一个事务块的开始。
    事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC命令原子性(atomic)地执行。
    O(1)操作;

  • EXEC
    执行所有事务块内的命令。
    假如某个(或某些) key 正处于 WATCH命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。
    时间复杂度:事务块内所有命令的时间复杂度的总和。
    返回:事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil

  • DISCARD
    取消事务,放弃执行事务块内的所有命令。
    如果正在使用 WATCH命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH
    O(1)操作,总是返回ok

  • WATCH key [key ...]
    监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
    O(1)操作

  • UNWATCH
    取消 WATCH命令对所有 key 的监视。
    如果在执行 WATCH命令之后, EXEC命令或DISCARD命令先被执行了的话,那么就不需要再执行 UNWATH了。

    因为 EXEC命令会执行事务,因此 WATCH命令的效果已经产生了;而 DISCARD命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH了。

内部原理

持久化

概述

Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制 来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。

持久化就是:redis将所有的数据保存在内存中,对数据的更新它会异步地保存到磁盘上

主流数据库的持久化方式:

  • 快照
    如:MySQL的Dump、Redis的RDB
  • 写日志
    如:MySQL的Binlog、Hbase的HLog、Redis的AOF日志
    两者区别:
  • 1、快照是一次全量备份,AOF 日志是连续的增量备份;
  • 2、快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本;
  • 3、AOF 日志在长期的运行过程中会 变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。

三种方式

RDB快照

RDB快照,也就是定时快照(snapshot),是存储在硬盘中的二进制文件,是一个复制媒介用于一次全量备份
RDB快照持久化,也就是是将redis数据库某个时点的数据信息以快照文件的形式保存到磁盘的持久化方法

  • 主要思想
    RDB方式实际就是在Redis内部存在一个定时器机制,扫描进程按照配置文件中的要求去检查数据的变化情况,即根据指定时间内的数据变化次数决定是否进行持久化
    当达到持久化的触发条件时,操作系统会单独创建(fork)一个子进程来进行数据持久化的操作,子进程默认与父进程有共同的地址空间,这样子进程就可以遍历整个内存来进行存储操作,主进程此时仍然可以正常提高服务,只是不进行I/O操作,有写入请求时,由操作系统按照内存页(Page)为单位进行写时复制Copy-on-Write,从而保证主进程的高效性能。

子进程数据写入时是将数据先写入一个临时文件中,当整个数据写入完毕后,才会用临时文件覆盖上一个持久化好的快照文件,这样保证了系统可以随时的进行数据备份,数据文件总是可用的。

由上可知:RDB模式的持久化,数据的完整性没有保障

触发复制的三种方式:
  • 手动触发:
    save一个同步的命令:O(n)操作
    该指令会阻塞当前 Redis 服务器,执行 save 指令期间,Redis 不能处理其他命令,直到 RDB 过程完成为止。如存在老的RDB文件,会新建一个临时文件,执行完毕后替换老文件,再删除老文件。
    bgsave 一个异步命令:O(n)操作
    执行该命令时,Redis 会在后台异步执行快照操作,此时 Redis 仍然可以相应客户端请求。如存在老的RDB文件,会新建一个临时文件,执行完毕后替换老文件。

    具体操作是Redis进程执行linux的fork函数 操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。Redis 只会在 fork 期间发生阻塞,但是一般时间都很短。但是如果 Redis 数据量特别大,fork 时间就会变长,而且占用内存会加倍,这一点需要特别注意。

  • 自动触发:(redis.conf 配置)
    参考:配置中的持久化模块
    可以设置指定时间内key的变化数量来自动触发。

  • 其他触发机制:(不可忽视)
    全量复制:涉及到主从复制,在没有触发手动和自动的时候,主从复制时(全量复制),主会生成rdb文件。
    debug reload操作:进行debug级别的重启,也会生成rdb文件。
    命令 shutdown [nosave|save]:关闭服务指定save时,也会生成

    Shutdown 命令执行以下操作:

  • 停止所有客户端

  • 如果有至少一个保存点在等待,执行 SAVE 命令

  • 如果 AOF 选项被打开,更新 AOF 文件

  • 关闭 redis 服务器(server)

原理
  • 背景:
    单线程同时在服务线上的请求还要进行文件 IO 操作,文件IO操作会严重拖垮服务器请求的性能。

    Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。
    在服务线上请求的同时,Redis还需要进行内存快照,内存快照要求 Redis 必须进行文件IO操作,可文件 IO 操作是不能使用多路复用API。

  • COW 写时复制(copy-on-write)
    为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变:

    比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这要怎么办? 所以,Redis 使用操作系统的fork多进程 COW(Copy On Write) 机制来实现快照持久化

大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率。

实现原理:

  • fork是类Unix操作系统上创建进程的主要方法。fork用于创建子进程(等同于当前进程的副本)。
    新的进程要通过老的进程复制自身得到,这就是fork!
  • fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。
    当父子进程都只读内存时,相安无事。
    当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。
    中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。

优点:
1、COW技术可减少分配和复制大量资源时带来的瞬间延时。
2、可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。

缺点:
1、如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。

  • fork多进程操作
    • Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。
    • 子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。
    • 进程分离的逻辑:
      fork 函数会在父子进程同时返回,在父进程里返回子进程的pid,在子进程里返回零。
      如果操作系统内存资源不足,pid 就会是负数,表示 fork 失败。
    • 子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。
      但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。
    • 这个时候就会使用操作系统的COW机制(写时复制, copy-on-write)来进行数据段页面的分离。

      数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。
      随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的2倍大小。
      另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。
      每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。
      子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,
      这也是为什么 Redis 的持久化叫「快照」的原因。
      接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。

AOF追写文件

AOF 日志是连续的增量备份,记录的是内存数据修改的指令记录文本
就是把执行过的写指令按照执行顺序写在记录文件的尾部,重启时读取文件指令明细按照顺序执行一遍就能完成恢复。

  • 主要思想总结:
    配置文件redis.conf中的appendonly参数就是控制AOF功能的启动与关闭,yes表示开启,如果在有写操作,指令就会被追加到记录文件的尾部。

AOF默认的存储策略是每秒钟执行一次,术语称之为fsync

即把缓存中的写指令记录到文件中,这是redis持久化与性能的最佳平衡点,此策略既能保证 redis 有很好的性能表现又能保障数据完整性,最多存在 1 秒钟的数据丢失

在文件追加过程中,如果发生了意外错误,例如系统宕机等意外状况,导致记录文件写入不完整的缺陷,这种情况下,redis提供了一个叫redis-check-aof 的工具可以对不完整的日志文件进行修复处理。

AOF文件一定会随着数据量变动越来越大,有可能导致日志空间不足等意外风险,为了避免这个低级的错误发生,redis 提供了一个方便实用的自我保护机制-重写(rewrite)机制

就是管理员预先设定 AOF 文件大小的阈值,当实际大小超过阈值时,redis 就会启动了文件内容压缩,只保留了能保障数据恢复的最
小可用的指令集。文件重写功能仍然采用了先创建并写入临时文件,当重写过程结束后,才更名及覆盖上一个可用的 aof 文件模式来保障备份文件的随时可用。

AOF 文件本身是可读且可编辑的

可读可编辑的好处:假设我们在操作 redis 时,不小心执行了 flushall,内存数据全部清空了,但是开启了AOF功能且AOF文件没有被重写的前提下,我
们可以暂停 redis 并对aof文件进行编辑,删除文件末尾保存的 flushall 指令,重启 redis,内存数据就能恢复。

RDB快照的问题
  • 1、耗时、耗性能。
    将内存中的数据dump到硬盘,是一个O(n)过程,比较耗时;
    bgsave中的fork():消耗内存,copy-on-write策略
    硬盘I/O:IO性能问题

  • 2、不可控、容易丢失数据。
    如该场景:
    T1时间 执行多个写命令;
    T2时间 满足RDB自动创建的条件;
    T3时间 再次执行多个写命令;
    T4时间 宕机,就会出现数据丢失

原理

AOF 日志存储的是 Redis服务器的顺序指令序列,只记录对内存进行修改的指令记录。

  • 假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过 对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内存数据结构的状态。
  • Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。
    这样即使遇到突发宕机,已经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。
  • Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身。
AOF重写(提高效率)

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。
日志瘦身:对执行的命令进行合并,如String类型多次赋值只保留最后一次,多次操作改为批量命令操作等。

其原理就是:对redis内存中的内容进行回溯,回溯成aof文件
fork开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

  • AOF重写配置:
    1
    2
    3
    4
    5
    auto-aof-rewrite-min-size 64mb
    AOF文件重写需要的尺寸

    auto-aof-rewrite-percentage 100
    AOF文件增长率
fsync操作(宕机AOF丢失数据问题)

AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了硬盘的缓冲区(内核为文件描述符分配的一个内存缓存中),然后内核会异步将脏数据刷回到磁盘的
这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个 时候就会出现日志丢失。那该怎么办?

  • Linux 的 glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁 盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。
    但是 fsync 是一个磁盘IO操作,它很慢。
    如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的地位就不保了。
  • 所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。
    这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失。
  • Redis 同样也提供了另外两种策略:
    一个是永不 fsync,让操作系统来决定合适同步磁 盘,很不安全;
    另一个是来一个指令就 fsync 一次,非常慢。
  • 三种配置策略:
    具体参考:aof持久化策略的配置
    1
    2
    3
    no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
    always 表示每次写入都执行fsync,以保证数据同步到磁盘。
    everysec 表示每秒执行一次fsync,可能会导致丢失这1s数据
混合持久化

重启 Redis 时,我们很少使用rdb 来恢复内存状态,因为会丢失大量数据(rdb的缺点)。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。Redis 4.0 为了解决这个问题,带来了一个新的持久化选项:混合持久化

  • 生效的两个配置:
    1
    2
    appendonly yes
    aof-use-rdb-preamble yes
  • 思想:
    将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
    于是在Redis重启的时候,可以先加载rdb的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF全量文件重放,重启效率因此大幅得到提升

RDB和AOF对比

  • rdb持久化:故障数据丢失比aof严重,但是服务重启恢复数据快
  • aof持久化:故障数据丢失较rdb少,但是服务启动时恢复数据慢,因为要把aof文件中指令执行一遍。
  • RDB 启动优先级低、体积小、恢复速度快、容易丢数据、比较重的操作;
    AOF 启动优先级高、体积大、恢复速度慢、丢数据要根据策略决定、比较轻的操作;

运维常见问题

  • 通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行
    从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。

    因为:

    • 快照(bgsave)是通过开启子进程的方式进行的,它是一个比较耗资源的操作;遍历整个内存,大块写磁盘会加重系统负载;
      AOF 的 fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系统 IO 负担。
    • 但是如果出现网络问题,从节点长期连不上主节点,就会出现数据不一致的问题。
      特别是在网络分区出现的情况下又不小心主节点宕机了,那么数据就会丢失,所以在生产环境要做好实时监控工作,保证网络畅通或者能快速修复。
    • 还应该再增加一个从节点以降低网络分区的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失。
  • 子进程开销和优化:

    • CPU
      RDB和AOF文件生成,属于CPU密集型;不做CPU绑定,不和CPU密集型部署。
    • 内存
      fork内存开销,copy-on-write,即父子进程共享只读分段文件时,父进程某个分段文件发生写入,会将其拷贝出一份新的分段文件,造成内存开销。
      单机部署时,不允许产生大量重写,
    • 硬盘
      AOF和RDB文件写入,可以集合iostat和iotop分析;
      1、不要和高硬盘负载的服务部署在一起,如:存储服务、消息队列等;
      2、no-appendfsync-no-rewrite = yes,表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,
      3、根据写入量决定磁盘类型:如SSD
      4、单机多实例持久化文件目录可以考虑分盘

主从复制

概述

Redis数据库在单机时:机器故障问题、容量瓶颈、QPS的瓶颈。

主从复制就是对主节点进行数据备份、读写分离对读进行分流。 可以为数据提供副本,扩展redis的读的性能。

1、一个Master可以有多个Slave;
2、一个Slave只能有一个Master;
3、数据流向是单向的,Master到Salve;
4、默认情况下,Redis都是主节点

  • 主要思想

从服务器启动并与主服务器成功建立连接后,它会主动发起 SYNC请求,Master 接收到同步请求时会调用 BGSAVE 指令来创建一个专用子进程来完成数据持久化处理

全量数据持久化处理可以分成两个阶段:

首先,将主服务器的存量数据都写入 RDB 文件,Master 在数据持久化期间产生的增量数据会启动
另一个后台存盘进程,把增量的写指令记录全部缓存在内存中,等待存量数据持久化完成后,Master 把 RDB 数据库文件传送给 Slave,Slave 接收到数据库文件后将其存放在磁盘上并逐步完成数据加载,这样存量数据就同步结束了;
然后,要进行同步的是持久化期间的增量数据,Master 将所有缓存在内存中的写指令按照约定的 redis 协议格式发给 Slave,Slave 接收后在本地执行这些数据写命令,从而达到最终的数据完全同步。

不论是那一种逻辑结构,Master只会执行一次持久化动作无论有多少个slave有同步请求,然后把持久化好的 RDB 文件分发下去
Redis2.8 的版本提供了数据增量同步策略
Master和Slave如果断开连接,之后又重新连接时。在连接成功后,可以尝试进行增量数据同步

增量数据同步策略是主服务器会在内存中维护一个缓冲区来存放待同步数据,主从连接成功后,Slave会把“申请同步的主服务器ID”和“请求的数据的偏移量(replication offset)”发送给Master,Master 收到增量同步请求时,根据上送的申请同步的主服务器ID去匹配自己的ID信息,匹配成功后检查自己的缓存区数据是否能满足申请的数据偏移量,如果都两个条件都满足,则能完成master-slave 的增量数据同步。
如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步。

步骤

  • 1、执行slaveof后,slave只保存master的地址信息,直接返回;
  • 2、主从建立socket连接
    slave内部通过每秒执行的定时任务维护复制相关逻辑,当发现新的matser时,尝试建立网络连接,slave会建立一个socket套接字,用于接收master发送的复制命令;如slave无法建立连接,定时任务会一直重连到成功或执行slaveof no one取消复制。
  • 3、发送ping命令
    连接成功后slave发送ping请求首次通信,检测MS之间套接字是否可用、是否可接收处理命令;
    如slave没收到master的pong回复或超时,下次定时任务会重连;
  • 4、权限验证
    如Master设置了requirepass参数,需要密码验证。
    如验证失败复制将终止,slave重新发起复制流程。
  • 5、同步数据集
    主从连接正常通信后,如首次建立,master会把持有的数据全部发送给slave。(通过rdb或socket的方式)
  • 6、命令持续复制
    当master把数据同步给slave复制流程建立成功后,后面M会持续的把写命令发送给S,保证主从数据一致性。

两种配置

1、slaveof命令:异步的操作

slave上执行命令slaveof MasterHost 6379来异步复制,slave机器重启就会丢失,不建议。
取消复制:slaveof no one

  • 取消时slave不会清除已经同步的数据;
  • slave配置新的Master时,会清除历史数据;
2、配置文件

replicaof <masterip> <masterport>
对Slave机器,替换为对应的主服务器IP和主服务器的端口号。
如果主服务器(redis)有设置密码的话,则需要配置密码masterauth
注意:修改需要重启

全量/部分复制:

涉及知识点
  • 1、runid
    每个Redis服务器都会有一个表明自己身份的ID。
    在PSYNC中发送的这个ID是指之前连接的Master的ID,如果没保存这个ID,PSYNC的命令会使用PSYNC ? -1 这种形式发送给Master,表示需要全量复制。

  • 2、复制偏移量offset:
    通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。在主从复制的Master和Slave双方都会各自维持一个offset。
    Master成功发送N个字节的命令后会将Master的offset加上N,Slave在接收到N个字节命令后同样会将Slave的offset增加N。
    Master和Slave如果状态是一致的那么它的的offset也应该是一致的。

  • 3、复制积压缓冲区backlog
    当从节点在一段时间内断开连接时,主节点会收集数据到backlog这个缓冲区。
    因此当一个从节点想要重新连接时,通常不需要完全的重新同步,但是部分的重新同步就足够了,只是通过在断开连接的时候传递数据的一部分。

    是由Master维护的一个固定长度的FIFO队列,默认大小为1M。
    它的作用是缓存已经传播出去的命令。
    当Master进行命令传播时,不仅将命令发送给所有Slave,还会将命令写入到复制积压缓冲区里面。
    因此当一个slave想要重新连接时,如runingid与M一致且偏移量与M相差没超过缓冲区大小,通常不需要完全的重新同步,增量同步缓冲区的命令就足够了。

  • 4、psync命令
    psync命令复制案例
    从节点使用psync命令完成部分复制和全量复制功能。

psync命令步骤

PSYNC执行过程中比较重要的概念有3个:runid、offset(复制偏移量)以及复制积压缓冲区。

  • 1、客户端向服务器发送SLAVEOF命令,让当前服务器成为Slave;
  • 2、 当前服务器根据自己是否保存Master runid来判断是否是第一次复制,如果是第一次同步则跳转到3,否则跳转到4;
  • 3、 向Master发送PSYNC ? -1 命令来进行完整同步;
  • 4、 向Master发送PSYNC runid offset;
  • 5、 Master接收到PSYNC 命令后首先判断runid是否和本机的id一致,如果一致则会再次判断offset偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,如果没有那么就给Slave发送CONTINUE,此时Slave只需要等待Master传回失去连接期间丢失的命令;
  • 6、 如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,
    那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步。
全量复制

一般用于初次复制场景,Redis早期支持的复制功能只有全量复制。它会把主节点全部数据一次性发送给从节点。
当数据量较大时,会对主从节点和网络造成很大的开销。
全量复制流程

  • 1、发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync-1;
  • 2、主节点根据psync-1解析出当前为全量复制,回复+FULLRESYNC响应;
  • 3、从节点接收主节点的响应数据保存运行ID和偏移量offset;
  • 4、主节点执行bgsave异步快照命令,保存RDB文件到本地;
  • 5、主节点发送RDB给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件,接收完RDB后从节点打印相关日志;
  • 6、对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令。
    因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送个从节点,保证主从之间数据一致性;
    全量复制注意:

    如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。
    client-output-buffer-limit replica 256mb 64mb 60 如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败;
    对于主节点,当发送完所有的数据后就认为全量复制完成;

  • 7、从节点接收完主节点传送来的全部数据后会清空自身旧数据;
  • 8、从节点清空数据后开始加载RDB文件,对于较大的RDB文件,这一步操作依然比较耗时,可以通过计算日志之间的时间差来判断加载RDB的总耗时;
  • 9、从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof AOF日志瘦身操作,为了保证全量复制后AOF持久化文件立刻可用。

全量复制比较耗时:

  • Master进行bgsave快照持久化时间;
  • RDB快照文件网络传输时间;
  • Slave清空老数据时间;
  • 可能存在的AOF重写时间;
部分复制

部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施。使用psync {runId}{offset}命令实现。用于处理在主从复制中因网络闪断等原因造成的数据丢失场景
当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。如复制缓冲区 repl-backlog-xx的配置

部分复制流程
流程:

  • 1、当主节点直接网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接;
  • 2、主从连接中断期间主节点依然响应命令,但因复制连接中断,命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB,可以通过into replication 查看;
  • 3、当从节点网络恢复后,从节点会再次连上主节点;
  • 4、当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。
    因此会把它们当做psync参数发送个主节点,要求进行部分复制操作;
  • 5、主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;
    之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+COUTINUE响应,表示可以进行部分复制;
  • 6、主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

心跳检测

主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令。

主从心跳检测机制:

  • 1、主从节点彼此都有心跳检测机制,各自模拟对方的客户端进行通信,主节点的连接状态为flags=M,从节点连接状态为flags=S
  • 2、主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可以通过repl-ping-replica-period 10 控制发送频率
  • 3、从节点在主线程中每隔一秒发送replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量。
  • 主节点根据replconf命令判断从节点超时时间,体现在info replication 统计中的lag信息中,
    lag表示从节点最后一次通信延迟的秒数,正常延迟应该在0到1之间。
  • 如果超过repl-timeout配置的值(默认60秒),则判定从节点下线并断开复制客户端连接。即使主节点判定从节点下线后,如果从节点重新恢复,心跳检测和继续执行.

故障转移

Master故障

假如主从都没数据持久化,此时千万不要立即重启服务,否则可能会造成数据丢失。应该:

  • 在slave上执行SLAVEOF ON ONE,来断开主从关系并把slave升级为主库;
  • 此时重新启动主数据库,执行SLAVEOF,把它设置为从库,自动备份数据。
Slave故障

如满足业务需求,可以将宕机的slave的连接转移到其他slave。
在Redis中从库重新启动后会自动加入到主从架构中,自动完成同步数据;
如果从数据库实现了持久化,只要重新假如到主从架构中会实现增量同步。

自动故障转移?哨兵机制

Redis提供了sentinel(哨兵)机制通过sentinel模式启动redis后,自动监控master/slave的运行状态
基本原理是:心跳机制+投票裁决

  • 每个sentinel会向其它sentinal、master、slave定时发送消息,以确认对方是否“活”着,
    如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的“主观认为宕机” Subjective Down,简称SDOWN)。
  • 若"哨兵群"中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称ODOWN), 通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。

sentinel哨兵故障自动转移流程

  • 1、通过心跳机制,多个sentinel发现并确认master有问题;
  • 2、选举出一个sentinel作为领导;
  • 3、选出一个slave作为新的master;
  • 4、通知其余slave成为新master的slave;
  • 5、通知客户端主从发生变化;(客户端连接哨兵即可获取主从的信息)
  • 6、等待老的master复活成为新master的slave;

读写分离

默认是读写分离的:replica-read-only yes
对于读占比较高的场景,可以通过把一部分读流量分摊到slave来减轻master压力,同时需要注意永远只对主节点执行写操作。
建议大家在做读写分离之前,可以考虑使用Redis Cluster 等分布式解决方案。

常见问题

1、读写分离:

默认是读写分离的:replica-read-only yes
用于读多写少的场景,将流量分配到Slave节点,减少Master的压力,扩展读的能力。
可能问题:

  • 复制数据延迟,出现读写不一致的情况;
  • 读到过期数据;

    过期策略:
    a. 懒惰性策略(操作key时才会看是否过期)
    b. 定时采样key,看是否过期;
    因为slave只要读操作不能del处理,要靠master将过期删除的命令发送过来再执行,会造成Slave读到过期数据。

  • 从节点发送故障;
2、主从配置不一致:

如:

  • maxmemory不一致:会出现丢失数据;
  • 数据结构优化参数不一致,如hash-max-ziplist-entries:会出现内存不一致问题;
3、规避全量复制:

1、新加slave第一次全量复制不可避免:
可以注意:

  • 小主节点,控制内存;
  • 低峰时间处理;
    2、节点runningid不匹配:
  • 主节点重启,runningid变化;
  • 故障转移,哨兵或集群;
    3、复制积压缓冲区不足:
  • 网络中断,部分复制无法满足;
  • 增大缓冲区的配置rel_backlog_size;
4、规避复制风暴:

如一主多从,主挂重启,需要生成rdb并复制到从节点;

  • 单主节点复制风暴:
    问题:主节点重启,多从节点复制;
    解决:更换复制拓扑,slave下挂slave。
  • 单机器复制风暴:
    机器上节点都是master,机器宕机,会有大量的全量复制;
    可以主节点分散多机器。

Sentinel哨兵

概述

因为主从复制的缺陷:
1、手动故障转移;
2、写能力和存储能力受限;
引出哨兵机制:
Redis的哨兵机制是官方推荐的一种高可用(HA)方案。
哨兵机制主要三个功能:

  • 1、监控:
    不停监控Redis主从节点是否安装预期运行;
  • 2、提醒:
    如果Redis运行出现问题可以按照配置文件中的配置项 通知客户端或者集群管理员;
  • 3、自动故障转移:
    当主节点下线之后,哨兵可以从主节点的多个从节点中选出一个为主节点,并更新配置文件和其他从节点的主节点信息。

客户端连接

步骤:

  • 1、获取所有的sentinel节点和masterName。
    遍历sentinel集合找到可用节点;
  • 2、在找到的可用sentinel节点上,执行命令sentinel get-master-addr-by-name masterName;会获取到master节点真正地址和端口。
  • 3、获取master节点信息后去role验证真伪;
  • 4、如redis节点发生了变化,client会感知到。
    内部基于发布订阅感知:client会订阅sentinel端某个频道,里面有谁是master端信息,有了变化会通知client。

实现原理

客户端高可用案例:(故障转移)

客户端Sentinel高可用案例

Redis Sentinel Failover故障选举测试:死循环对redis哨兵主从进行读写
故障转移恢复案例:

  • 执行该死循环程序;
  • 将其中的7000节点进行强制宕机;这时程序会大量的报错:Connection refused。
  • 过了n秒后,sentinel自动进行完故障转移后,程序就会正常执行打印;
服务端日志分析:数据节点和sentinel节点

日志中故障转移大致流程:

  • 1、发现master不可用,进入主观不可用(SDOWN);
  • 2、进行投票,如达到了quorum(配置文件指定),进入客观不可用(ODOWN)
  • 3、当前配置版本被更新;
  • 4、达到failover条件,正等待其他sentinel的选举:
    开始要选择一个slave当选新master;
    找到了一个适合的slave来担当新master;
    当把选择为新master的slave的身份进行切换;
  • 5、Failover状态变为reconf-slaves
  • 6、sentinel发送SLAVEOF命令把它重新配置,重新配置到新主;将其他slave配置到新master;
  • 7、新的master对新的slave进行数据复制同步;
  • 8、老的master离开客观不可用(ODOWN),failover成功完成。
  • 9、master地址发生改变,变为新的master
  • 10、检测slave并添加到slave列表。
三个定时任务:
  • 1、每10秒每个sentinel会对master和slave进行info操作
    作用就是发现slave节点,并且确认主从关系,
    因为redis-Sentinel节点启动的时候是知道 master节点的,只是没有配置相应的slave节点的信息
  • 2、每隔2秒,sentinel都会通过master节点内部的channel来交换信息(pub/sub订阅模式):
    作用是通过master节点的频道来交互每个Sentinel对master节点的判断信息
  • 3、每隔一秒每个sentinel对其他的redis节点(master,slave,sentinel)执行ping操作
    对于master来说若超过30s没回复,就对该master进行主观下线并询问其他的Sentinel节点是否可以客观下线。
    心跳检测的过程,用来判断上下线的依据。

主观下线和客观下线:

1
2
sentinel monitor mymaster 127.0.0.1 6379 2
#主节点名称 IP 端口号 选举次数
  • 主观下线:每个Sentinel节点对Redis节点失败的“偏见”。
  • 客观下线:所有Sentinel节点对Redis节点失败达成共识。
Sentinel节点领导者选举

因为:只有一个sentinel节点完成故障转移即可。

选举:

通过sentinel is-master-down-by-addr命令都有希望成为leader。

该命令的作用:
a.交换master节点的失败判定;
b.进行领导者选举。

  • 1、每个做主观下线的sentinel节点向其他sentinel节点发命令,要求它设为领导者;
  • 2、收到命令的sentinel节点如目前还没同意通过其他sentinel节点发的命令,就会同意当前请求,否则拒绝;
  • 3、如该sentinel节点的票数超过sentinel集合半数且超过指定的quorum,它就成为领导者;
  • 4、如果有多个sentinel节点成为了领导者,就会等一段时间继续重新选举。
故障转移:(sentinel领导者完成)
  • 1、从slave节点选择一个合适的节点作为新的master;
  • 2、对上面的slave节点执行slaveof on one命令,让它成为master;
  • 3、向剩余的slave节点发送slaveof命令,让它们成为新master的slave;
  • 4、进行新主从数据的复制同步;
  • 5、更新老的master为slave,并一直关注它,当重启生效后就去复制新的master节点的数据。

如何选择合适的slave节点作为新的master?

  • 1、选择slave-priority优先级最高的slave节点,如存在返回,否则继续;
  • 2、选择复制偏移量最大的slave节点(数据最完整,类似zookeeper),如存在返回,否则继续;
  • 3、选择runningid最小的slave节点(也就是最早启动的节点);
注意
  • 1、sentinel集群节点大于等于3且为奇数
  • 2、redis的sentinel是配置中心不是代理,其中的数据节点和普通数据节点没区别。
    客户端初始化连接的是sentinel节点集合,不是具体的redis节点;
    它通过3个定时任务实现了sentinel节点对于master和slave和其余sentinel节点的监控;

Cluster集群

为什么需要集群?

  • 并发问题:redis最高的ops并发量为10w,如果业务需要ops为100万就无法解决;
  • 数据量问题:单机的内存太小,无法满足需求;

集群架构

  • 单机架构(主从模式也是单主机架构)
  • 分布式架构:
    服务端多个节点,每个节点都可读写,节点间都是可以通信的,节点间互相了解各自对应的槽。
    client访问任意节点,读key时,如在该节点会直接返回,否则返回key真实的槽所在的节点信息,在做对应跳转获取;

数据分布:

数据分布

两种分区方式:
两种分区方式比较

顺序分布

顺序分布
(上图3个节点,平均每个节点33个数据)
顺序分区的数据量不可确定性会导致倾斜,支持顺序访问,但不支持批量操作。

哈希分布

哈希分布

节点取余分区 hash(key)%nodes
数据分散度高,无法顺序访问,支持批量操作;

1、节点取余分区:hash(key)%nodes

节点取余分区

  • 会有节点伸缩:数据节点关系变化,导致数据迁移;
  • 迁移数量和添加节点的数量有关,建议翻倍扩容,迁移数据量会比较小。
    如3个节点变为6个节点,这样数据迁移量在50%左右;

节点取余分区
如对节点进行扩容时:如3个节点变为4个节点。
问题:

  • 如果要增加分区,数据迁移量在80%左右;
  • 数据迁移第一次是无法从缓存中取到的,数据库需要回写到新节点;
2、一致性哈希分区:哈希 + 顺时针(优化取余)

一致性哈希分区
将数据作为一个0~2^ 32大小的token环中,有4个node,为每个node分配一个token,每个node负责范围内的数据;
如某key进行hash计算落在node3和4范围内,它会顺时针找离自己最近的node,即node3。

  • 节点伸缩:只影响邻近节点,但是还是有数据迁移;
  • 翻倍伸缩:保证最小迁移数据和负载均衡;
  • 多用在节点非常多的时候。

一致性哈希分区
节点扩容的情况:
如上图,添加节点node5,会进行数据的漂移,但不会影响node3和4。尤其节点非常多的时候效率提高太多;

3、虚拟槽分区:(共享消息模式,集群默认)

虚拟槽分区

  • 预设虚拟槽(redis cluster范围0-16383):每个槽映射一个数据子集,一般比节点数大;
  • 良好的哈希函数:例如CRC16;
  • 服务端管理节点、槽、数据:例如Redis Cluster;

假如有10w数据,有16384个槽,5个节点,对槽进行分配,对key按照一定的hash规则计算后,再对16383进行取余,会把取余对结果发生给cluster中的任意一个节点,而每个节点都知道自己负责的槽,如落在自己槽的范围内,就由它管理。如不在该槽,因为节点间会共享消息,所以就会知道该key对应的真实的槽。

集群伸缩

伸缩原理

集群伸缩

集群伸缩 = Slot槽和数据在节点之间的移动。

集群扩容:增加新节点
  • 1、准备新节点
    集群模式下,配置和其他节点统一,目前启动后仍是孤儿节点;

  • 2、加入集群
    作用:为它迁移槽和数据实现扩容;作为slave节点负责故障转移;
    加入集群

  • 3、迁移槽和数据
    迁移槽和数据

    迁移数据过程:

    • 对目标节点发送:cluster setslot {slot) importing {sourceNodeld)命令,让目标节点准备导入槽的数据。
    • 对源节点发送:cluster setslot {slot) migrating {targetNodeld)命令,让源节点准备迁出槽的数据。
    • 源节点循环执行cluster getkeysinslot {slot) {count)命令,每次获取count个属于槽的健。
    • 在源节点上执行migrate {targetlp} {targetPort} key 0 {timeout}命令把指定key迁移。
    • 重复执行步骤3~4直到槽下所有的键数据迁移到目标节点。
    • 向集群内所有主节点发送cluster setslot {slot)node {targetNodeld)命令,通知槽分配给目标节点。
集群缩容:下线节点和槽

下线节点和槽

客户端路由:(实际开发会遇到的问题)

moved重定向

moved重定向
client接收到moved异常后,会拿到正确的目标节点需自己去执行;

注意:client不会自动找到目标节点进行跳转,需要二次写的逻辑进行功能开发;

moved重定向
槽命中,直接返回(可查看key的slot)

moved重定向
槽未命中:返回moved异常:
客户端不会自己找到异常节点,需要自己写逻辑;

集群和非集群环境对比
集群和非集群环境下:
集群环境下会自动完成:捕获moved异常和重新写的操作;

ask重定向

在进行集群伸缩时,会出现数据slot迁移,出现ask重定向。
cluster-ask
ask是槽还在迁移中;
moved是槽已经完成了迁移;

smart(智能)客户端:JedisCluster、追求性能

JedisCluster工作原理

工作原理:(具体可看源码:redis.clients.jedis.JedisClusterCommand#runWithRetries)

  • 1.从集群中选一个可运行节点,使用cluster slots初始化槽和节点映射。
  • 2.将cluster slots的结果映射到本地,为每个节点创建JedisPool。
  • 3.准备执行命令。

集群原理

批量操作

四种批量操作

四种批量操作实现优化(mget,mset必须在一个槽)

  • 串行mget:相当于for循环遍历keys去对应的集群节点get值,最后将结果汇总;简单效率低,需要n次的网络时间;
  • 串行IO:对串行mget优化,在client本地做內聚合将key的槽计算出,对key根据节点分组,之后通过几次pipeline操作即可;只需要节点个数次网络时间;
  • 并行IO:对串行IO优化,使用多线程,只需1次网络时间;
  • hash_tag:将key进行hash_tag包装,使所有key都在一个节点,只需要1次网络时间即可;

四种批量操作对比

故障转移

Redis Sentinel的故障转移依赖外部节点sentinel来实现
Redis Cluster自身实现了高可用,当前节点出了问题其他节点会监控得知,实现故障转移

故障发现:

通过节点间的ping/pong消息实现,不需要sentinel。

  • 主观下线:
    某个节点认为另一个节点不可用。

主观下线流程

  • 客观下线:
    客观下线:半数以上持有槽点主节点标记某节点主观下线。

客观下线流程

故障恢复:
  • 资格检查:
    资格检查

  • 准备选举时间:
    保证偏移量大的slave有更小的延迟达到选举时间,保证数据一致性更高。
    准备选举时间

  • 选举投票:
    选举投票

  • 替换主节点:
    替换主节点

常见问题

集群完整性

cluster-require-full-coverage默认为yes
表示是否需要所有集群节点都是在线的状态,所有的16384个槽是全部可用的,才会认为集群是完整的,才可以对外提供服务。
注意:如为yes

  • 节点故障或者正在故障转移时会有:(error)CLUSTERDOWN The cluster is down
  • 大多数业务无法容忍,cluster-require-full-coverage建议设置为no
宽带消耗
  • 官方建议节点不超过1000个,因为节点间进行ping/pong操作,过多会带来比较大的带宽消耗;

优化:

  • 避免“大”集群:避免多业务使用一个集群,大业务可以多集群。
  • cluster-node-timeout:带宽和故障转移速度的均衡。
  • 尽量均匀分配到多机器上:保证高可用和带宽
Pub/Sub广播模式的局限性:

问题:发布一条消息,每个节点都会接收到,加重带宽消耗。
解决:单独“走”一套Redis Sentinel。

集群倾斜问题:

数据倾斜和请求倾斜。

  • 数据倾斜:内存不均。
    4种原因:

  • 节点和槽分配不均;

  • 不同槽对应键值数量差异较大;

  • 包含bigkey;

  • 内存相关配置不一致;

  • 请求倾斜:热点数据(缓存常见问题)

集群的读写分离:

集群的读写分离
只读连接:集群模式下从节点不接受任何读写请求
如进行了读操作:会重定向到负责槽点主节点;
使用readonly命令可以读,是一个连接级别的命令。
上图7000为Master,7003为它的Salve。

集群的读写分离更复杂:

  • 同样的问题:复制延迟、读取过期数据、从节点故障
  • 修改客户端:cluster slaves {nodeld)
集群VS单机(sentinel/主从/单点):
  • 针对集群
    针对集群

  • 分布式集群redis不一定好。大部分场景下Redis Sentinel就足够了。
    集群

总结

  • Redis Cluster数据分区规则采用虚拟槽方式(16384个槽),每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。

  • 搭建集群包括四个步骤:准备节点、节点握手、分配槽、复制。

  • 集群伸缩通过在节点之间移动槽和相关数据实现。
    扩容时:根据槽迁移计划,把槽从源节点迁移到新节点;
    缩容时:如果下线的节点有负责的槽,就需要迁移到其他节点上,再通过cluster forget命令让集群内所有节点忘记并下线该节点。

  • 使用smart客户端操作集群打到通信效率最大化,客户端内部负责计算维护键->槽->节点的映射,用于快速定位到目标节点。

  • 集群自动故障转移过程包括:故障发现节点恢复
    节点下线包括主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时,标记它为客观下线状态。
    从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。

经典案例

Redis缓存

缓存的收益和成本?

收益:

  • 1、加速读写速度:
  • 2、降低后端负载:
    后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL负载等

成本:

  • 1、数据不一致:
    缓存层和数据层有时间窗口不一致,和更新策略有关;
  • 2、代码维护成本:
    要多加一层缓存逻辑;
  • 3、运维成本:
    如Redis Cluster维护;

缓存更新策略:

常见策略
三种缓存算法:(FIFO/LRU/LFU)
  • FIFO算法
    先进先出(FIFO,队列)。
    如果一个数据是最先进入的,那么可以认为在将来它被访问的可能性很小。空间满的时候,最先进入的数据会被最早置换(淘汰)掉。

  • LRU算法
    LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,如果空间不足淘汰掉最近最少使用的数据。。
    思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)。
    实现可参考:
    LRU算法实现测试

  • LFU算法
    LFU(Least Frequently Used ,最近最不常用算法),也就是淘汰一定时期内被访问次数最少的数据
    LFU 算法本质上可以看做是一个 top K 问题(K = 1),即选出频率最小的元素。
    因此可以用二项堆来选择频率最小的元素,这样的实现比较高效。最终实现策略为小顶堆+哈希表。

超时剔除:

时间过期时间

主动更新:

开发来控制缓存的生命周期;

redis的内存驱逐策略:

配置参数:maxmemory-policy noeviction
驱逐策略:内存容量超过maxmemory后的处理策略。

  • volatile-lru:利用LRU算法移除设置过过期时间的key。

    LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

  • volatile-random:随机移除设置过过期时间的key。

  • volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL)

  • allkeys-lru:利用LRU算法移除任何key。

  • allkeys-random:随机移除任何key。

  • noeviction:不移除任何key,只是返回一个写错误。

  • volatile-lfu:从已经设置过期时间的数据中,挑选最不经常使用的数据淘汰。

  • allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。

一致性问题
  • 低一致性:最大内存和淘汰策略
  • 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底。

常见缓存问题:

缓存穿透

缓存穿透,即大量缓存中不存在的请求key访问直接落到数据库,一般是恶意攻击;

  • 解决方案:
    有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器

    将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

    另外也有一个更为简单粗暴的方法:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存击穿

缓存击穿,指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞
这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key

  • 解决方案:
    添加互斥锁
    结合上面的击穿的情况,在第一个请求去查询数据库的时候对他加一个互斥锁,其余的查询请求都会被阻塞住,直到锁被释放,从而保护数据库。
    但是也是由于它会阻塞其他的线程,此时系统吞吐量会下降。需要结合实际的业务去考虑是否要这么做。
缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中过期失效

如:redis服务器挂掉导致请求大量涌至数据库;

而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

案例:
比如双十二活动,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

一般采取不同分类商品,缓存不同周期在同一分类中的商品,加上一个随机因子
这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。

解决方案:
  • 事前:
    使用集群缓存,保证缓存服务的高可用

    这种方案就是在发生雪崩前对缓存集群实现高可用,
    如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。

  • 事中:
    加本地缓存 + Hystrix限流&降级,避免MySQL被hit死。

    • 使用本地缓存的目的也是考虑在Redis Cluster 完全不可用的时候,本地缓存还能够支撑一阵。
    • 使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。
    • 然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。
  • 事后:
    开启Redis持久化机制,尽快恢复缓存集群。一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。

Redis实现分布式锁

分布式锁一般有三种实现方式:

    1. 数据库乐观锁;
    1. 基于Redis的分布式锁;
    1. 基于ZooKeeper的分布式锁。

确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性
    在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁
    即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 具有容错性
    只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 解铃还须系铃人
    加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";

/**
* 尝试获取分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
/*
* 执行下面的set()方法就只会导致两种结果:
* 1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
* 2. 已有锁存在,不做任何操作。
*
* 参数:
* - 第一个为key,我们使用key来当锁,因为key是唯一的。
* - 第二个为value,我们传的是requestId,因为分布式锁要满足第四个条件解铃还须系铃人,
* 通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。
* requestId可以使用UUID.randomUUID().toString()方法生成。
* - 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,
* 即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
* - 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
* - 第五个为time,与第四个参数相呼应,代表key的过期时间。
*/
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
return LOCK_SUCCESS.equals(result);
}

加锁代码满足我们可靠性里描述的三个条件。

  • 首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性
  • 其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁
  • 最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。
  • 由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

解锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static final Long RELEASE_SUCCESS = 1L;

/**
* 释放分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否解锁成功
*/
public static boolean releaseLock(Jedis jedis, String lockKey, String requestId) {
// Lua脚本代码:保证命令执行的原子性。
// 即eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
// 首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
// 将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。
// eval()方法是将Lua代码交给Redis服务端执行。
Object result = jedis.eval(script,
Collections.singletonList(lockKey),
Collections.singletonList(requestId));
return RELEASE_SUCCESS.equals(result);
}

评论