Redis 教程

Nosql 概述

为什么要用 Nosql

1、最早的单机 Mysql 的年代

image-20200709105625636

90年代,一个网站的访问量一般不会太大,单个数据库完全足够!

大部分都是使用静态 html 网页,服务器根本没有压力

瓶颈如下:

1、数据量太大,一个机器放不下

2、数据索引(B+Tree),一个机器内存也放不下

3、访问量太大(读写混合),一个服务器承受不了

2、Memcached(缓存)+ MySQL + 垂直拆分(读写分离)

网站 80% 的情况都是在读,每次都去查询数据库就很麻烦,所以使用缓存来保证效率

发展过程:优化数据库结构和索引 → 文件缓存(IO)→ Memcache(当时最热门的技术)

image-20200709110640230

3、分库分表 + 水平拆分 + 集群

早些年 MyISAM:表锁,十分影响效率,高并发下会出现严重的锁问题

转战 InnoDB:行锁

使用分库分表解决写的压力

image-20200709112134405

4、如今的互联网架构模型

MySQL 等关系型数据库已经不够用了,各种各样的数据(音乐、热榜、图片等)都存在 MySQL 的话,数据库表很大,压力爆表,效率自然低下

image-20200709113811000

为什么要用 NoSQL

用户的个人信息、社交网络、自理位置、用户日志等等爆发式增长!

这个时候就需要用 NoSQL 数据库,NoSQL 可以很好处理以上情况

什么是 NoSQL

NoSQL

NoSQL = Not Only Sql (不仅仅是sql)

泛指非关系型数据库,随着 web2.0 互联网的诞生,传统的关系型数据库很难应对,尤其是超大规模的高并发社区,暴露出来很多难以克服的问题。NoSQL 在当今大数据环境下发展十分迅速,Redis 是发展最快,当下必须要掌握的技术

很多的数据类型用户的个人信息、社交网络、自理位置、用户日志这些数据不需要固定格式,不需要多元的操作就可以横向扩展。

NoSQL 特点

1、方便扩展(数据之间没有关系)

2、大数据量高性能(Redis 一秒写 8 万次,一秒读取 11 万次,缓存记录集,性能高)

3、多种多样的数据类型(不需要事先设计数据库,随取随用)

4、传统的 RDBMS 和 NoSQL

1
2
3
4
5
6
7
8
传统的 RDBMS
- 结构化组织
- SQL
- 数据和关系存在单独的表中 row clo
- 多元操作
- 严格的一致性
- 事务
- …………
1
2
3
4
5
6
7
8
NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储、列存储、文档存储、图形数据库(社交关系)
- 最终一致性
- CAP 定理和 BASE
- 高性能,高可用,高可扩展
- …………

了解:3V+3高

大数据时代的3V:

  1. 海量数据
  2. 多样
  3. 实时

大数据时代的3高:

  1. 高并发
  2. 高可扩
  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
# 1、商品的基本信息(名称、价格、商家信息)
关系型数据库
- MySQL
- Oracle

# 2、商品的描述、评论
文档型数据库
- MongoDB

# 3、图片
分布式文件系统
- FastDFS
- TFS(淘宝)
- GFS(Google)
- Hadoop
- oss(阿里云)

# 4、商品的关键字
搜索引擎
- solr elasticsearch
- Isearch

# 5、热门的波段信息
内存数据路
- redis
- Memcache

Nosql 的四大分类

KV 键值对

  • 新浪:Redis
  • 美团:Redis + Tair
  • 阿里、百度:Redis + Memcache

文档型数据库

  • MongoDB
    • MongoDB 是一个基于分布式文件存储的数据库,C++ 编写
    • 主要用来处理大量的文档
    • MongoDB 是一个介于关系型数据库和非关系型数据库中间的产品,MongoDB 是非关系型数据库中功能最丰富,最像关系型数据库的
    • CouchDB

列存储数据库

  • HBase
  • 分布式文件系统

图关系数据库(不是存图形,存的是关系,比如社交网络、广告推荐)

myfriends

  • Neo4j、InfoGrid

四者对比

image-20200709183241350

Redis 入门

概述

Redis 是什么?

Redis(Remote Dictionary Server ),即远程字典服务。是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API

与 memcached 一样,为了保证效率,数据都是缓存在内存中。区别的是 redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave (主从)同步。

免费和开源!当下最热门的 NoSQL 技术之一,也被人们成为结构化数据库

Redis 能干嘛?

1、内存存储、持久化(rdb、aof)

2、效率高,可以用于高速缓存

3、发布订阅系统

4、地图信息分析

5、计时器、计数器(浏览量)

6、…………

Redis 特性

1、多样的数据类型

2、持久化

3、集群、事务

4、…………

Redis 推荐在 Linux 服务器上搭建

官网:https://redis.io/

中文网:http://redis.cn/

下载地址:通过官网下载即可

image-20200709184914345

注意:Windows 在 GitHub 上下载(已停更)

Linux 安装

1、下载安装包

2、解压安装包

image-20200709190925080

3、进入解压后的文件夹,可以看到配置文件

image-20200709191049617

4、基本的环境安装

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
[root@localhost redis-6.0.5]# yum install gcc-c++
[root@localhost redis-6.0.5]# make

# make 报错了,解决办法:升级 gcc

make[1]: *** [server.o] Error 1
make[1]: Leaving directory `/usr/local/src/redis-6.0.5/src'
make: *** [all] Error 2

# 升级 gcc
[root@localhost redis-6.0.5]# yum -y install centos-release-scl
[root@localhost redis-6.0.5]# yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils

# 临时启用 gcc9
[root@localhost redis-6.0.5]# scl enable devtoolset-9 bash

# 长期使用
[root@localhost redis-6.0.5]# echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile

# 安装成功会出现
Hint: It's a good idea to run 'make test' ;)

[root@localhost redis-6.0.5]# make install
cd src && make install
make[1]: Entering directory `/usr/local/src/redis-6.0.5/src'

Hint: It's a good idea to run 'make test' ;)

INSTALL install
INSTALL install
INSTALL install
INSTALL install
INSTALL install
make[1]: Leaving directory `/usr/local/src/redis-6.0.5/src'
[root@localhost redis-6.0.5]#

5、redis 的默认安装路径:/usr/local/bin

image-20200709192625352

6、将 redis 配置文件复制到当前目录

image-20200709192840646

7、redis 默认不是后台启动的,修改配置文件

image-20200709193013031

8、启动 redis 服务

image-20200709193203460

性能测试

redis-benchmark 是一个官方自带的性能测试工具

命令参数如下:

image-20200709215712113

简单测试一下

1
2
# 测试:100个并发连接,每个并发100000个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

image-20200709220626415

如何分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@localhost bin]# ./redis-benchmark -h localhost -p 6379 -c 100 -n 100000
====== SET ======
100000 requests completed in 3.47 seconds # 10万个请求
100 parallel clients # 100的并发
3 bytes payload # 每次写入3个字节
keep alive: 1 # 只有一台服务器来处理请求,单机性能
host configuration "save": 900 1 300 10 60 10000
host configuration "appendonly": no
multi-thread: no

0.00% <= 0.6 milliseconds
0.02% <= 0.7 milliseconds
3.56% <= 1.2 milliseconds
6.57% <= 1.3 milliseconds
10.28% <= 1.4 milliseconds
14.41% <= 1.5 milliseconds
18.84% <= 1.6 milliseconds
23.53% <= 1.7 milliseconds
99.91% <= 28 milliseconds
99.96% <= 29 milliseconds
99.98% <= 32 milliseconds
99.99% <= 33 milliseconds
100.00% <= 33 milliseconds
28785.26 requests per second # 平均每秒处理 28785.26 个请求

基础知识

Redis 默认有 16 个数据库(配置文件中)

image-20200709221505611

默认使用的是第 0 个

可以使用 select 进行切换数据库

1
2
3
4
5
6
7
# 切换数据库
127.0.0.1:6379> select 3
OK
# 查询数据库大小
127.0.0.1:6379[3]> dbsize
(integer) 0
127.0.0.1:6379[3]>

image-20200709221859432

1
2
3
127.0.0.1:6379[3]> keys *		# 查看所有的 key
1) "name"
127.0.0.1:6379[3]>

清空当前数据库:flushdb

1
2
3
4
5
127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty array)
127.0.0.1:6379[3]>

清空所有数据库:flushall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> keys *
1) "mylist:{tag}"
2) "counter:{tag}:__rand_int__"
3) "key:{tag}:__rand_int__"
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> flushall
OK
127.0.0.1:6379[3]> select 0
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>

Redis 是单线程的!

Redis 是基于内存操作的,CPU 不是 Redis 的性能瓶颈,Redis 的瓶颈是在于机器的内存和网络的带宽。所以就使用了单线程

Redis 单线程为什么这么快?

Redis 是 C 语言写的,官方提供的数据为 100000+ 的 QPS,完全不比同样使用 key => value 的 Memcache 差

误区:

  • 高性能一定是多线程?

  • 多线程(CPU 调度,上下文切换会消耗资源)一定比单线程效率高(CPU > 内存 > 硬盘)

核心:Redis 是将数据存储在内存的,所以单线程处理是效率最好的,对于内存系统来说,如果没有上下文切换,效率就是最高的,多次读写都是在一个 CPU 上的

五大数据类型

官网文档

image-20200709223659344

image-20200709224106251

Redis-key

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
[root@localhost bin]# ./redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set name cat # set key
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> set age 1
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> move age 1 # 移除当前 key
(integer) 1
127.0.0.1:6379> keys *
2) "name"
127.0.0.1:6379> exists name # 判断当前 key 是否存在
(integer) 1
127.0.0.1:6379> type name # 查看当前 key 的类型
string
127.0.0.1:6379> expire name 10 # 设置 key 的过期时间
(integer) 1
127.0.0.1:6379> get name
"cat"
127.0.0.1:6379> ttl name # 查看当前 key 的剩余时间
(integer) -2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379>

使用官网查询命令

image-20200709225931808

String(字符串)

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
########################################################################################
127.0.0.1:6379> set key1 v1 # 设置值
OK
127.0.0.1:6379> get key1 # 获取值
"v1"
127.0.0.1:6379> keys * # 查看所有 key
1) "key1"
127.0.0.1:6379> exists key1 # 判断某一个key是否存在
(integer) 1
127.0.0.1:6379> append key1 hello # 追加字符串,如果当前key不存在,相当于set key
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> append key1 ", world"
(integer) 14
127.0.0.1:6379> strlen key1 # 获取key的长度
(integer) 14
127.0.0.1:6379> get key1
"v1hello, world"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # 自增1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views # 自减1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incrby views 10 # 指定步长,设置增量
(integer) 11
127.0.0.1:6379> incrby views 10
(integer) 21
127.0.0.1:6379> decrby views 5
(integer) 16
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> set key1 "hello world"
OK
127.0.0.1:6379> get key1
"hello world"
127.0.0.1:6379> getrange key1 0 5 # 截取字符串
"hello "
127.0.0.1:6379> getrange key1 0 -1 # 获取所有字符串,相当于get
"hello world"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> set key2 "abcdefg"
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> setrange key2 0 "xx" # 替换指定位置开始的字符串
(integer) 7
127.0.0.1:6379> get key2
"xxcdefg"
127.0.0.1:6379>
########################################################################################
127.0.0.1:6379> setex key3 10 "2020" # 设置key3的值为2020,10秒过期
OK
127.0.0.1:6379> ttl key3
(integer) 3
127.0.0.1:6379> get key3
(nil)
127.0.0.1:6379> setnx mykey "apple" # 如果mykey不存在则设置
(integer) 1
127.0.0.1:6379> keys *
1) "key2"
2) "mykey"
3) "key1"
127.0.0.1:6379> setnx mykey "banner" # 如果mykey不存在则设置(返回0,创建失败)
(integer) 0
127.0.0.1:6379> get mykey
"apple"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # msetnx 是一个原子性操作,一起成功,一起失败
(integer) 0
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> getset db redis # 如果不存在则返回nil,不存在则设置值
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb # 如果存在则返回值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"
127.0.0.1:6379>

想象一个简单业务场景:

要存储用户1的关注数 10、粉丝数 20;用户2的关注数 5、粉丝数 30;

使用 redis 如何存储

方案一:

1
2
3
4
5
6
127.0.0.1:6379> mset user:1 {follow:10,fans:20} user:2 {follow:5,fans:30}
OK
127.0.0.1:6379> mget user:1 user:2
1) "{follow:10,fans:20}"
2) "{follow:5,fans:30}"
127.0.0.1:6379>

得到数据后,进行解析

方案二:

1
2
3
4
5
6
7
8
127.0.0.1:6379> mset user:1:follow 10 user:1:fans 20 user:2:follow 5 user:2:fans 30
OK
127.0.0.1:6379> mget user:1:follow user:1:fans user:2:follow user:2:fans
1) "10"
2) "20"
3) "5"
4) "30"
127.0.0.1:6379>

得到数据后,不用解析

List

image-20200710100505779

在 redis 里面,栈、堆、队列、阻塞队列都可以用 list 来实现

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
144
145
146
147
148
149
150
# 所有的list命令都是l开头的
127.0.0.1:6379> lpush list one # 将一个或多个值插入到列表头部
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1
1) "three"
2) "two"
127.0.0.1:6379> rpush list xxx # 将一个或多个值插入到列表尾部
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "xxx"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "xxx"
127.0.0.1:6379> lpop list # 移除list头部
"three"
127.0.0.1:6379> rpop list # 移除list尾部
"xxx"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1 # 通过下标获取list某个值
"one"
127.0.0.1:6379> lindex list 0
"two"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> lpush list one two three
(integer) 3
127.0.0.1:6379> llen list # 返回列表长度
(integer) 3
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> lpush list three
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 1 one # 移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 1 three
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> lpush list 'helllo' 'hello1' 'hello2' 'hello3'
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "hello3"
2) "hello2"
3) "hello1"
4) "helllo"
127.0.0.1:6379> ltrim list 1 2 # 通过下标截取指定长度,会改变list,只剩下截取的元素
OK
127.0.0.1:6379> lrange list 0 -1
1) "hello2"
2) "hello1"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> lpush mylist "hello"
(integer) 1
127.0.0.1:6379> lpush mylist "hello1"
(integer) 2
127.0.0.1:6379> lpush mylist "hello2"
(integer) 3
127.0.0.1:6379> rpoplpush mylist mylist1 # 移除列表最后一个元素,移动到新的列表中并返回
"hello"
127.0.0.1:6379> lrange mylist 0 -1 # 查看原来的列表
1) "hello2"
2) "hello1"
127.0.0.1:6379> lrange mylist1 0 -1 # 查看新的列表
1) "hello"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> exists list # 判断列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 列表不存在更新会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item # 如果存在更新指定下标的值
OK
127.0.0.1:6379> lrange list 0 0
1) "item"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> rpush mylist hello
(integer) 1
127.0.0.1:6379> rpush mylist world
(integer) 2
127.0.0.1:6379> linsert mylist before world other # 将一个值插入到列表中某个元素的前面或后面
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "world"
127.0.0.1:6379>

list 小结

  • 实际上是一个链表,left、right 都可以插入值
  • 如果 key 不存在,创建新的链表
  • 如果 key 存在,新增内容
  • 如果移除了所有值,就是空链表,也意味着该 key 也不存在了
  • 在两边插入或改动值,效率最高。

消息队列(lpush rpop),栈(lpush lpop)

Set(集合)

set 中的值是不能重复的(无序不重复集合)

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
########################################################################################

127.0.0.1:6379> sadd myset hello # 向set集合中添加元素
(integer) 1
127.0.0.1:6379> sadd myset world
(integer) 1
127.0.0.1:6379> smembers myset # 查看set集合所有元素
1) "hello"
2) "world"
127.0.0.1:6379> sismember myset hello # 查询某值是否存在于set集合中
(integer) 1
127.0.0.1:6379> sismember myset cat
(integer) 0
127.0.0.1:6379> scard myset # 集合中元素个数
(integer) 2
127.0.0.1:6379> srem myset world # 将某值移出set
(integer) 1
127.0.0.1:6379> smembers myset
1) "hello"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> SRANDMEMBER myset # 随机取出一个元素
"world"
127.0.0.1:6379> SRANDMEMBER myset
"cat"
127.0.0.1:6379> SRAND2MEMBER myset
"dog"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> spop myset # 随机移出一个元素
"hello"
127.0.0.1:6379> spop myset
"dog"
127.0.0.1:6379> SMEMBERS myset
1) "cat"
2) "world"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> sadd myset hello
(integer) 1
127.0.0.1:6379> sadd myset world
(integer) 1
127.0.0.1:6379> sadd myset dog
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "dog"
3) "world"
127.0.0.1:6379> sadd myset2 cat
(integer) 1
127.0.0.1:6379> SMEMBERS myset2
2) "dog"
127.0.0.1:6379> smove myset myset2 dog # 将一个集合中指定的元素移动到另一个集合中
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "world"
127.0.0.1:6379> SMEMBERS myset2
1) "cat"
2) "dog"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> sadd key1 a
(integer) 1
127.0.0.1:6379> sadd key1 b
(integer) 1
127.0.0.1:6379> sadd key1 c
(integer) 1
127.0.0.1:6379> sadd key2 c
(integer) 1
127.0.0.1:6379> sadd key2 d
(integer) 1
127.0.0.1:6379> sadd key2 e
(integer) 1
127.0.0.1:6379> SDIFF key1 key2 # 查看两个集合的差集
1) "b"
2) "a"
127.0.0.1:6379> SINTER key1 key2 # 查看两个几个的交集
1) "c"
127.0.0.1:6379> SUNION key1 key2 # 并集
1) "b"
2) "c"
3) "a"
4) "e"
5) "d"
127.0.0.1:6379>

Hash(哈希)

Map 集合,key-map 这时候的值是一个 map 集合

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
127.0.0.1:6379> hset myhash field1 hello					# set一个具体的 key-value
(integer) 1
127.0.0.1:6379> hget myhash field1 # 获取一个字段值
"hello"
127.0.0.1:6379> hmset myhash field1 hello field2 world # set多个key-value
(integer) 1
127.0.0.1:6379> hget myhash field1
"hello"
127.0.0.1:6379> hget myhash field2
"world"
127.0.0.1:6379> hmget myhash field1 field2 # 获取多个字段值
1) "hello"
2) "world"
127.0.0.1:6379> hgetall myhash # 查看指定key的hash 所有key-value
1) "field1"
2) "hello"
3) "field2"
4) "world"
127.0.0.1:6379> hdel myhash field1 # 删除hash指定的字段key
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
127.0.0.1:6379> hlen myhash # 获取指定key的hash长度
(integer) 1
127.0.0.1:6379> hmset myhash field1 hello
OK
127.0.0.1:6379> hlen myhash
(integer) 2
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "world"
3) "field1"
4) "hello"
127.0.0.1:6379> HEXISTS myhash field1 # 判断hash指定字段是否存在
(integer) 1
127.0.0.1:6379> HEXISTS myhash field3
(integer) 0
127.0.0.1:6379> hkeys myhash # 获取hash所有字段的key
1) "field2"
2) "field1"
127.0.0.1:6379> hvals myhash # 获取hash所有字段的value
1) "world"
2) "hello"
127.0.0.1:6379> HINCRBY myhash field3 1 # 指定增量
(integer) 1
127.0.0.1:6379> HINCRBY myhash field3 3
(integer) 4
127.0.0.1:6379> HINCRBY myhash field3 -1
(integer) 3
127.0.0.1:6379> HSETNX myhash field4 dog # 不存在则可以设置
(integer) 1
127.0.0.1:6379> HSETNX myhash field4 cat # 存在则不能设置
(integer) 0
127.0.0.1:6379>

Zset(有序集合)

在 set 的基础上,增加了一个值

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
127.0.0.1:6379> zadd myset 1 one					# 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three # 添加多个值
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379>

########################################################################################

# ZRANGEBYSCORE key min max
127.0.0.1:6379> zadd salary 2500 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 3000 zlisi
(integer) 1
127.0.0.1:6379> zadd salary 100 wangwu
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示所有用户 从小到大
1) "wangwu"
2) "zhangsan"
3) "zlisi"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores # 显示所有用户并附带工资
1) "wangwu"
2) "100"
3) "zhangsan"
4) "2500"
5) "zlisi"
6) "3000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 #显示工资小于等于2500的用户,升序排列
1) "wangwu"
2) "zhangsan"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores
1) "wangwu"
2) "100"
3) "zhangsan"
4) "2500"
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> zrange salary 0 -1
1) "wangwu"
2) "zhangsan"
3) "zlisi"
127.0.0.1:6379> zrem salary zlisi # 移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "wangwu"
2) "zhangsan"
127.0.0.1:6379> ZREVRANGE salary 0 -1 # 从大到小排序
1) "zhangsan"
2) "wangwu"
127.0.0.1:6379> zcard salary # 查看集合中元素的个数
(integer) 2
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> zadd myset 1 hello 2 world 3 cat
(integer) 3
127.0.0.1:6379> zcount myset 1 3 # 获取指定区间的元素数量
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
127.0.0.1:6379>

三种特殊类型

geospatial 地理位置

可以推算地理位置的信息,比如两地之间的距离,方圆几里的人

可以查询一些测试数据:http://www.jsons.cn/lngcode/

中文官网文档:https://www.redis.net.cn/order/3685.html

只有六个命令:

image-20200710183336838

1、geoadd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 添加地理位置
# 南北极 无法直接添加,城市数据一般通过程序直接导入,
# 参数:key value(纬度、经度、城市)
# 有效的经度从-180度到180度。
# 有效的纬度从-85.05112878度到85.05112878度。
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误

127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
127.0.0.1:6379>

geopos

获得的定位一定是一个坐标值

1
2
3
4
5
6
127.0.0.1:6379> geopos china:city beijing shanghai
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "121.47000163793563843"
2) "31.22999903975783553"
127.0.0.1:6379>

geodist

GEODIST 命令 - 返回两个给定位置之间的距离

如果两个位置之间的其中一个不存在, 那么命令返回空值。

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。

返回值

计算出的距离会以双精度浮点数的形式被返回。 如果给定的位置元素不存在, 那么命令返回空值。

1
2
3
4
5
127.0.0.1:6379> GEODIST china:city beijing shanghai
"1067378.7564"
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"1067.3788"
127.0.0.1:6379>

georadius

GEORADIUS 命令 - 以给定的经纬度为中心, 找出某一半径内的元素

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

范围可以使用以下其中一个单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST:在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD:将位置元素的经度和维度也一并返回。
  • WITHHASH:以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。

命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

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

在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count> 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。

返回值

  • 在没有给定任何 WITH 选项的情况下, 命令只会返回一个像 [“New York”,”Milan”,”Paris”] 这样的线性(linear)列表。
  • 在指定了 WITHCOORDWITHDISTWITHHASH 等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。

在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:

  1. 以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
  2. geohash 整数。
  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
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km	# 以100,30这个经纬度为中心,寻找方圆500km的城市
1) "chongqing"
2) "xian"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist # 以100,30这个经纬度为中心,寻找方圆500km的城市,并返回距离
1) 1) "chongqing"
2) "341.9374"
2) 1) "xian"
2) "483.8340"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withcoord # 以100,30这个经纬度为中心,寻找方圆500km的城市,并返回经纬度
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withcoord withdist count 2 #以100,30这个经纬度为中心,寻找方圆1000km的2个城市
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379>

georadiusbymember

1
2
3
4
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km	# 找出位于指定元素周围的其他元素
1) "beijing"
2) "xian"
127.0.0.1:6379>

geohash

该命令将返回11个字符的 Geohash 字符串

1
2
3
4
5
# 将二维的经纬度,转换为一维的字符串,如果连个字符串越接近,那么距离越近
127.0.0.1:6379> GEOHASH china:city beijing shanghai
1) "wx4fbxxfke0"
2) "wtw3sj5zbj0"
127.0.0.1:6379>

geo 底层就是基于 zset,可以通过 zset 命令操作 geo

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing
(integer) 1
127.0.0.1:6379>

hyperloglog

简介

hyperloglog 是用来统计基数的算法

网页的 UV (一个人访问网站多次,但还是算作一个人)

传统的方式,利用 set 来保存用户id,因为 set 不允许重复,就可以统计 set 中的元素作为标准判断

这个方式如果保存大量的用户 id,就会比较麻烦(消耗大量的空间保存用户id),我们的目的是计数,而不是用来保存用户id

优点:Hyperloglog 占用的内存是固定的,如果从内存角度来说,hyperloglog 一定是首选

0.81% 错误率

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> PFADD mykey a b c d e f g 		# 创建第一组数据
(integer) 1
127.0.0.1:6379> PFCOUNT mykey # 统计第一组数据的基数数量
(integer) 7
127.0.0.1:6379> PFADD mykey2 e f g h i j k
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 7
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 # 合并两组,返回两组的并集生成新的一组
OK
127.0.0.1:6379> PFCOUNT mykey3 # 查看新的一组基数数量
(integer) 11
127.0.0.1:6379>

如果允许容错,一定使用 hyperloglog

如果不允许容错,使用 set 或自己的数据类型就可以

bitmaps

位存储

一种位图数据结构,操作二进制来进行记录,只有 0 和 1 两个状态

业务场景一:

记录用户周一到周日(0-6)的签到情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:6379> setbit sign_1 0 1		# 周一 已签到
(integer) 0
127.0.0.1:6379> setbit sign_1 1 1 # 周二 已签到
(integer) 0
127.0.0.1:6379> setbit sign_1 2 1 # 周三 已签到
(integer) 0
127.0.0.1:6379> setbit sign_1 3 0 # 周四 未签到
(integer) 0
127.0.0.1:6379> setbit sign_1 4 0 # 周五 未签到
(integer) 0
127.0.0.1:6379> setbit sign_1 5 1 # 周六 已签到
(integer) 0
127.0.0.1:6379> setbit sign_1 6 0 # 周日 未签到
(integer) 0
127.0.0.1:6379> getbit sign_1 2 # 用户1周三的签到情况
(integer) 1
127.0.0.1:6379> bitcount sign_1 # 用户1这一周签到总数
(integer) 4
127.0.0.1:6379>

业务场景二:

用户在线状态

1
2
3
4
5
6
7
8
9
10
11
12
# 用户id为710的今日在线
127.0.0.1:6379> setbit online 710 1
(integer) 0
# 用户id为711的今日不在线
127.0.0.1:6379> setbit online 711 0
(integer) 0
# 获取用户id为710、711的在线情况
127.0.0.1:6379> getbit online 710
(integer) 1
127.0.0.1:6379> getbit online 711
(integer) 0
127.0.0.1:6379>

事务

ACID 原则:

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency)

事务前后数据的完整性必须保持一致。

隔离性(Isolation)

事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

redis 的事务没有隔离性的概念

redis 事务的特性:

一次性、顺序性、排他性

redis 事务的基础命令:

  • 开启事务(multi)
  • 命令入队(……)
  • 执行事务(exec)
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
127.0.0.1:6379> multi		# 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379>

########################################################################################

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> discard # 放弃事务,事务中的命令都不会被执行
OK
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379>

编译型异常(命令错误,事务中所有的命令都不会被执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 命令错误
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> exec # 事务执行报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1 # 所有命令不生效
(nil)
127.0.0.1:6379>

运行时异常(运行错误,事务中的命令是可以正常执行的。运行错误的命令抛出异常)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec # 事务执行成功
1) (error) ERR value is not an integer or out of range # 命令执行时错误
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2 # 事务中其他命令生效!
"v2"
127.0.0.1:6379>

redis 事务实现监控

正常执行成功的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监控 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec # 事务正常执行
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>

测试多线程修改值,使用 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
127.0.0.1:6379> WATCH money			# 监控 money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> exec # 执行之前,另一个线程修改了 money,所以事务执行失败
(nil)
127.0.0.1:6379> UNWATCH # 取消监控 money
OK
127.0.0.1:6379> watch money # 重新监控 money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 100
QUEUED
127.0.0.1:6379> incrby out 100
QUEUED
127.0.0.1:6379> exec # 执行成功
1) (integer) 900
2) (integer) 120
127.0.0.1:6379>

redis.conf 详解

启动 redis 服务时,就是使用 redis.conf 启动的

单位

image-20200711120203433

配置文件中的 unit 单位对大小写不敏感

包含文件

image-20200711120255561

可以包含其他配置文件

网络

1
2
3
bind 127.0.0.1	# 绑定的ip
protected-mode yes # 保护模式
port 6379 # 端口

通用 GENERAL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
daemonize yes	# 以守护进程的方式运行,默认是no,需要开启
pidfile /var/run/redis_6379.pid

# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境
# warning (only very important / critical messages are logged)
loglevel notice
logfile "" # 日志文件目录
databases 16 # 数据库的数量,默认是16
always-show-logo yes # 是否总是显示logo

快照

redis 是内存数据库,如果没有持久化,那么数据断电及失

1
2
3
4
5
6
7
8
9
10
11
# 如果900s内,如果至少有1个key进行过修改,就进行持久化操作
save 900 1
# 如果300s内,如果至少有10个key进行过修改,就进行持久化操作
save 300 10
# 如果60s内,有1000个key进行过修改,就进行持久化操作
save 60 1000

stop-writes-on-bgsave-error yes # 持久化如果出错,是否继续工作
rdbcompression yes # 是否压缩rdb文件,需要消耗一些cpu的资源
rdbchecksum yes # 保存rdb文件时,进行错误检查
dir ./ # rdb文件保存的位置

REPLICATION 主从配置

SECURITY 安全

可以设置 redis 的密码,默认是没有密码的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass # 查看当前密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 设置密码
OK
127.0.0.1:6379>
[root@localhost bin]# ./redis-cli -p 6379 # 断开重新连接
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> config get requirepass # 命令无权限
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 # 使用密码登录
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379>

CLIENTS 限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
maxclients 10000	# 设置redis最大连接数量
maxmemory <bytes> # redis 配置最大的内存容量
maxmemory-policy noeviction # 内存到达上限时的处理策略

# redis 中的默认的过期策略是 volatile-lru 。

# 设置方式 config set maxmemory-policy volatile-lru
# maxmemory-policy 六种方式
# 1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
# 2、allkeys-lru : 删除lru算法的key
# 3、volatile-random:随机删除即将过期key
# 4、allkeys-random:随机删除
# 5、volatile-ttl : 删除即将过期的
# 6、noeviction : 永不过期,返回错误

APPEND ONLY MODE aof 配置

1
2
3
4
5
6
appendonly no	# 默认是不开启aof模式的,默认使用rdb方式持久化,大部分情况下,rdb完全够用
appendfilename "appendonly.aof" # 持久化的文件的名字

# appendfsync always # 每次修改都会sync,消耗性能
appendfsync everysec # 每秒执行一次sync,可能会丢失这一秒的数据
# appendfsync no # 不执行sync,操作系统自己同步数据,速度最快

redis 持久化

redis 是内存数据库,如果不将内存中的数据保存到磁盘,那么一旦服务器进程退出,服务器中的数据也会丢失,所以 redis 提供了持久化功能!

RDB(Redis DataBase)

什么是 RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 Snapshot 快照,它恢复时是将快照中的文件直接读到内存中。

redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程不进行任何 IO 操作,这就确保了极高的性能。如果需要大规模数据的恢复,且对于数据恢复的完整性不是很敏感,那么 RDB 方式要比 ROF 方式更高效,RBD 的缺点就是最后一次持久化后的数据可能丢失,默认的就是 RDB,一般情况下不需要修改这个配置

image-20200711132619528

rdb 保存的文件:dump.rdb

image-20200711125649417

触发机制

  1. 满足 save 规则时,会触发 rdb

  2. 执行 flushall,也会触发 rdb

  3. 退出 redis ,也会触发 rdb

image-20200711130325698

如何恢复 rdb 文件

  1. 只要将 rdb 文件放在 redis 启动目录中,redis 启动的时候就会自动检查 dump.rdb 恢复其中的数据

  2. 查看 rdb 文件存放位置

    1
    2
    3
    4
    127.0.0.1:6379> config get dir
    1) "dir"
    2) "/usr/local/bin"
    127.0.0.1:6379>

优点

  1. 适合大规模数据恢复
  2. 对数据完整性要求不高

缺点

  1. 需要一定时间间隔进行操作,如果 redis 意外宕机了,最后一次修改的数据就没有了
  2. fork 进程时,会占用一定的内存空间

AOF(Append Only File)

将所有写入的命令记录下来,恢复时将命令重新执行一遍

AOF 是什么

以日志的形式记录每个写操作,将 redis 执行过的所有执行记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动时会读取该文件重新构建数据。换言之,redis 重启就是根据日志文件的内容将指令从前往后执行一次,完成数据的恢复工作

AOF 保存的文件:appendonly.aof

image-20200711133157744

默认是不开启的,需要手动开启 appendonly yes

重启 redis 即可生效

image-20200711133415125

进入 redis 执行命令,然后关机退出

image-20200711133750974

查看 appendonly.aof

1
vim /usr/local/bin/appendonly.aof
image-20200711133926344

手动修改 appendpnly.aof ,故意破坏内容后再次启动 redis 发现启动失败

image-20200711134153526

Redis 提供一个工具,用来修复 aof 文件

image-20200711134307591

修复成功后,重新启动 redis,会发现数据已恢复

image-20200711134600067

但是发现,k3 的数据丢失了!

优点

  1. 每一次修改都会同步,文件的完整性会更好

  2. 拥有不同的同步策略

    1
    2
    3
    4
    5
    6
    appendonly no	# 默认是不开启aof模式的,默认使用rdb方式持久化,大部分情况下,rdb完全够用
    appendfilename "appendonly.aof" # 持久化的文件的名字

    # appendfsync always # 每次修改都会sync,消耗性能
    appendfsync everysec # 每秒执行一次sync,可能会丢失这一秒的数据
    # appendfsync no # 不执行sync,操作系统自己同步数据,速度最快

缺点

  1. aof 的数据文件远远大于 rdb,恢复速度很慢
  2. 运行效率也低于 rdb,所以 redis 默认是用 rdb 持久化

aof 文件的重新规则

Aof 的规则就是无限追加,文件就会越来越大

image-20200711135327328

文件大于 64 M,就会 fork 一个新的进程重写文件

RBD 和 ROF 的对比

b5718f91883d9f0e90144e681d1c2ea7

扩展:

  1. RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
  2. AOF 持久化方式记录每次对服务器写的操作,当服务器重启时会重新执行这些命令来回复数据,AOF 命令以 Reids 协议追加保存每次写的操作到文件末尾(redis 还能对 aof 文件进行后台重写,使得 aof 文件的体积不至于过大)
  3. 如果只做缓存,只希望数据在服务器运行的时候存在,就不使用任何持久化
  4. 同时开启两种持久化方式
    1. 这种情况下,当 redis 重启的时候会优先载入 aof 文件来恢复原始的数据,因为在通常的情况下 aof 文件保存的数据集要比 rdb 文件保存的数据集更加完整
    2. rdb 的数据不实时,同时使用两者时服务器重启也会只找 aof 文件。那是不是只使用 aof 呢?建议不要,因为 rdb 更适合用于备份数据库(aof 在不断变化不好备份),快速重启,而且不会有 aof 可能潜在的bug,留着作为一个万一的手段
  5. 性能建议
    1. 因为 rdb 文件只用作备份用途,建议只在 slave 上持久化 rdb 文件,而且只要 15 分钟备份一次就够了,只保留 save 900 1 这条规则
    2. 如果开启 aof,好处是在最恶劣情况下丢失数据也不会超过两秒数据,启动脚本简单只需要 load 自己的 aof 文件即可。代价一是带来了持续的 IO,二是 aof rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 aof rewrite 的频率,aof rewrite 的基础大小默认值 64M 太小了,可以设置到 5G 以上,默认超过原大小 100% 重新可以改到适当的数值
    3. 如果不开启 aof,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔 IO,也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时宕掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 rdb 文件,载入比较新的那一个,微博就是这种

Redis 订阅发布

Redis 订阅发布(pub/sub)是一种消息通信模式

发布者(pub)发送消息、订阅者(sub)接收消息

redis 一个客户端可以订阅任意数量的频道

订阅发布消息图:

img

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

img

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

img

命令

序号 命令及描述
1 [PSUBSCRIBE pattern pattern …] 订阅一个或多个符合给定模式的频道。
2 [PUBSUB subcommand argument [argument …]] 查看订阅与发布系统状态。
3 PUBLISH channel message 将信息发送到指定的频道。
4 [PUNSUBSCRIBE pattern [pattern …]] 退订所有给定模式的频道。
5 [SUBSCRIBE channel channel …] 订阅给定的一个或多个频道的信息。
6 [UNSUBSCRIBE channel [channel …]] 指退订给定的频道。

测试

订阅端:

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> SUBSCRIBE star					# 订阅一个频道 star
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "star"
3) (integer) 1

# 等待读取信息
1) "message" # 消息
2) "star" # 频道
3) "liuyifei" # 消息具体内容
1) "message"
2) "star"
3) "dilireba"

发送端:

1
2
3
4
5
127.0.0.1:6379> PUBLISH star "liuyifei"			# 发布者发布消息到指定频道
(integer) 1
127.0.0.1:6379> PUBLISH star "dilireba" # 发布者发布消息到指定频道
(integer) 1
127.0.0.1:6379>

原理

通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个的 channel,而字典的值则是一个链表,链表保存了所有订阅这个 channel 的客户端,SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中

通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的 channel 作为键,在它维护的字典中查找记录了订阅这个 channel 的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者

Pub/Sub 从字面上理解就是发布(Publish)和订阅(Subscribe),在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值进行消息发布后,所有订阅它的客户端都会收到对应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天、群聊等

image-20200711150129632

Redis 主从复制

概念

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(master),后者称为从节点(slave)。数据的复制是单向的,只能由主节点到从节点。Master 以写为主,Slave 以读为主

默认情况下,每台 Redis 服务器都是主节点

一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点

主从复制的作用主要包括:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
  • 高可用(集群)基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。

一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的,原因如下:

  1. 从结构上,单个 Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
  2. 从容量上,单个 Redis 服务器内存容量有限,就算一台 Redis 服务器内容容量为 256G,也不能将所有内容用作 Redis 存储内存,一般来说,单台 Redis 最大使用内存不应该超过20G

电商网站上的商品,一般都是一次上传,无数次浏览,多读少写

对于这种场景,使用如下架构:

image-20200711151637321

环境配置

只配置从库,不用配置主库

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> info replication		# 查看当前库信息
# Replication
role:master # 角色:主机
connected_slaves:0 # 连接的从库数量:0
master_replid:b2f799e62d946ebb93873e5dae635337433fecd6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>

复制配置文件进行修改

1
2
3
4
5
6
7
8
9
[root@localhost config]# pwd
/usr/local/bin/config
[root@localhost config]# ll
total 336
-rw-r--r--. 1 root root 82658 Jul 10 08:32 redis6379.conf
-rw-r--r--. 1 root root 82658 Jul 10 08:33 redis6380.conf
-rw-r--r--. 1 root root 82658 Jul 10 08:35 redis6381.conf
-rw-r--r--. 1 root root 82646 Jul 10 06:47 redis.conf
[root@localhost config]#

修改 redis6379.conf、redis6380.conf、redis6381.conf

  1. 端口
  2. pid 文件
  3. log 文件名字 logfile
  4. dump.db 文件名字 dbfilename

依次使用这三个配置文件,启动三个服务

1
2
3
[root@localhost bin]# ./redis-server config/redis6379.conf
[root@localhost bin]# ./redis-server config/redis6380.conf
[root@localhost bin]# ./redis-server config/redis6381.conf

image-20200711153712383

分别连接三个服务:

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
# 6379
[root@localhost bin]# ./redis-cli -p 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_replid:b2f799e62d946ebb93873e5dae635337433fecd6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379>

# 6380
[root@localhost bin]# ./redis-cli -p 6380
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_replid:16ec0af43a4a5298c2fe49d925647fb1f86e670f
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6380>

# 6381
[root@localhost bin]# ./redis-cli -p 6381
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_replid:85dc09574791d7773085c83463bcb4338093252b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6381>

一主二从

默认情况下,每台 redis 服务器都是主节点,所以只用配置从机就好了

我们使用 6379 端口的作为主机,6380、6381 作为从机

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
# 在 6380 端口的客户端进行配置
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave # 当前角色为从机
master_host:127.0.0.1 # 主机的地址
master_port:6379 # 主机的端口
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:f9577f9528ab15f137d134365fb44acf24336789
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
127.0.0.1:6380>

########################################################################################

# 查看主机此时的状态
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1 # 已连接的从机数量:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=42,lag=1 # 第1个从机的信息
master_replid:f9577f9528ab15f137d134365fb44acf24336789
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:42
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:42
127.0.0.1:6379>

########################################################################################

# 在 6381 端口的客户端进行配置
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:280
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:f9577f9528ab15f137d134365fb44acf24336789
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:280
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:281
repl_backlog_histlen:0
127.0.0.1:6381>

########################################################################################

# 查看主机此时的状态
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2 # 已连接的从机数量:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=364,lag=0 # 第1个从机的信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=364,lag=1 # 第2个从机的信息
master_replid:f9577f9528ab15f137d134365fb44acf24336789
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:364
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:364
127.0.0.1:6379>

真实的主从配置应该从配置文件中配置,我们这里使用命令进行配置,只是暂时的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
################################# REPLICATION #################################

# Master-Replica replication. Use replicaof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
# +------------------+ +---------------+
# | Master | ---> | Replica |
# | (receive writes) | | (exact copy) |
# +------------------+ +---------------+
#
# 1) Redis replication is asynchronous, but you can configure a master to
# stop accepting writes if it appears to be not connected with at least
# a given number of replicas.
# 2) Redis replicas are able to perform a partial resynchronization with the
# master if the replication link is lost for a relatively small amount of
# time. You may want to configure the replication backlog size (see the next
# sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
# network partition replicas automatically try to reconnect to masters
# and resynchronize with them.
#
# replicaof <masterip> <masterport> 修改此处的地址和端口

细节

主机可以读写,从机只能读,主机中的所有信息和数据,都会同步到从机

主机写:

image-20200711155413665

从机读:

image-20200711155450395

测试:主机断开连接,从机是依旧链接到主机的,当主机重连时,从机依旧可以连接到主机

如果使用命令行配置主从,从机断开连接重新连接后,默认又会变回成主机的状态,再次配置为从机后,立马会将主机的数据同步过来

复制原理

Slave 启动成功连接到 Master 后会发送一个 sync 同步命令

Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕后,Master 将传送整个数据文件到 Slave,并完成一次完全同步

  • 全量复制:Slave 服务在接受到数据文件数据后, 将其存盘并加载到内存中
  • 增量复制:Master 继续将所有收集到的修改命令依次传给 Slave,完成同步

当 Slave 第一次连接或重新连接到 Master 时,一次完全同步(全量复制)将被自动执行

层层链路

image-20200711163750846

也可以实现主从复制

当 Master 断开连接,是否可以选择一台 Slave 作为新的 Master

当主机断开连接时,使用 SLAVEOF no one 设置从机成为新的主机,其他从机就可以连接新的主机了(手动)

哨兵模式

概述

当主机宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候我们优先考虑哨兵模式,Redis 从 2.8 就开始正式提供了 Sentinel (哨兵)架构来解决这个问题。

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立运行的进程,其原理是哨兵通过发送命了,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例

image-20200711170809458

哨兵的两个作用

  • 通过发送命令,让 Redis 服务器返回信息,监控其运行状态,包括主服务器和从服务器
  • 当哨兵检测到 Master 宕机,会自动将 Slave 切换成 Matser,然后通过订阅发布模式通知其他从服务器,修改配置文件,使从服务器重新绑定主服务器

然后一个哨兵进程对多个 Redis 服务器进行监控,可能会出现问题,为此,我们使用多个哨兵进行监控,各个哨兵之间还会进行互相监控,这样就形成了多哨兵模式

image-20200711173023514

假设主服务器宕机,哨兵 1 先检测到这个结果,系统不会马上进行 failover 过程,仅仅是哨兵 1 主观的认为主服务器不可用,最个现象称为主观下线。当后面的哨兵也检测到主服务器不可用,且数量达到一定值的时候,哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover(故障转移)的操作。切换成功后,通过订阅发布模式,由各个哨兵将自己监控的服务器切换主机,这个过程称为客观下线

测试

目前的状态是一主二从(6379 主机、6380、6381从机)

1、配置哨兵配置文件 sentinel.conf

1
2
3
# 最基本的配置
# sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1

后面数字1,代表主机挂了,进行投票看让哪一台 slave 接替成为主机,票高者当选

2、启动哨兵

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
[root@localhost bin]# ./redis-sentinel config/sentinel.conf
9761:X 10 Jul 2020 10:53:43.059 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
9761:X 10 Jul 2020 10:53:43.059 # Redis version=6.0.5, bits=64, commit=00000000, modified=0, pid=9761, just started
9761:X 10 Jul 2020 10:53:43.059 # Configuration loaded
9761:X 10 Jul 2020 10:53:43.060 * Increased maximum number of open files to 10032 (it was originally set to 1024).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.0.5 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 9761
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'

9761:X 10 Jul 2020 10:53:43.061 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
9761:X 10 Jul 2020 10:53:43.067 # Sentinel ID is 84e5276c728e9cb61bb41f66b4a146c9e5b46c2a
9761:X 10 Jul 2020 10:53:43.067 # +monitor master myredis 127.0.0.1 6379 quorum 1
9761:X 10 Jul 2020 10:53:43.069 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:53:43.075 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379

如果此时主机宕机,哨兵会从从机中选择一台服务器成为新的主机(投票算法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


9761:X 10 Jul 2020 10:56:39.583 # +sdown master myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.583 # +odown master myredis 127.0.0.1 6379 #quorum 1/1
9761:X 10 Jul 2020 10:56:39.584 # +new-epoch 1
9761:X 10 Jul 2020 10:56:39.584 # +try-failover master myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.590 # +vote-for-leader 84e5276c728e9cb61bb41f66b4a146c9e5b46c2a 1
9761:X 10 Jul 2020 10:56:39.590 # +elected-leader master myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.590 # +failover-state-select-slave master myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.660 # +selected-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.660 * +failover-state-send-slaveof-noone slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.739 * +failover-state-wait-promotion slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.858 # +promoted-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.858 # +failover-state-reconf-slaves master myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:39.921 * +slave-reconf-sent slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:40.890 * +slave-reconf-inprog slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:40.890 * +slave-reconf-done slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:40.993 # +failover-end master myredis 127.0.0.1 6379
9761:X 10 Jul 2020 10:56:40.993 # +switch-master myredis 127.0.0.1 6379 127.0.0.1 6381
9761:X 10 Jul 2020 10:56:40.993 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6381
9761:X 10 Jul 2020 10:56:40.993 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6381
9761:X 10 Jul 2020 10:57:11.030 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6381

可以看到 6381 成为了主机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=19800,lag=1
master_replid:1610d6694c340595a8713a46ccd630deadbf3fda
master_replid2:6aaa92137ca176c42b8138eea5a3f18714b63916
master_repl_offset:19932
second_repl_offset:18298
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1220
repl_backlog_histlen:18713
127.0.0.1:6381>

此时之前宕掉的主机重新连接后,只能归到新的主机成为从机,再次查看 6381

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=42197,lag=0
slave1:ip=127.0.0.1,port=6379,state=online,offset=42197,lag=1
master_replid:1610d6694c340595a8713a46ccd630deadbf3fda
master_replid2:6aaa92137ca176c42b8138eea5a3f18714b63916
master_repl_offset:42197
second_repl_offset:18298
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1220
repl_backlog_histlen:40978
127.0.0.1:6381>

优缺点

优点:

  1. 哨兵集群,基于主从复制模式,所有主从配置的有点,它全有
  2. 主从可以切换,故障可以转移,系统的可用性更好
  3. 哨兵模式是主从模式的升级,手动到自动,更加健壮

缺点:

  1. Redis 不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
  2. 哨兵模式的配置比较麻烦,很多的配置项

哨兵模式的全部配置

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 缓存穿透和雪崩

Redis 缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题,其中最要害的问题,就是数据一致性的问题,从严格意义上讲,此问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。

另外一些典型的问题比如:缓存穿透、缓存雪崩和缓存击穿。目前业界都有比较流行的解决方案

缓存穿透

概念

缓存穿透的概念很简单,客户端发起请求查询一个数据,发现 redis 内存数据库中没有,也就是缓存没有命中,于是向持久层数据库发起查询,发现也没有,查询失败。当用户很多的时候回,大量缓存没有命中,于是都去请求了持久层数据库,这样就给持久层数据库造成了跟大的压力,这时候就相当于出现了缓存穿透

image-20200711180527831

解决方案

布隆过滤器

布隆过滤器是一种概率型数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的压力。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的

原理:

布隆过滤器是一个叫“布隆”的人提出的,它本身是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是 0 ,就是 1。

现在我们新建一个长度为 8 的布隆过滤器,默认值都是 0,就像下面这样:

image-20200711190316300

现在我们添加 2008 这个数据,先通过三种不同的计算方式(hash1、hash2、hash3)得到三个数字

1
2
3
hash1('2008') = 1;
hash2('2008') = 4;
hash3('2008') = 6;

将得到的三个数字,填充至创建的布隆过滤器

image-20200711183921339

添加 2020 这个数据,先通过三种不同的计算方式(hash1、hash2、hash3)得到三个数字

1
2
3
hash1('2020') = 2;
hash2('2020') = 4;
hash3('2020') = 7;

将得到的三个数字,填充至创建的布隆过滤器

image-20200711184404398

可以看出,仅仅从布隆过滤器本身而言,根本没有存放完整的数据,只是运用一系列随机映射函数计算出位置,然后填充二进制向量。

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “2022” 这个值是否存在,哈希函数返回了 1、4、8 三个值,结果我们发现 8 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “2020” 这个值不存在。而当我们需要查询 “2006” 这个值是否存在的话,假设哈希函数返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “2006” 存在了么?答案是不可以,只能是 “2006” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “2006” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “2006” 这个值存在。

也就是说布隆过滤器只能判断数据是否一定不存在,而无法判断数据是否一定存在。

遗憾的是布隆过滤器是很难做到删除数据的

所以:

  • 优点:由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度够快
  • 缺点: 随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在

缓存空对象

当存储层不命中后,即时返回空对象也要缓存起来,同时设置一个过期时间,之后在访问这个数据将会从缓存中获取,保护了后端数据源

但这个方法存在两个问题:

  1. 当空值被缓存起来时,这就意味这缓存需要更多的空间存储更多的键,因为这当中可能包含很多空值的键
  2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据有一段时间的不一致,这个对于需要保持数据一致性的业务会有很大影响

缓存击穿

概述

这里需要注意和缓存穿透的区别,缓存击穿是指一个 key 非常热点,在不停的扛着大并发集中对着一个点进行访问。当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

在某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力过大(微博宕机)

解决方案

设置缓存永不过期

如果缓存永不过期,就肯定不会出现被击穿的可能

加互斥锁

使用分布式锁,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需等待即可,这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大

image-20200711190633421

缓存雪崩

概念

缓存雪崩,是指在某一段时间,缓存集中过期失效,或者 redis 宕机

当缓存集中过期或服务宕机时,所有请求到达存储层,也会造成存储层压力过大

集中过期不是非常致命,比较致命的缓存雪崩是缓存服务器再某个节点断网或宕机。因为自然形成的缓存雪崩,一定是在某个时间段内的,这个时候数据库也是可以抗住压力的,无非就是对数据库产生周期性的压力而已。而缓存服务器宕机,对数据库服务器造成的压力是不可预知的,有可能瞬间就压垮数据库服务器

解决方案

redis 高可用

这个思想是多增设几台 redis ,这样一台挂掉其他的还可以继续工作,其实就是集群的概念

限流降级

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待

数据预热

数据预热的含义是在正式部署前,先把可能的数据都预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀

该笔记整理与 B 站 up 主:狂神说Java

完整视频地址: https://www.bilibili.com/video/BV1S54y1R7SB

感谢狂神!