Redis系统学习

Redis系统学习

一、什么是Redis?

  1. Redis是用C语言开发的一个开源、免费、高性能键值对内存数据库
  2. 它提供5种数据类型来存储值:字符串类型、散列类型、列表类型、集合类型、有序集合类型
  3. 他是一种NoSQL数据库
  4. 可用于缓存、内存数据库、消息队列等

1.1 什么是NoSQL?

  • NoSQL,即Not-Only-SQL,泛指非关系型数据库
  • NoSQL数据库为了解决高并发高可用高可拓展大数据存储问题而产生的数据库解决方案
  • NoSQL可以作为关系型数据库的良好补充,但不是替代关系型数据库

1.2 NoSQL数据库分类

1.2.1 键值存储数据库

相关产品: Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB

典型应用: 内容缓存,主要处理大量数据的高访问负载

数据模型: 一系列键值对

优势: 快速查询

劣势: 存储的数据缺少结构化

1.2.2 列存储数据库

相关产品: Cassardra、HBase、Riak

典型应用: 分布式的文件系统

数据模型: 以列簇式存储,将同一列数据存在一起

优势: 查找速度快、可拓展性强,更容易进行分布式拓展

劣势: 功能相对局限

1.2.3 文档型数据库

说明: 与Key-Value类似,Value是结构化的,即可嵌套

相关产品: Mongo DB、Couch DB

典型应用: Web应用

数据模型: 一系列键值对

优势: 数据结构要求不严格

1.3 Redis的应用场景

  • 内存数据库(登陆信息、购物车信息、用户浏览记录等)
  • 缓存服务器(商品数据、广告数据等)
  • 解决分布式集群架构中的session分离问题(session共享)
  • 任务队列(秒杀、抢购、12306等)
  • 支持发布-订阅的消息模式
  • 应用排行榜
  • 网站访问统计
  • 数据过期处理(精确到毫秒)

二、安装启动

2.1 解压二进制安装包

1
2
3
4
$ tar -xf redis-3.2.9.tar.gz # 加上-z参数可以显示解压进度
$ cd redis-3.2.9
$ make # 需要安装GCC
$ make install PREFIX=/redis # 指定安装目录安装

2.2 启动

2.2.1 前端启动

1
2
3
4
# 直接运行bin/redis-server将以前端模式启动
$ ./redis-server

# ctrl + c关闭

2.2.2 守护进程启动

  1. 修改redis-conf配置文件,将文件中的daemonize项改为yes
  2. bind 127.0.0.1改为bind <redis实例所在机器的真实IP>,如:bind 192.168.10.133(本地测试忽略)
1
2
3
4
5
6
7
8
# 指定配置文件位置启动
$ redis-server /usr/local/bin/redis.conf

# 检查是否启动成功
$ ps -ef | grep redis

# 关闭redis
$ ./redis-cli shutdown

2.3 其他命令介绍

  • redis-server: 启动Redis服务
  • redis-cli: 进入Redis命令客户端
  • redis-benchmark: 性能测试工具
  • redis-check-aof: aof文件进行检查的工具
  • redis-check-dump: rdb文件进行检查的工具
  • redis-sentinel: 启动哨兵监控服务

2.4 Redis客户端

2.4.1 自带的命令客户端

1
2
3
4
5
# 默认进入当前机器的6379端口所在的Redis
$ ./redis-cli

# 指定机器和端口(host port)
$ ./redis-cli -h IP地址 -p 端口

2.4.2 程序客户端-Jedis

  • Redis不仅可以使用命令客户端进行操作,还可以使用程序客户端进行操作
  • 现在的主流语言都有客户端支持,比如:Java、C、C#、C++、PHP、Node.js、Go等
  • Java的客户端有: JedisRedisson、Jredis、JDBC-Redis

2.4.3 多数据库支持

默认一共是16个数据库,每个数据库之间是相互隔离(但是可以使用flushall一次清空所有的库)。数据库的数量是在redis.conf中配置的。

切换数据库使用命令:select 数据库编号(0-15)

例如:select 1

三、通用命令

3.1 set & get命令

1
2
3
4
5
# redis通过set key value的方式塞值
set name tom

# redis通过get key的方式取值
get name

3.2 keys命令

1
2
3
4
5
6
7
8
9
10
11
# 返回满足给定pattern的所有key
# 语法:keys pattern
$ keys *
1) "age"
2) "id"
3) "Category"
4) "name"
5) "ID"

$ keys i*
1) "id"

3.3 setnx命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# setnx只能在key不存在的场景下可以塞值成功
$ get name
"Jack"

$ setnx name "Bob"
(integer) 0 # setnx返回0代表塞值失败

$ get name
"Jack"

$ setnx name2 "Bob"
(integer) 1 # setnx返回1代表塞值成功

$ get name2
"Bob"

# 提示:可以用keys命令来检验key是否已存在

该命令可以用于分布式锁,只能赋值成功一次的场景: 把key当作是锁

3.4 append命令

1
2
3
4
5
6
7
8
$ set sayhi "hello"
OK

$ append sayhi " world"
(integer) 11 # 该数字代表当前value的长度

$ get sayhi
"hello world"

3.5 strlen命令

1
2
3
4
$ strlen sayhi
(integer) 11

# 键不存在会返回0

3.6 同时设置多个值和获取多个值

1
2
3
4
5
6
7
$ mset k1 v1 k2 v2 k3 v3
OK

$ mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"

3.7 del命令

del命令是根据key来删除的,所以5种数据类型通用

1
2
$ del name
(integer) 1

3.8 判断一个key是否存在

1
2
3
4
5
$ exists k1
(integer) 1 # 存在

$ exists k9
(integer) 0 # 不存在

3.9 给一个key重命名

1
2
3
4
5
6
7
$ rename k1 k11
OK

$ keys k*
1) "k3"
2) "k11"
3) "k2"

3.10 判断一个value的类型

1
2
3
4
5
6
7
8
$ type k11
string

$ type list1
list

$ type tom
hash

3.11 设置缓存过期时间(生存时间)

Redis在实际使用过程中,更多的用作缓存.然而缓存的数据一般都是需要设置生存时间的.

即:到期后数据销毁

1
2
3
4
# expire key seconds 设置生存时间(秒)
# expire key milliseconds (毫秒)
# ttl key 查看key剩余生存时间
# persist key 清除生存时间(永生)

四、Redis数据类型

4.1 String类型

1
2
3
4
5
6
7
8
9
10
11
12
13
# 赋值
$ set name tom
OK

# 取值
$ get name
"tom"

# 取值并赋值(取出老的值并赋予新的值)
$ getset name Jack
"tom"
$ get name
"Jack"

4.1.1 String类型递增数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 如果我们key的value是可以转换成正整数的字符串
# 那么就可以使用incr命令来实现自增
# 该操作是原子性的,可用于分布式系统的唯一ID生成
$ set ID "10"
OK

$ incr ID
(integer) 11

$ get ID
"11"

# incr还可以指定增幅:使用incrby命令
$ incrby ID 10
(integer) 21

# 递减
$ decr ID
(integer) 20

# 递减并指定幅度
$ decrby ID 10
(integer) 10

4.2 Hash类型

Hash类型使用hset命令,不区分插入和更新操作,当执行插入操作的时候返回1;当执行更新操作的时候返回0

该类型可以理解为他的value是一个map,即: 键 - (属性:值)

属性就可以理解为map的key;值则是map的值

这样的数据结构更能体现Java对象的特征

语法: hset key field value

注意事项: 存储对象属性经常发生增删改操作的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ hset "tom" age 20
(integer) 1

$ hset "tom" addr beijing
(integer) 1

$ hget tom age
"20"

$ hget tom addr
"beijing"

# 当然hmset和hmget可以实现多个操作,例如:
$ hmget tom age addr
1) "20"
2) "beijing"

4.2.1 Hash类型递增数字

同样,hash类型也是可以递增数字的,例如:

1
2
3
# 不存在hincr命令,只有hincrby
$ hincrby tom age 2
(integer) 22

4.2.2 判断属性是否存在

1
2
3
4
5
$ hexists tom age
(integer) 1 # 存在

$ hexists tom name
(integer) 0 # 不存在

4.2.3 获取所有的field或所有的值

1
2
3
4
5
6
7
$ hkeys tom
1) "age"
2) "addr"

$ hvals tom
1) "22"
2) "beijing"

4.2.4 获取所有的field和所有的值

1
2
3
4
5
$ hgetall tom
1) "age"
2) "22"
3) "addr"
4) "beijing"

4.2.5 获取字段数

1
2
$ hlen tom
(integer) 2

4.3 List类型

内部实现是双向链表,所以向两端添加元素的时间复杂度为O(1),获取越接近两端的元素速度就越快.

这意味着,即使是一个有几千万个元素的列表,获取头部和尾部的10条记录也是极快的.

4.3.1 向列表增加和查看列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 向列表左边增加元素
$ lpush list1 1 2 3 4 5
(integer) 5

# 因为依次向左边添加元素,因此目前的列表应该是
# 5,4,3,2,1

# 遍历该列表
# 索引可以是负数,-1代表最后一个元素
$ lrange list1 0 4
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"

# 从右边增加元素则是使用rpush,示例省略

4.3.2 从列表两端弹出元素

1
2
3
4
5
6
7
8
9
10
$ lpop list1
"5"

$ lrange list1 0 -1
1) "4"
2) "3"
3) "2"
4) "1"

# 从右边删除元素则是使用rpop,示例省略

利用列表,从一端添加,从另一端pop就可以实现消息队列的效果

4.3.3 获取列表长度

1
2
$ llen list1
(integer) 4

4.3.4 删除列表中指定的值(且指定删除的个数)

语法: lrem 列表 个数 值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 删除前
$ lrange list1 0 -1
1) "2"
2) "4"
3) "3"
4) "2"
5) "1"

# 从list1当中删除1个2
$ lrem list1 1 2
(integer) 1

# 删除后
$ lrange list1 0 -1
1) "4"
2) "3"
3) "2"
4) "1"

4.3.5 获取指定索引的元素值

1
2
3
4
5
6
7
8
9
$ lrange list1 0 -1
1) "4"
2) "3"
3) "2"
4) "1"

# 获取索引为1的元素值
$ lindex list1 1
"3"

4.3.6 将列表中元素插入另一个列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ lrange list1 0 -1
1) "4"
2) "3"
3) "2"
4) "1"

$ rpoplpush list1 newlist
"1"

$ lrange newlist 0 -1
1) "1"

$ lrange list1 0 -1
1) "4"
2) "3"
3) "2"

4.4 Set类型

不可重复(去重)

4.4.1 新增&删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ sadd s1 1 2 3 4 5
(integer) 5 # 该数字表示此次插入成功的元素个数

# 补充:因为set类型具备去重的特性,如果sadd的值已存在,则会出现add数字为0的情况

# 查看key下的所有值
$ smembers s1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"

# 删除元素
$ srem s1 4
(integer) 1

4.4.2 判断集合中是否存在该元素

1
2
3
4
$ sismember s1 6
(integer) 1 # 表示存在
$ sismember s1 7
(integer) 0 # 表示不存在

4.4.3 集合运算

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
# 已知:
$ smembers s1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"

$ smembers s2
1) "2"
2) "4"
3) "6"
4) "8"

# 交集运算
$ sinter s1 s2
1) "2"
2) "4"
3) "6"

$ sinter s2 s1
1) "2"
2) "4"
3) "6"

# 差集运算
# 即:从[前者]里面减去[前者]和[后者]同时存在的值
$ sdiff s1 s2
1) "1"
2) "3"
3) "5"

$ sdiff s2 s1
1) "8"

# 并集运算
$ sunion s1 s2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "8"

4.4.4 获取集合中元素的个数

1
2
$ scard s1
(integer) 6

4.4.5 从集合中弹出一个元素

必须要注意的是,此处的弹出(删除)是随机的.因此可以使用它来实现抽奖系统等

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
# 传递的数字代表要弹出几个元素,而不是弹出什么元素
# 例1
$ spop s1 6
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"

$ smembers s1
(empty list or set)

# 例2
$ sadd s1 1 2 3 4 5 6
(integer) 6

$ scard s1
(integer) 6

$ spop s1
"6"

$ spop s1
"3"

$ spop s1
"1"

$ scard s1
(integer) 3

$ spop s1 2
1) "5"
2) "2"

$ scard s1
(integer) 1

4.5 SortedSet类型(别名:ZSet)

有序集合实现排序的方式:通过给每一个元素指定一个分数,然后基于分数进行排名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 语法:zadd 集合名 分数 元素
$ zadd z1 80 Jerry 100 Tom 75 Jack
(integer) 3

# 排名
# 默认:从小到大
$ zrange z1 0 -1
1) "Jack"
2) "Jerry"
3) "Tom"

# 降序
$ zrevrange z1 0 -1
1) "Tom"
2) "Jerry"
3) "Jack"

4.5.1 获取元素的分数

1
2
$ zscore z1 Jack
"75"

4.5.2 删除元素

1
2
3
4
5
6
$ zrem z1 Jack
(integer) 1

$ zrange z1 0 -1
1) "Jerry"
2) "Tom"

五、Redis事务

5.1 Redis事务介绍

  • Redis事务是通过MULTIEXECDISCARDWATCH这四个命令实现的
  • Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合
  • Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
  • 不支持事务回滚

5.2 命令介绍

5.2.1 MULTI命令

用于标记一个事务块的开始

Redis会将后续的命令逐个放入队列中,然后才能使用EXEC命令原子化地执行这个序列

5.2.2 EXEC命令

在一个事务中执行所以先前放入命令队列中的命令,然后恢复正常的连接状态

5.2.3 DISCARD命令

清除所有在先前一个事务中放入命令队列中的命令,然后恢复正常的连接状态

5.2.4 WATCH命令

当某个事务需要按条件执行时,就要使用这个命令.将给定的键设置为受监控的

使用这个命令可以实现Redis的乐观锁

使用unwatch清除所有先前为一个事务监控的键

5.2.5 示例

演示事务开启并取消:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ MULTI # 开启事务命令集合
OK

$ set s111 v111 # 往命令队列中添加命令1
QUEUED

$ set s222 v222 # 往命令队列中添加命令2
QUEUED

$ DISCARD # 取消命令队列中的命令
OK

$ exec
(error) ERR EXEC without MULTI

演示事务开启并执行:

1
2
3
4
5
6
7
8
9
10
11
12
$ MULTI # 开启事务命令集合
OK

$ set s111 v111 # 往命令队列中添加命令1
QUEUED

$ set s222 v222 # 往命令队列中添加命令2
QUEUED

$ exec # 开始依次执行命令集合中的命令
1) OK
2) OK

演示WATCH乐观锁:

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
# 检视初始值
$ get s1
"111"

$ get s2
"222"

# 开始监控s1的状态:如果s1的值发生变化,则事务不执行
$ watch s1
OK

# 开启事务
$ MULTI
OK

$ set s2 999
QUEUED

# 由于s1的值在其他地方被改变,事务执行失败
$ exec
(nil)

# s2的值没有被修改
$ get s2
"222"

5.3 Redis事务失败

  • Redis语法错误
  • Redis类型错误

5.3.1 为什么Redis不支持事务回滚?

  1. 大多数事务失败是因为语法错误或类型错误,这两种错误都可以在开发阶段预见
  2. Redis为了性能方面忽略了事务回滚

六、Redis实现分布式锁

6.1 锁的处理

  • 单应用中使用锁:单进程多线程

    Synchronize、Lock

  • 分布式应用中使用锁:多进程

6.2 分布式锁的实现方式

  • 数据库的乐观锁
  • 基于Zookeeper的分布式锁
  • 基于Redis的分布式锁

6.3 分布式锁的注意事项

  • 互斥性:在任意时刻,只有一个客户端能持有锁
  • 同一性:加锁和解锁必须是同一个客户端,客户端不能把别人加的锁
  • 避免死锁:即使有一个客户端持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端加锁

6.4 实现分布式锁

6.4.1 获取锁

SET命令中,有很多选项可用来修改命令的行为,以下是SET命令可用的基本语法。

1
$ SET key value [EX seconds] [PX milliseconds] [NX|XX]
  • EX seconds:设置指定的到期时间(以秒为单位)。
  • PX milliseconds:设置指定的到期时间(以毫秒为单位)。
  • NX:仅在键不存在的时候设置键。
  • XX:仅在键已存在的时候才设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 方式1 使用redis的set命令实现获取分布式锁
* @param lockKey 可以就是锁
* @param requestId 请求ID,保证同一性
* @param expireTime 过期时间,避免死锁
* @return
*/
public static boolean getLock(String lockKey, String requestId, int expireTime) {
// NX:保证互斥性
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
if("OK".equals(result)) {
return true;
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 方式2(使用setnx命令实现)
*/
public static boolean getLock(String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if(result == 1) {
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}

6.4.2 释放锁

1
2
3
4
5
6
7
8
/**
* 方式1 del命令实现 释放分布式锁
*/
public static void releaseLock(String lockKey,String requestId) {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 方式2(redis+lua脚本实现)--推荐
*/
public static boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

if (result.equals(1L)) {
return true;
}
return false;
}

七、持久化方案

Redis是一个内存数据库,为了保证数据的持久性,它提供了两种持久化方案:

  • RDB方式(默认)
  • AOF方式

7.1 RDB方式

7.1.1 介绍

  • RDB是Redis默认采用的持久化方式
  • RDB方式是通过快照(snapshotting)完成的,当符合一定条件时Redis会自动将内存中的数据进行快照并持久化到硬盘。默认保存在dump.rdb文件中。(本机位置:/usr/local/bin/dump.rdb)
  • Redis会在指定的情况下触发快照
    1. 符合自定义配置的快照规则
    2. 执行save或者bgsave命令
    3. 执行flushall命令
    4. 执行主从复制操作

7.1.2 配置dbfilename指定rdb快照文件的名称

1
2
# The filename where to dump the DB
dbfilename dump.rdb

注意事项

  1. redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的。
  2. 这就使得我们可以通过定时备份RDB文件来实现redis数据库的备份, RDB文件是经过压缩的二进制文件,占用的空间会小于内存中的数据,更加利于传输。

7.1.3 自定义快照规则

在redis.conf中设置自定义快照规则:

1
2
3
4
5
6
7
8
格式:save <seconds> <changes>

示例:
save 900 1 : 表示15分钟(900秒钟)内至少1个键被更改则进行快照。
save 300 10 : 表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 : 表示1分钟内至少10000个键被更改则进行快照。

可以配置多个条件(每行配置一个条件),每个条件之间是“或”的关系。

特别说明:

1. Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存。
 2. 根据数据量大小与结构和服务器性能不同,这个时间也不同。通常将记录一千万个字符串类型键、大小为1GB的快照文件载入到内存中需要花费20~30秒钟。

7.1.4 RDB的优缺点

优点:

RDB可以最大化Redis的性能:父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无序执行任何磁盘I/O操作。同时这个也是一个缺点,如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求

缺点:

使用RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受范围。如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化

7.2 AOF方式

7.2.1 介绍

  • 默认情况下Redis没有开启AOF(append only file)方式的持久化
  • 开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件,这一过程显然会降低Redis的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高AOF的性能。
  • 可以通过修改redis.conf配置文件中的appendonly参数开启
1
appendonly yes

AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。

默认的文件名是appendonly.aof,可以通过appendfilename参数修改:

1
appendfilename appendonly.aof

7.2.2 参数说明

  • auto-aof-rewrite-percentage 100 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以启动时aof文件大小为准

  • auto-aof-rewrite-min-size 64mb 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化

7.2.3 同步磁盘数据

Redis每次更改数据的时候, aof机制都会将命令记录到aof文件,但是实际上由于操作系统的缓存机制,数据并没有实时写入到硬盘,而是进入硬盘缓存。再通过硬盘缓存机制去刷新到保存到文件。

参数说明:

appendfsync always 每次执行写入都会进行同步 , 这个是最安全但是是效率比较低的方式

appendfsync everysec 每一秒执行(默认)

appendfsync no 不主动进行同步操作,由操作系统去执行,这个是最快但是最不安全的方式

7.2.4 AOF文件损坏以后如何修复

服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。

当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:

  1. 为现有的 AOF 文件创建一个备份

  2. 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复。

redis-check-aof –fix readonly.aof

重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。

7.3 如何选择RDB和AOF

  • 一般来说,如果对数据的安全性要求非常高的话,应该同时使用两种持久化功能。

  • 如果可以承受数分钟以内的数据丢失,那么可以只使用 RDB 持久化。

  • 有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快 。

  • 两种持久化策略可以同时使用,也可以使用其中一种。如果同时使用的话, 那么Redis重启时,会优先使用AOF文件来还原数据

八、Redis主从复制

持久化保证了即使redis服务重启也不会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当redis服务器的硬盘损坏了可能会导致数据丢失,不过通过redis的主从复制机制就可以避免这种单点故障。

说明:

  • 主redis中的数据有两个副本(replication)即从redis1和从redis2,即使一台redis服务器宕机其它两台redis服务也可以继续提供服务。

  • 主redis中的数据和从redis上的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上。

  • 只有一个主redis,可以有多个从redis。

  • 主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求

  • 一个redis可以即是主又是从

8.1 主从配置

8.1.1 主redis配置

无需特殊配置。

8.1.2 从redis配置

修改从服务器上的redis.conf文件:

1
slaveof <masterip> <masterport>

8.2 实现原理

  • Redis的主从同步,分为全量同步和增量同步。

  • 只有从机第一次连接上主机是全量同步

  • 断线重连有可能触发全量同步也有可能是增量同步(master判断runid是否一致)

  • 除此之外的情况都是增量同步

8.2.1 全量同步

Redis的全量同步过程主要分三个阶段:

  1. 同步快照阶段:Master创建并发送快照给Slave,Slave载入并解析快照。Master同时将此阶段所产生的新的写命令存储到缓冲区。

  2. 同步写缓冲阶段:Master向Slave同步存储在缓冲区的写操作命令。

  3. 同步增量阶段:Master向Slave同步写操作命令。

8.2.2 增量同步

  • Redis增量同步主要指Slave完成初始化后开始正常工作时,Master发生的写操作同步到Slave的过程

  • 通常情况下,Master每执行一个写命令就会向Slave发送相同的写命令,然后Slave接收并执行。

九、Redis Sentinel哨兵机制

Redis主从复制的缺点:没有办法对master进行动态选举,需要使用Sentinel机制完成动态选举。

说明:

  • Sentinel(哨兵)进程是用于监控redis集群中Master主服务器工作的状态

  • 在Master主服务器发生故障的时候,可以实现Master和Slave服务器的切换,保证系统的高可用(HA)

  • 其已经被集成在redis2.6+的版本中,Redis的哨兵模式到了2.8版本之后就稳定了下来。

9.1 哨兵进程的作用

  1. 监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。

  2. 提醒(Notification): 当被监控的某个Redis节点出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。

  3. 自动故障迁移(Automatic failover):当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作

它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master;

当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用现在的Master替换失效Master。

Master和Slave服务器切换后,Master的redis.conf、Slave的redis.conf和sentinel.conf的配置文件的内容都会发生相应的改变,即,Master主服务器的redis.conf配置文件中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换。

9.2 哨兵进程的工作方式

  1. 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。

  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线SDOWN)。

  3. 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态

  4. 有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)

  5. 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。

  6. 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

9.3 案例演示

修改从机的sentinel.conf

1
2
#sentinel monitor <master-name> <master ip> <master port> <quorum>
sentinel monitor mymaster 192.168.10.133 6379 1

其他配置项说明:

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
# Example sentinel.conf

# 哨兵sentinel实例运行的端口 默认26379
port 26379

# 哨兵sentinel的工作目录
dir /tmp

# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2

# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd


# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000

# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1



# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000

# SCRIPTS EXECUTION

#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。

#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
一个是事件的类型,
一个是事件的描述。
如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh

# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

通过redis-sentinel启动哨兵服务:

1
./redis-sentinel sentinel.conf

十、Redis Cluster集群

redis3.0以后推出的redis cluster 集群方案,redis cluster集群保证了高可用、高性能、高可扩展性。

架构细节:

(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.

(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.

(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可

(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点

最小节点数:3台

(1)节点失效判断:集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过(cluster-node-timeout),认为该master节点挂掉.

(2)集群失效判断:什么时候整个集群不可用(cluster_state:fail)?

Ø 如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0-16383]slot映射不完全时进入fail状态。

如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。