Redis-快速入门

简介

介绍

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo 写的key-value 存储系统。
Redis 是一个开源的使用ANSI C 语言编写、遵守BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的API 。
它通常被称为数据结构服务器,因为值value 可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets) 等类型。

安装

Windows

下载地址:https://github.com/MSOpenTech/redis/releases
Redis 支持32位和64位,根据你自己的需求自行下载所需的内容,下载后解压缩即可。
下图是解压后的文件夹内容:
1
接下来打开一个cmd 命令行窗口,进入到对应的目录下,运行一下命令:

1
redis-sever.exe redis.windows.conf

如果你想方便的话,可以将redis 的路径加入到系统的环境变量中。redis.windows.conf 可以省略,省略则采用默认配置。

以上运行的内容只是启动了Redis 服务器,下面开始连接Redis 服务器,进行测试。
重新启动一个cmd 命令行,切换到redis 的目录下。

1
2
3
redis-cli.exe -h 127.0.0.1 -p 6379
set myKey abc
get myKey

2

Linux

下载地址:http://redis.io/download
本教程使用的是最新文本文档为5.0.5,下载并安装。

1
2
3
4
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
tar xzf redis-5.0.5.tar.gz
cd redis-5.0.5
make

make 完成后,在目录下会出现编译后的redis-server,还有用于测试的客户端程序redis-cli,两个程序位于安装目录的src 之下。

1
2
cd src
./redis-server ../redis.conf

redis.conf 是一个配置文件,可以根据自己的需要使用自己的配置文件。
启动Redis 服务进程后,就可以测试客户端程序redis-cli 和redis 服务交互了。

1
2
3
4
cd src
./redis-cli
set myKey abc
get myKey
Ubuntu

在Ubuntu 中安装Redis 就很简单了。

1
2
sudo apt-get update
sudo apt-get install redis-server

启动Redis 。

1
redis-server

查看Redis 是否启动?

1
redis-cli

下面是上述命令打开的终端。

1
2
redis 127.0.0.1:6379> ping
PONG

配置

Redis 的配置文件位于Redis 安装目录下,文件名为redis.conf(Windows 名为redis.windows.conf)。可以通过CONFIG 命令查看或设置配置项,也可以通过修改redis.conf 文件或使用CONFIG set 命令来修改配置。
Redis CONFIG GET 命令格式如下:

1
redis 127.0.0.1:6379> CONFIG GET CONFIG_SETTING_NAME

Redis CONFIG SET 命令基本语法:

1
redis 127.0.0.1:6379> CONFIG SET CONFIG_SETTING_NAME NEW_CONFIG_VALUE

入门

命令

Redis 的命令用于在redis 服务上执行,要在redis 服务商执行命令需要一个redis 客户端。前边下载的redis 的安装包中就有。
redis-cli 命令:

1
redis-cli -h host -p port -a password
本地启动redis 客户端
1
2
3
4
$redis-cli
redis 127.0.0.1:6379>
redis 127.0.0.1:6379> PING
PONG
启用远程服务
1
2
3
4
$redis-cli -h 127.0.0.1 -p 6379 -a "mypass"
redis 127.0.0.1:6379>
redis 127.0.0.1:6379> PING
PONG

数据类型

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

String(字符串)

string 类型是二进制安全的

1
2
3
4
redis 127.0.0.1:6379> SET name "hello World"
OK
redis 127.0.0.1:6379> GET name
"hello World"

一个键最大能存储512MB

hash(哈希)

Redis hash 是一个键值(key=>value )对集合。
Redis hash 是一个string 类型的field 和value 的映射表,hash 特别适合用于存储对象。

1
2
3
4
5
6
redis 127.0.0.1:6379> HMSET myhash field1 "Hello" field2 "World"
"OK"
redis 127.0.0.1:6379> HGET myhash field1
"Hello"
redis 127.0.0.1:6379> HGET myhash field2
"World"
list(列表)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

1
2
3
4
5
6
7
8
9
10
redis 127.0.0.1:6379> lpush vgbh redis
(integer) 1
redis 127.0.0.1:6379> lpush vgbh mongodb
(integer) 2
redis 127.0.0.1:6379> lpush vgbh rabitmq
(integer) 3
redis 127.0.0.1:6379> lrange vgbh 0 10
1) "rabitmq"
2) "mongodb"
3) "redis"
set(集合)

Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
sadd 命令
添加一个string 元素到key 对应的set 集合中,成功返回1,如果元素已经在集合中返回0,如果key 对应的set 不存在则返回错误。

1
2
3
4
5
6
7
8
9
10
11
12
redis 127.0.0.1:6379> sadd vgbh redis
(integer) 1
redis 127.0.0.1:6379> sadd vgbh mongodb
(integer) 1
redis 127.0.0.1:6379> sadd vgbh rabitmq
(integer) 1
redis 127.0.0.1:6379> sadd vgbh rabitmq
(integer) 0
redis 127.0.0.1:6379> smembers vgbh
1) "redis"
2) "rabitmq"
3) "mongodb"

实例中 rabitmq 添加了两次,但根据集合内元素的唯一性,第二次插入的元素将被忽略。

zset(sorted set:有序集合)

Redis zset 和set 一样也是string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
zset 的成员是唯一的,但分数(score)却可以重复。
zadd 命令
添加元素到集合,元素在集合中存在则更新对应score

1
2
3
4
5
6
7
8
9
10
11
12
redis 127.0.0.1:6379> zadd vgbh 0 redis
(integer) 1
redis 127.0.0.1:6379> zadd vgbh 0 mongodb
(integer) 1
redis 127.0.0.1:6379> zadd vgbh 0 rabitmq
(integer) 1
redis 127.0.0.1:6379> zadd vgbh 0 rabitmq
(integer) 0
redis 127.0.0.1:6379> > ZRANGEBYSCORE vgbh 0 1000
1) "mongodb"
2) "rabitmq"
3) "redis"

事务

Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:

  • 批量操作在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

示例:
以下是一个事务的例子,它先以MULTI 开始一个事务,然后将多个命令入队到事务中,最后由EXEC 命令触发事务, 一并执行事务中的所有命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days"
QUEUED
redis 127.0.0.1:6379> GET book-name
QUEUED
redis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

这个是官网上的说明:

1
It's important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands.

发布订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 的SUBSCRIBE 命令可以让客户端订阅任意数量的频道,每当有新信息发送到被订阅的频道时,信息就会被发送给所有订阅指定频道的客户端。
1
当有新消息通过PUBLISH 命令发送给频道channel1 时,这个消息就会被发送给订阅它的三个客户端:
2

示例

创建一个订阅频道为redisChat。

1
2
3
4
5
redis 127.0.0.1:6379> SUBSCRIBE redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1

重新开启一个redis 客户端,然后在redisChat 发布两次消息,订阅者就会收到消息。

1
2
3
4
5
6
7
8
9
10
11
12
redis 127.0.0.1:6379> PUBLISH redisChat "Redis is a great caching technique"
(integer) 1
redis 127.0.0.1:6379> PUBLISH redisChat "Learn redis by vgbh.cn"
(integer) 1

# 订阅者的客户端会显示如下消息
1) "message"
2) "redisChat"
3) "Redis is a great caching technique"
1) "message"
2) "redisChat"
3) "Learn redis by vgbh.cn"
全部命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
PSUBSCRIBE pattern [pattern ...] 
订阅一个或多个符合给定模式的频道。
PUNSUBSCRIBE [pattern [pattern ...]]
退订所有给定模式的频道。

PUBSUB subcommand [argument [argument ...]]
查看订阅与发布系统状态。

SUBSCRIBE channel [channel ...]
订阅给定的一个或多个频道的信息。
PUBLISH channel message
将信息发送到指定的频道。
UNSUBSCRIBE [channel [channel ...]]
指退订给定的频道。

脚本

Redis 脚本使用Lua 解释器来执行脚本。 Redis 2.6 版本通过内嵌支持Lua 环境。执行脚本的常用命令为 EVAL

语法

Eval 命令的基本语法如下:

1
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
示例
1
2
3
4
5
redis 127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
Redis 脚本命令
1
2
3
4
5
6
7
8
9
10
11
12
EVAL script numkeys key [key ...] arg [arg ...] 
执行Lua 脚本
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
执行Lua 脚本
SCRIPT EXISTS script [script ...]
查看指定的脚本是否已经被保存在缓存当中
SCRIPT FLUSH
从脚本缓存中移除所有脚本
SCRIPT KILL
杀死当前正在运行的Lua 脚本
SCRIPT LOAD script
将脚本script 添加到脚本缓存中,但并不立即执行这个脚本

服务器

Redis 服务器命令主要是用于管理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
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
redis 127.0.0.1:6379> INFO

# Server
redis_version:2.8.13
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:c2238b38b1edb0e2
redis_mode:standalone
os:Linux 3.5.0-48-generic x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.7.2
process_id:3856
run_id:0e61abd297771de3fe812a3c21027732ac9f41fe
tcp_port:6379
uptime_in_seconds:11554
uptime_in_days:0
hz:10
lru_clock:16651447
config_file:

# Clients
connected_clients:1
client-longest_output_list:0
client-biggest_input_buf:0
blocked_clients:0

# Memory
used_memory:589016
used_memory_human:575.21K
used_memory_rss:2461696
used_memory_peak:667312
used_memory_peak_human:651.67K
used_memory_lua:33792
mem_fragmentation_ratio:4.18
mem_allocator:jemalloc-3.6.0

# Persistence
loading:0
rdb_changes_since_last_save:3
rdb_bgsave_in_progress:0
rdb_last_save_time:1409158561
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
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

# Stats
total_connections_received:24
total_commands_processed:294
instantaneous_ops_per_sec:0
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
evicted_keys:0
keyspace_hits:41
keyspace_misses:82
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:264

# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

# CPU
used_cpu_sys:10.49
used_cpu_user:4.96
used_cpu_sys_children:0.00
used_cpu_user_children:0.01

# Keyspace
db0:keys=94,expires=1,avg_ttl=41638810
db1:keys=1,expires=0,avg_ttl=0
db3:keys=1,expires=0,avg_ttl=0
Redis 服务器命令

具体的命令可以在需要使用时查询可得。

HyperLogLog

Redis 在2.8.9 版本添加了HyperLogLog 结构。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在Redis 里面,每个HyperLogLog 键只需要花费12 KB 内存,就可以计算接近2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

基数

比如数据集 {1, 3, 5, 7, 5, 7, 8},那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

1
2
3
4
5
6
7
8
redis 127.0.0.1:6379> PFADD vgbhkey "redis"
1) (integer) 1
redis 127.0.0.1:6379> PFADD vgbhkey "mongodb"
1) (integer) 1
redis 127.0.0.1:6379> PFADD vgbhkey "mysql"
1) (integer) 1
redis 127.0.0.1:6379> PFCOUNT vgbhkey
(integer) 3
全部命令
1
2
3
4
5
6
PFADD key element [element ...] 
添加指定元素到HyperLogLog 中
PFCOUNT key [key ...]
返回给定HyperLogLog 的基数估算值
PFMERGE destkey sourcekey [sourcekey ...]
将多个HyperLogLog 合并为一个HyperLogLog

高级

数据备份与恢复

备份数据

Redis SAVE 命令用于创建当前数据库的备份。
语法:

1
redis 127.0.0.1:6379> SAVE
恢复数据

如果需要恢复数据,只需将备份文件(dump.rdb) 移动到redis 安装目录并启动服务即可。获取redis 目录可以使用CONFIG 命令,如下所示:

1
2
3
redis 127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/redis/bin"
Bgsave

创建redis 备份文件也可以使用命令 BGSAVE,该命令在后台执行。
示例:

1
2
127.0.0.1:6379> BGSAVE
Background saving started

安全

可以通过redis 的配置文件设置密码参数,这样客户端连接到redis 服务就需要密码验证,这样可以让你的redis 服务更安全。

示例

可以通过以下命令查看是否设置了密码验证:

1
2
3
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) ""

默认情况下requirepass 参数是空的,这就意味着无需通过密码验证就可以连接到redis 服务。
通过以下命令来修改该参数:

1
2
3
4
5
127.0.0.1:6379> CONFIG set requirepass "vgbh"
OK
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) "vgbh"

设置密码后,客户端连接redis 服务就需要密码验证,否则无法执行命令。

使用

语法:

1
127.0.0.1:6379> AUTH password

示例:

1
2
3
4
5
6
127.0.0.1:6379> AUTH "vgbh"
OK
127.0.0.1:6379> SET mykey "Test value"
OK
127.0.0.1:6379> GET mykey
"Test value"

管道技术

Redis 是一种基于客户端-服务端模型以及请求/响应协议的TCP 服务。这意味着通常情况下一个请求会遵循以下步骤:

  • 客户端向服务端发送一个查询请求,并监听Socket 返回,通常是以阻塞模式,等待服务端响应。
  • 服务端处理命令,并将结果返回给客户端。
Redis 管道技术

Redis 管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。
示例:
查看Redis 管道,启动一个redis 客户端并输入一下命令。

1
2
3
4
5
6
7
$(echo -en "PING\r\n SET vgbhkey redis\r\nGET vgbhkey\r\nINCR visitor\r\nINCR visitor\r\nINCR visitor\r\n"; sleep 10) | nc localhost 6379
+PONG
+OK
redis
:1
:2
:3

在返回的结果中我们可以看到这些命令一次性向redis 服务提交,并最终一次性读取所有服务端的响应

管道技术的优势

管道技术最显著的优势是提高了Redis 服务的性能。

下面的测试使用了Redis 的Ruby 客户端,支持管道技术特性,测试管道技术对速度的提升效果。

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
require 'rubygems' 
require 'redis'
def bench(descr)
start = Time.now
yield
puts "#{descr} #{Time.now-start} seconds"
end
def without_pipelining
r = Redis.new
10000.times {
r.ping
}
end
def with_pipelining
r = Redis.new
r.pipelined {
10000.times {
r.ping
}
}
end
bench("without pipelining") {
without_pipelining
}
bench("with pipelining") {
with_pipelining
}

下面是结果:

1
2
without pipelining 1.185238 seconds 
with pipelining 0.250783 seconds

如你所见,开启管道后,我们的速度效率提升了5倍。

分区

分区是分割数据到多个Redis 实例的处理过程,因此每个实例只保存key 的一个子集。

分区的优势

通过利用多台计算机内存的和值,允许我们构造更大的数据库。

  • 通过多核和多台计算机,允许我们扩展计算能力。
  • 通过多台计算机和网络适配器,允许我们扩展网络带宽。
分区类型

Redis 有两种类型分区。 假设有4个Redis 实例R0、R1、R2、R3,和类似user:1、user:2这样的表示用户的多个key ,对既定的key 有多种不同方式来选择这个key 存放在哪个实例中。也就是说,有不同的系统来映射某个key 到某个Redis 服务。

分区的不足

Redis 的一些特性在分区方面表现的不是很好:

  • 涉及多个key 的操作通常是不被支持的。当两个set 映射到不同的redis 实例上时,就不能对这两个set 执行交集操作。
  • 涉及多个key 的redis 事务不能使用。
  • 当使用分区时,数据处理较为复杂,比如你需要处理多个rdb/aof文件,并且从多个实例和主机备份持久化文件。
  • 增加或删除容量也比较复杂。redis集群大多数支持在运行时增加、删除节点的透明数据平衡的能力,但是类似于客户端分区、代理等其他系统则不支持这项特性。一种叫做presharding的技术对此是有帮助
范围分区

最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的Redis实例。
比如,ID 从0到10000的用户会保存到实例R0,ID 从10001到 20000的用户会保存到R1,以此类推。这种方式是可行的,并且在实际中使用,不足就是要有一个区间范围到实例的映射表。这个表要被管理,同时还需要各种对象的映射表,通常对Redis 来说并非是好的方法。

哈希分区

另外一种分区方法是HASH 分区。这对任何key 都适用,也无需是object_name: 这种形式,像下面描述的一样简单:
用一个hash 函数将key 转换为一个数字,比如使用crc32 hash函数。对key foobar 执行crc32(foobar)会输出类似93024922的整数。对这个整数取模,将其转化为0-3 之间的数字,就可以将这个整数映射到4个Redis 实例中的一个了。93024922 % 4 = 2 ,就是说key foobar 应该被存到R2 实例中。
取模操作是取除的余数,通常在多种编程语言中用%操作符实现。

主从复制

作用
  1. 数据冗余。 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复。 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余。
  3. 负载均衡。 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis 数据时应用连接主节点,读Redis 数据时应用连接从节点),分担服务器负载。尤其在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis 服务器的并发量。
  4. 读写分离。 可以用于实现读写分离,主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可以根据需求的变化,改变从库的数量。
  5. 高可用基石。 除了上述的作用之外,主从复制还是哨兵和集群能够实现的基础,因此说主从复制时Redis 高可用的基础。
使用

从节点开启主从复制,有三种方式:

  • 配置文件。 在从服务器的配置文件中加入:slaveof。
  • 启动命令。 redis-server 启动命令后加入 –slaveof。
  • 客户端命令。 Redis 服务器启动后,直接通过客户端执行命令:slaveof,则该Redis 实例成为从节点。
原理

主从复制,是指将一台Redis 服务器的数据,复制到其他的Redis 服务器。前者称为主节点(master) ,后者称为从节点(slave) ,数据的复制是单向的,只能由主节点到从点。
默认情况下,每台Redis 服务器都是主节点,且一个主节点可以有多个从节点(或者没有从节点),但一个从节点只能有一个主节点。

主从复制过程大概可以分为三个阶段:连接建立阶段数据同步阶段命令传播阶段
在从节点执行slaveof 命令之后,复制过程便开始运作,下面可以看到,从图中可以看出大致分为六个过程:
1

主从配置之后在日志记录中也可以看出这个流程

  1. 保存主节点(master) 信息。
    执行slaveof 后Redis 会打印如下日志:
    1
    [8788] 05 Jul 14:12:19.513 * SLAVE OF 127.0.0.1:6379 enabled (user request from 'id=2 addr=127.0.0.1:5254 fd=7 name= age=15 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=18446744073709537584 events=r cmd=slaveof')
  2. 从节点(slave) 内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。
    3.jpg
    从节点会建立一个socket 套接字,从节点建立了一个端口为51234 的套接字,专门用于接受主节点发送的复制命令。
    从节点连接成功后打印如下日志:
    1
    2
    [8788] 05 Jul 14:12:19.548 * Connecting to MASTER 127.0.0.1:6379
    [8788] 05 Jul 14:12:19.548 * MASTER <-> SLAVE sync started
    如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行slaveof no one 取消复制。
    关于连接失败,可以在从节点执行info replication 查看master_link_down_since_seconds 指标,他会记录与主节点连接失败的系统时间。
    从节点连接主节点失败时也会每秒打印如下日志,方便发性问题:
    1
    # Error condition on socket for SYNC: {socket_error_reason}
  3. 发送PING 命令。连接建立成功后从节点发送PING 请求进行首次通信,PING 请求主要目的如下:
  • 检测主从之间网络套接字是否可用。
  • 检测主节点当前是否可接受处理命令。

如果发送PING 命令后,从节点没有收到主节点的PONG 回复或者超时,比如网络超时或者主节点正在阻塞无法响应命令,从节点会断开复制链接,下次定时任务会发起重连。
从节点发送的PING 命令成功返回,Redis 打印如下日志,并继续后续复制流程:

1
[8788] 05 Jul 14:12:19.551 * Master replied to PING, replication can continue...
  1. 权限验证。 如果主节点设置了requirepass 参数,则需要密码验证,从节点必须配置masterauth 参数保证与主节点相同的密码才能通过验证。
    如果验证失败复制将终止,从节点重新发起复制流程。
    1
    [8788] 05 Jul 14:12:19.570 * Full resync from master: 8e50b47ea04a75d6a14d0d434687d1c58426db00:1
  2. 同步数据集。主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是最耗时的步骤。
    1
    2
    3
    4
    [8788] 05 Jul 14:12:19.944 * MASTER <-> SLAVE sync: receiving 91 bytes from master
    [8788] 05 Jul 14:12:19.947 * MASTER <-> SLAVE sync: Flushing old data
    [8788] 05 Jul 14:12:19.947 * MASTER <-> SLAVE sync: Loading DB in memory
    [8788] 05 Jul 14:12:19.949 * MASTER <-> SLAVE sync: Finished with success
  3. 命令持续复制。当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。
    接下来主节点会持续地把命令发送给从节点,保证主从数据一致性。
解决方法

在现有企业中80% 的公司大部分使用的是Redis 单机服务器,在实际的场景当中单一节点的Redis 服务器容易面临风险。

主要面临的问题:

  • 机器故障。我们部署到一台Redis 服务器,当机器发生故障时,需要迁移到另外一台服务器并且要保证数据是同步的。
  • 容量瓶颈。当我们需求需要扩展Redis 内存时,从16G 升级到64G ,单机肯定满足不了。

解决方法:
要实现分布式数据库的更大的存储容量和承受高并发访问量,我们会将原本集中式数据库的数据分别存储在其他多个网络节点上。

Redis 为了解决这个单一节点的问题,也会把数据复制多个副本部署到其他节点上进行复制,实现Redis 的高可用,实现对数据的冗余备份,从而保证数据和服务的高可用。

哨兵和复制

Redis 利用这两个功能来保证Redis 的高可用。

哨兵(Sentinel)

Sentinel 可以管理多个Redis 服务器,它提供了监控、提醒以及自动故障转移的功能。
此外,Sentinel 功能是对Redis 的发布与订阅功能的一个利用。

复制(Replication)

Replication 是负责让一个Redis 服务器可以配备多个备份的服务器。

持久化机制

前面说的都是如何使用Redis,并且是如何更好的使用Redis,那么下面的说的持久化机制就是在Redis 出现异常之后,通过那些机制可以保持Redis 中的数据不丢失,并且可以快速恢复服务。

机制
  • RDB 持久化
    RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。

  • AOF 持久化
    AOF 持久化是指在每收到一个写入的命令时都通过write() 函数添加到文件中,默认文件名称为appendonly.aof 。

RDB 持久化机制

RDB 持久化机制这种方式也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。

  1. 配置

    1
    2
    3
    save 900 1      # 900秒内如果超过1个key被修改,则发起快照保存
    save 300 10 # 300秒内如果超过10个key被修改,则发起快照保存
    save 60 10000 # 60秒内如果超过10000个key被修改,则发起快照保存
  2. 优势

  • 备份非常方便,整个Redis 数据库只有一个文件。
  • RDB 在恢复数据时的速度相较于AOF 要快。
  • RDB 可以最大化Redis 的性能。在进行备份时,备份操作由子进程操作,不影响父进程的I/O操作。
  1. 劣势
  • 因为触发RDB 备份操作的原因,可能在Redis 出现异常时的前几分钟内的数据都会丢失。
AOF 持久化机制
  1. 配置

    1
    2
    3
    4
    appendonly yes         # 启用aof持久化方式
    # appendfsync always # 每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
    appendfsync everysec # 每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
    # appendfsync no # 完全依赖os,性能最好,持久化没保证
  2. 优势

  • 使用AOF 持久化机制会让Redis 的数据变得非常耐久,即使不设置fsync 策略,每秒钟fsync 一次,那么在这种配置下,即使Redis 出现异常,也仅仅只是丢失了前一秒的数据。
  • AOF 持久化机制是一种追加日志的方式实现的数据恢复,这样即便是因为其他的外界因素导致命令不完全或者其他的因素,redis-check-aof也可以修复这些问题。
  • AOF 持久化机制的重写,当AOF 文件非常大时,后台会自动的对AOF 文件进行重写,重写后的AOF 文件包含了回复当前数据所需的最小命令集。
  • AOF 文件中的命令是有序的,因此在维护人员进行了非法操作后,也可以通过备份的AOF 文件中命令集,删除掉错误的命令,也可以恢复到操作之前的数据环境。
  1. 劣势
  • AOF 文件的大小比RDB 的要大。
  • 根据所使用的fsync 策略,AOF 持久化机制所需要的时间会比RDB 所需时间要多。
  • AOF 在特定操作下存在bug。比如执行阻塞命令BRPOPLPUSH 会导致数据无法恢复成保存之前的样子。
使用选择
  • 项目对于数据的安全性非常看重的话,建议两种持久化机制同时使用。
  • 如果非常关心数据,但又觉得损失几分钟的数据损失不大,那么RDB 会比较好。
  • 其余的情况,推荐使用AOF 机制。

使用

Java 使用

前提
  1. JDK 环境
  2. Java redis 驱动包
  3. classpath 中包含该驱动包
代码

连接到redis 服务:

1
2
3
4
5
6
7
8
9
10
11
import redis.clients.jedis.Jedis;

public class RedisJava {
public static void main(String[] args) {
//连接本地的 Redis 服务
Jedis jedis = new Jedis("localhost");
System.out.println("连接成功");
//查看服务是否运行
System.out.println("服务正在运行: "+jedis.ping());
}
}

测试

1
2
连接成功
服务正在运行: PONG

Redis Java String 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import redis.clients.jedis.Jedis;

public class RedisStringJava {
public static void main(String[] args) {
//连接本地的 Redis 服务
Jedis jedis = new Jedis("localhost");
System.out.println("连接成功");
//设置 redis 字符串数据
jedis.set("vgbhkey", "www.vgbhfive.cn");
// 获取存储的数据并输出
System.out.println("redis 存储的字符串为: "+ jedis.get("vgbhkey"));
}
}

测试

1
2
连接成功
redis 存储的字符串为: www.vgbhfive.cn

分布式锁

详细内容见Zookeeper-分布式锁

Spring Boot 使用

https://github.com/vgbhfive/SpringBootDemo

高可用Redis 服务架构

Sentinel(哨兵)

Redis Sentinel 可以理解为一个监控Redis Server 服务是否运行正常的进程,并且一旦检测出不正常,可以自动地将备份(slave) Redis Server 启用,使得外部用户对Redis 服务内部出现的异常无感知。

单机版Redis Server,无Sentinel

1
一般情况下,我们搭建的个人网站、开发时起的单实例Redis Server。调用后直接连上Redis 服务即可,甚至Client 和Redis 本身就处于同一台服务器上。这种搭配也仅限于学习娱乐,毕竟这种配置总会由单点问题无法解决。一旦Redis 服务挂掉,那么服务就不可用了,并且如果没有配置Redis 数据持久化的话,Redis 内部存储的数据也会丢失。

主从同步Redis Server,单实例Sentinel

2
为了实现高可用,解决方案1中的单点故障问题,因此决定增加一个备份服务,即在两台服务器上分别各启动一个Redis Server 进程,一般情况下,master 提供服务,而slave 只负责同步和备份。
与此同时,再额外启动一个Sentinel 进程,监控两个Redis Server 实例的可用性,以便在master 挂掉的时候,及时把slave 提升为master 角色继续提供服务,这样就实现了Redis 的高可用。
对于Redis 服务的调用方来说,现在要连接的是Redis Sentinel 服务,而不是Redis Server了。常见的调用过程就是,Client 先连接Redis Sentinel 并询问当前Redis Server 中哪一个服务是master ,然后再去连接相应的Redis Server 进行操作。
然而,我们实现了Redis Server 服务的主从切换后,又引入了一个新的问题,即Redis Sentinel 本身也是一个单点服务,一旦Sentinel 进程挂掉,那么客户端也没有办法链接Sentinel 了。

主从同步Redis Server,双实例Sentinel

3
为了解决方案2的问题,现在把Sentinel 进程也额外启动一份,两个Sentinel 进程同时为客户端提供服务发现的功能。
对于客户端来说,他可以链接任意一个Redis Sentinel 服务,来获取当前的Redis Server 实例的基本信息。通常情况下,会在Client 端配置多个Redis Sentinel 的链接地址,Client 发现其中一个链接失败,就回去链接其他的Sentinel 实例。
然而,现实很残酷。红线部分是俩台服务器之间的通信,假设服务器1停机,只剩下服务器2上的Redis Sentinel 和Redis Server 进程。这时,Sentinel 其实是不会将仅剩的slave 切换为master 继续服务的,这也就导致了Redis 服务不可用。因为Redis 的设定只有当超过50% 的Sentinel 进程可以连通并投票选取新的master,才会真正发生主从切换
本例中的两个Sentinel 只有一个可以连通,等于50% 并不在可以主从切换的场景中。
假如允许小于等于50% 的Sentinel 连通的场景下可以进行主从切换,会发生什么呢?

主从同步Redis Server,三实例Sentinel

4
鉴于方案3的问题,最终版本就是方案4了。
在引入了服务器3后,在服务器3上面又搭建起一个Redis Sentinel进程,现在由三个Sentinel进程来管理两个Redis Server实例。这种场景下,不管是单一进程故障、还是单个机器故障、还是某两个机器网络通信故障,都可以继续对外提供Redis服务。
实际上,如果你的机器比较空闲,当然也可以把服务器3上面也开启一个Redis Server,形成1 master + 2 slave的架构,每个数据都有两个备份,可用性会提升一些。当然也并不是slave 越多越好,毕竟主从同步也是需要时间成本的。

最终解决方案

5.jpg
作为服务的提供方,我们总是会讲到用户体验问题。对于单机版Redis,Client 端直接连接Redis Server,我们只需要给一个ip 和port ,Client 就可以使用服务了。
有没有办法还是像在使用单机版的Redis 那样,只给Client 一个固定的ip 和port 就可以提供服务呢?
答案当然是肯定的,这就要引入虚拟IP(Virtual IP,VIP),可以把虚拟IP 指向Redis Server master 所在的服务器,在发生Redis 主从切换的时候,会触发一个回调脚本,回调脚本中将VIP 切换至slave 所在的服务器。这样对于Client 端来说,他仿佛在使用的依然是一个单机版的高可用Redis 服务。
在实际业务使用中,还会启用supervisor 做进程监控,一旦程序意外退出,会自动尝试重新启动。

开源解决方案

对于搭建高可用Redis 服务,网上已有了很多方案,例如Keepalived、Codis、Twemproxy、Redis Sentinel。
其中Codis 和Twemproxy 主要是用于大规模的Redis 集群中,也是在Redis 官方发布Redis Sentinel 之前twitter 和豌豆荚提供的开源解决方案。
最终在Keepalived 和Redis Sentinel 之间做了选择,选择了官方的解决方案Redis Sentinel。


面试

缓存雪崩

解释

缓存雪崩可以简单的理解为:由于原有的大量缓存失效,而新的缓存还未到达,就会导致原本查询缓存的请求全部去查询数据库,造成数据库短时的巨大压力,造成系统的崩溃。

解决
  1. 设置缓存的过期时间时,加上一个随机值,避免大量的缓存在同一时间集体过期。
  2. 或者使用队列的方式防止不会有大量的请求同时对数据库进行操作。
  3. 给每一个缓存数据增加相应的缓存标记,记录缓存标记是否失效,若失效,则更新数据缓存。
  4. 使用二级缓存(具体的使用可以自行研究)
  5. Redis 挂掉,请求全部走数据库。
  • 事发前:实现Redis 的高可用,可使用高可用的服务架构,避免这种情况的发生。
  • 事发中:设置**本地缓存(ehcache)限流(hystrix)**,避免数据库崩溃。
  • 事发后:Redis 持久化,重启后自动从磁盘加载数据,快速恢复缓存数据。

缓存穿透

解释

缓存穿透是指:用户在查询数据的时候,在数据库中没有找到,自然在缓存中也没有。
这样也就导致用户查询时,在缓存中未找到,每次都要去查询数据库,然后返回空内容。这样的请求就饶过缓存直接查询数据库,这也就是常提到的缓存命中率问题。

解决
  1. 采用布隆过滤器。将所有可能存在的数据哈希到一个巨大的bitmap 中,一个一定不存在的数据会被这个bitmap 拦截,从而避免对数据库的查询压力。
  2. 将查询为空的结果进行缓存,但过期时间会很短。遇到再一次请求时,会直接返回空结果。

缓存击穿

解释

缓存击穿是指:某一时刻,大量请求同时查询一个 key,而此时这个 key 已经失效,这就会导致大量的请求都进入到数据库上,造成短时数据库压力剧增。

解决

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

缓存预热

解释

缓存预热是指:在系统上线后,将相关的缓存数据直接加载到缓存系统。这样可以用户直接查询事先被预热的缓存数据。

解决
  1. 设置缓存刷新页面,上线时手动操作。
  2. 在项目启动时,自动进行加载数据。
  3. 定时刷新缓存。

缓存更新

我们在使用缓存时,除了Redis 自带的缓存失效策略之外,我们还可以根据具体的业务自定义淘汰策略。

解决
  1. 定时清理过期的缓存。
  2. 当有用户请求时,再判断这个请求所用到的缓存是否过期,过期就到数据库读取并更新缓存。

缓存降级

缓存降级是指:在当访问量、服务出现问题或非核心服务受到影响时,仍然需要保证服务是可用的,即使时有损服务。
系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的目的在于保证核心服务可用,即便是有损的,而且有些服务是无法降级的。
在进行降级前需要对系统进行梳理,看看哪些系统是可以丢卒保帅,从而梳理出哪些必须誓死保护,哪些可以降级。
比如可以参考日志级别设置方案

  • 一般:一些服务因为网络波动或者服务正在上线,可以自动降级。
  • 警告:有些服务在一段时间内成功率有波动,可以自动降级或人工降级,并发送告警。
  • 错误:比如利用率偏低、数据库连接池已满等,可以自动降级或人工降级。
  • 严重错误;因为特殊原因数据报错,此时就需要紧急人工降级。

缓存与数据库双写一致

在正常的请求中,会首先查询缓存,如果不存在就回去查询数据库,查询到数据后,将查询到的数据写入到缓存中,最后将数据返回给请求。

产生问题的原因

如果请求是查询数据时,不会产生这个问题,那当更新数据时,各种情况就会造成数据库和缓存的数据不一致了。

解决
  1. 重试机制,但会侵入业务代码
  • 更新数据库。
  • 删除缓存,删除成功则退出。
  • 若缓存删除失败,则将需要删除的 key 加入到队列中,提供重试机制。
  • 从队列中获取到需要删除的 key。
  • 重复删除操作。
  1. 重试机制,但比较复杂
  • 更新数据库数据。
  • 数据库将数据操作信息写入到 binlog 日志中。
  • 订阅程序提取出所需要的数据和 key。
  • 启动另外一段非业务代码,获取该数据和 key。
  • 尝试删除缓存数据,成功则退出,失败则将数据和 key 发送到队列中。
  • 重新从队列中获取到数据和 key,并重复删除缓存操作。
    上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。

解决方案

Redis 热点Key 问题发现

产生原因
  1. 用户消费的数据远大于产生的数据(热卖产品、热点新闻、热点评论、明星直播)。
    在日常生活中的一些突发事件。比如双十一期间某些热门商品的低价销售,当这中的某一件商品被点击上万次后,会形成一个较大的需求量,这是就会出现热点问题。
  2. 请求分片集中,超过单Server 的性能极限。
    在服务端读数据进行访问时,往往会对数据进行分片切分,此过程中会在某一主机Server 上对相应的Key 进行访问,当访问超过Server 极限时,就会导致热点Key 问题的产生。
危害
  1. 流量集中,达到物理网卡的上限。
    当某一热点Key 的请求在某一主机上超过该主机网卡上限时,由于流量的过度集中,会导致服务器中其它服务无法进行。
  2. 请求过多,缓存分片服务被打垮。
    如果热点过于集中,热点Key 的缓存过多,超过目前的缓存容量时,就会导致缓存分片服务被打垮现象的产生。
  3. DB 击穿,引起业务雪崩。
    当缓存服务崩溃后,此时再有请求产生,会缓存到后台DB 上,由于DB 本身性能较弱,在面临大请求时很容易发生请求穿透现象,会进一步导致雪崩现象,严重影响设备的性能。
解决方案

通常的解决方案主要集中在对客户端和 Server 端进行相应的改造。

  1. 服务端缓存方案
    1.jpg
    首先Client 会将请求发送至Server 上,而Server 又是一个多线程的服务,本地就有一个基于Cache LRU 策略的缓存空间。
    当Server 本身就拥堵时,Server 不会将请求进一步发送给DB 而是直接返回,只有当Server 本身畅通时才会将Client 请求发送至 DB,并且将该数据重新写入到缓存中。此时就完成了缓存的访问跟重建。
  2. 使用Memcache、Redis 方案
    2.jpg
    该方案通过在客户端单独部署缓存的方式来解决热点Key 问题。
    使用过程中Client 首先访问服务层,再对同一主机上的缓存层进行访问。该种解决方案具有就近访问、速度快、没有带宽限制的优点。
  3. 使用本地缓存方案
    使用本地缓存则存在以下问题:
  • 需要提前获知热点。
  • 缓存容量有限。
  • 不一致性时间增长。
  • 热点 Key 遗漏

传统的热点解决方案都存在各种各样的问题,那么究竟该如何解决热点问题呢?
4. 读写分离方案解决热点问题
4.jpg
架构中各节点的作用如下:

  • SLB 层做负载均衡
  • Proxy 层做读写分离自动路由
  • Master 负责写请求
  • ReadOnly 节点负责读请求
  • Slave 节点和 Master 节点做高可用

实际过程中Client 将请求传到SLB,SLB 又将其分发至多个Proxy 内,通过Proxy 对请求的识别,将其进行分类发送。
例如,将同为Write 的请求发送到Master 模块内,而将Read 的请求发送至ReadOnly 模块。
而模块中的只读节点可以进一步扩充,从而有效解决热点读的问题。
读写分离同时具有可以灵活扩容读热点能力、可以存储大量热点Key 、对客户端友好等优点。

  1. 热点数据解决方案
    5.jpg
    该方案通过主动发现热点并对其进行存储来解决热点Key 的问题。

首先Client 也会访问SLB,并且通过SLB 将各种请求分发至Proxy 中,Proxy 会按照基于路由的方式将请求转发至后端的Redis 中。
在热点key 的解决上是采用在服务端增加缓存的方式进行。
具体来说就是在Proxy 上增加本地缓存,本地缓存采用LRU 算法来缓存热点数据,后端DB 节点增加热点数据计算模块来返回热点数据。
Proxy 架构的主要有以下优点:

  • Proxy 本地缓存热点,读能力可水平扩展。
  • DB 节点定时计算热点数据集合。
  • DB 反馈 Proxy 热点数据。
  • 对客户端完全透明,不需做任何兼容。

接口幂等性校验

概念

幂等性是指:一个接口,多次发起同一个请求,必须保证操作只执行一次。

常见解决方案
  1. 唯一索引,防止新增脏数据。
  2. Token机制,防止页面重复提交。
  3. 悲观锁,获取数据的时候加锁(锁表或锁行)。
  4. 乐观锁,基于版本号version 实现,再更新数据那一刻校验数据。
  5. 分布式锁,Redis 或Zookeeper 实现。
  6. 状态机,状态变更,更新数据时判断状态。
实现思路

拟采用Redis + Token 机制实现接口幂等性校验。
为保证幂等性的每一次请求创建一个唯一标识Token,先获取Token ,并将Token 存入Redis ,请求接口时,将此Token 放到header 或者作为请求参数请求接口,后端接口判断Redis 中是否存在此Token 。
如果存在,正常处理业务逻辑,并从Redis 中删除此Token ,那么,如果重复请求,由于Token 已被删除,则不能通过校验,则返回请勿重复操作提示。
如果不存在,说明参数不合法或者重复请求,返回提示即可。

项目Demo

https://github.com/vgbhfive/SpringBootDemo


个人备注

此博客内容均为作者学习所做笔记!
若转作其他用途,请注明来源!

参考内容

redis-基础-性能测试&客户端连接&管道&分区&Java使用redis