第 1 阶段:
app -> dao -> mysql
第 2 阶段:
app -> dao -> cache -> [mysql1, mysql2, mysql3]
第 3 阶段:
app -> dao -> cache -> 主库 -> [从库1, 从库2]
第 4 阶段:
app -> dao -> cache -> {[主库 -> (从库1, 从库2)], [主库 -> (从库1, 从库2)]}
互联网需求的 3 高:高并发,高可扩,高性能。
Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的 NoSQL(Not only SQL)数据库。
NoSQL 非关系型数据库和传统 RDBMS 关系型数据库相比的优势:
RDBMS
NoSQL
Redis 的常用使用场景:
Redis / Memcache / MongoDB 都是 NoSQL 数据库。
Redis 和 Memcache
Redis 和 MongoDB
传统的关系型数据库事务具备 ACID:
分布式数据库的 CAP:
Consistency - 强一致性
All nodes see the same data at the same time,更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性;一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题;从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
Availability - 高可用性
可用性指 Reads and writes always succeed,即服务一直可用,而且要是正常的响应时间;好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
Partition Tolerance - 分区容错性
即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务;分区容错性要求能够让应用,虽然是一个分布式系统,但看上去却是一个可以运转正常的整体;比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
CAP 理论提出就是针对分布式数据库环境的,所以,P 这个属性必须容忍它的存在,而且是必须具备的。
因为 P 是必须的,那么需要选择的就是 A 和 C。
在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当 P 发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:
最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,就面临着选择 A - 继续提供服务,但是数据不保证准确,C - 用户处于等待状态,一直等到数据同步完成。
分区是常态,不可避免,三者不可共存。
可用性和一致性:
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
Redis:http://www.redis.net.cn
图形工具:https://redisdesktop.com/download
虽然可以在安装在 windows 操作系统,但是官方不推荐,所以安装在 Linux 系统中。
1)上传 tar.gz 包 到 /opt 目录下,并解压:
tar -zxvf redis-5.0.4.tar.gz
2)安装 gcc(必须有网络):
yum -y install gcc
忘记是否安装过,可以使用 gcc -v 命令查看 gcc 版本,如果没有安装过,会提示命令不存在。
3)进入 Redis 目录,进行编译:
make
4)编译之后,开始安装:
make install
后台运行方式
Redis 默认不会使用后台运行,如果需要,修改配置文件daemonize=yes
,当后台服务启动的时候,会写成一个进程文件运行。
打开配置文件:
vim /opt/redis-5.0.4/redis.conf
注释掉 bind,关闭保护模式,并修改为后台启动:
...
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 loopback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bind 127.0.0.1
...
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode no
...
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes
...
以配置文件的方式启动:
cd /usr/local/bin
redis-server /opt/redis-5.0.4/redis.conf
29674:C 02 Oct 2021 02:56:47.338 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
29674:C 02 Oct 2021 02:56:47.338 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=29674, just started
29674:C 02 Oct 2021 02:56:47.338 # Configuration loaded
防火墙开放 Redis 的端口号:
firewall-cmd --zone=public --add-port=6379/tcp --permanent
firewall-cmd --reload
关闭数据库
redis-cli shutdown
redis-cli -p 6379 shutdown
常用操作
netstat -lntp | grep 6379
ps -ef | grep redis
连接 Redis 并测试
redis-cli
ping
Redis 在 linux 支持命令补全(tab)
HelloWorld
# 保存数据
set k1 china
# 获取数据
get kl
测试性能
先 ctrl + c,退出 Redis 客户端
redis-benchmark
执行命令后,命令不会自动停止,需要手动 ctrl+c 停止测试
====== PING_INLINE ======
100000 requests completed in 2.52 seconds
50 parallel clients
3 bytes payload
keep alive: 1
62.39% <= 1 milliseconds
99.95% <= 2 milliseconds
99.97% <= 3 milliseconds
100.00% <= 3 milliseconds
39682.54 requests per second
...
默认 16 个数据库
vim /opt/redis-5.0.4/redis.conf
...
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT where
# dbid is a number between 0 and 'databases'-1
databases 16
...
127.0.0.1:6379> get k1 # 查询 k1
"china"
127.0.0.1:6379> select 16 # 切换 16 号数据库
(error) ERR DB index is out of range # 数据库的下标超出了范围
127.0.0.1:6379> select 15 # 切换 15 号数据库
OK
127.0.0.1:6379[15]> get k1 # 查询 k1
(nil)
127.0.0.1:6379[15]> select 0 # 切换 0 号数据库
OK
127.0.0.1:6379> get k1 # 查询 k1
"china"
数据库键的数量
dbsize
清空数据库
flushdb
flushall
模糊查询(keys)
模糊查询 keys 命令,有三个通配符:*
,?
,[]
*
----- 通配任意多个字符
查询所有的键:
keys *
模糊查询 k 开头,后面随便多少个字符:
keys k*
模糊查询 e 为最后一位,前面随便多少个字符:
keys *e
双 * 模式,匹配任意多个字符 - 查询包含 k 的键:
keys *k*
?
----- 通配单个字符
模糊查询 k 字头,并且匹配一个字符:
keys k?
只记得第一个字母是 k,长度是 3
keys k??
[]
----- 通配括号内的某一个字符
记得其他字母,第二个字母可能是 a 或 e
keys r[ae]dis
键(key)
127.0.0.1:6379[15]> EXISTS k1
(integer) 1
127.0.0.1:6379[15]> EXISTS y1
(integer) 0
127.0.0.1:6379[15]> MOVE x1 8 # 将 x1 移动到 8 号库
(integer) 1 # 移动成功
127.0.0.1:6379[15]> EXISTS x1 # 查看当前库中是否存在 x1
(integer) 0 # 不存在(因为已经移走了)
127.0.0.1:6379[15]> SELECT 8 # 切换 8 号库
OK
127.0.0.1:6379[8]> KEYS * #查看当前库中的所有键
1) "x1"
127.0.0.1:6379[8]> TTL x1
(integer) -1 # 永不过期
127.0.0.1:6379[8]> set k1 v1 # 保存 k1
OK
127.0.0.1:6379[8]> TTL k1 # 查看 k1 的过期时间
(integer) -1 # 永不过期
127.0.0.1:6379[8]> EXPIRE k1 10 # 设置 k1 的过期时间为 10 秒
(integer) 1 # 设置成功
127.0.0.1:6379[8]> get k1 # 获取 k1
"v1"
127.0.0.1:6379[8]> TTL k1 # 查看 k1 的过期时间
(integer) 1 # 还有 1 秒过期
127.0.0.1:6379[8]> get k1
(nil) # 从内存中销毁了
127.0.0.1:6379[8]> type k1
string # k1 的数据类型是 string 字符串
操作文档:http://redisdoc.com/
127.0.0.1:6379[8]> set k1 v1 # 保存数据
OK
127.0.0.1:6379[8]> set k2 v2 # 保存数据
OK
127.0.0.1:6379[8]> keys *
1) "k2"
2) "k1"
127.0.0.1:6379[8]> del k2 # 删除数据 k2
(integer) 1
127.0.0.1:6379[8]> keys *
1) "k1"
127.0.0.1:6379[8]> get k1 # 获取数据 k1
"v1"
127.0.0.1:6379[8]> append k1 abc # 往 k1 的值追加数据 abc
(integer) 5 # 返回值的长度(字符数量)
127.0.0.1:6379[8]> get k1
"v1abc"
127.0.0.1:6379[8]> strlen k1 # 返回 k1 值的长度(字符数量)
(integer) 5
127.0.0.1:6379[8]> set k1 1 # 初始化 k1 的值为 1
OK
127.0.0.1:6379[8]> incr k1 # k1 自增 1(相当于 ++)
(integer) 2
127.0.0.1:6379[8]> incr k1
(integer) 3
127.0.0.1:6379[8]> get k1
"3"
127.0.0.1:6379[8]> decr k1 # k1 自减 1(相当于 --)
(integer) 2
127.0.0.1:6379[8]> decr k1
(integer) 1
127.0.0.1:6379[8]> get k1
"1"
127.0.0.1:6379[8]> INCRBY k1 3 # k1 自增 3(相当于 +=3)
(integer) 4
127.0.0.1:6379[8]> GET k1
"4"
127.0.0.1:6379[8]> DECRBY k1 2 # k1 自减 2(相当于 -=2)
(integer) 2
127.0.0.1:6379[8]> get k1
"2"
range:范围
127.0.0.1:6379[8]> SET k1 abcdef # 初始化 k1 的值为 abcdef
OK
127.0.0.1:6379[8]> get k1
"abcdef"
127.0.0.1:6379[8]> getrange k1 0 -1 # 查询 k1 全部的值
"abcdef"
127.0.0.1:6379[8]> getrange k1 0 3 # 查询 k1 的值,范围是下标 0 ~ 下标 3(包含 0 和 3,共返回 4 个字符)
"abcd"
127.0.0.1:6379[8]> setrange k1 1 xxx # 替换 k1 的值,从下标 1 开始提供为 xxx
(integer) 6
127.0.0.1:6379[8]> get k1
"axxxef"
set with expir:添加数据的同时设置生命周期
127.0.0.1:6379[8]> SETEX k1 5 v1 # 添加 k1 v1 数据的同时,设置 5 秒的声明周期
OK
127.0.0.1:6379[8]> get k1
"v1"
127.0.0.1:6379[8]> get k1 # 已过期,k1 的值 v1 自动销毁
(nil)
set if not exist:添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉
127.0.0.1:6379[8]> setnx k1 v1 # k1 不存在,添加成功
(integer) 1
127.0.0.1:6379[8]> setnx k1 zm # 添加失败,因为 k1 已经存在
(integer) 0
127.0.0.1:6379[8]> set k1 v1 k2 v2 # set 不支持一次添加多条数据
(error) ERR syntax error
127.0.0.1:6379[8]> mset k1 v1 k2 v2 k3 v3 # mset 可以一次添加多条数据
OK
127.0.0.1:6379[8]> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379[8]> mget k2 k3 # 一次获取多条数据
1) "v2"
2) "v3"
127.0.0.1:6379[8]> msetnx k3 v3 k4 v4 # 一次添加多条数据时,如果添加的数据中有已经存在的,则失败
(integer) 0
127.0.0.1:6379[8]> msetnx k4 v4 k5 v5 # 一次添加多条数据时,如果添加的数据中都不存在的,则成功
(integer) 1
127.0.0.1:6379[8]> getset k6 v6 # 因为没有 k6,所以 get 为 null,然后将 k6 的值 v6 添加到数据库
(nil)
127.0.0.1:6379[8]> keys *
1) "k6"
2) "k2"
3) "k1"
4) "k4"
5) "k5"
6) "k3"
127.0.0.1:6379[8]> get k6
"v6"
127.0.0.1:6379[8]> getset k6 vv6 # 先获取 k6 的值,然后修改 k6 的值为 vv6
"v6"
127.0.0.1:6379[8]> get k6
"vv6"
l:left 自左向右添加 (从上往下添加)
r:right 自右向左添加(从下往上添加)
# 从上往下添加
127.0.0.1:6379[1]> lpush list01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> keys *
1) "list01"
# 查询 list01 中的全部数据 0 表示开始,-1 表示结尾(队列,先进后出)
127.0.0.1:6379[1]> lrange list01 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
# 从下往上添加
127.0.0.1:6379[1]> rpush list02 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> lrange list02 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# 从左(上)边移除第一个元素
127.0.0.1:6379[1]> LPOP list02
"1"
# 从右(下)边移除第一个元素
127.0.0.1:6379[1]> RPOP list02
"5"
127.0.0.1:6379[1]> lrange list01 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
# 从上到下数,下标为 2 的值
127.0.0.1:6379[1]> lindex list01 2
"3"
# 从上到下数,下标为 1 的值
127.0.0.1:6379[1]> lindex list01 1
"4"
127.0.0.1:6379[1]> llen list01
(integer) 5
127.0.0.1:6379[1]> lpush list01 1 2 2 3 3 3 4 4 4 4
(integer) 10
# 从 list01 中移除 2 个 3
127.0.0.1:6379[1]> lrem list01 2 3
(integer) 2
127.0.0.1:6379[1]> lrange list01 0 -1
1) "4"
2) "4"
3) "4"
4) "4"
5) "3"
6) "2"
7) "2"
8) "1"
ltrim key begindex endindex
127.0.0.1:6379[1]> lpush list01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379[1]> lrange list01 0 -1
1) "9"
2) "8"
3) "7"
4) "6"
5) "5"
6) "4"
7) "3"
8) "2"
9) "1"
# 截取下标 3 ~ 6 的值,别的全扔掉
127.0.0.1:6379[1]> ltrim list01 3 6
OK
127.0.0.1:6379[1]> lrange list01 0 -1
1) "6"
2) "5"
3) "4"
4) "3"
127.0.0.1:6379[1]> rpush list01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> lrange list01 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379[1]> rpush list02 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> lrange list02 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# list01 右边出一个,从左进入到 list02 的第一个位置
127.0.0.1:6379[1]> RPOPLPUSH list01 list02
"5"
127.0.0.1:6379[1]> LRANGE list01 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379[1]> LRANGE list02 0 -1
1) "5"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
127.0.0.1:6379[1]> lrange list02 0 -1
1) "5"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
# 将 list02 中下标为 0 的元素修改成 x
127.0.0.1:6379[1]> lset list02 0 x
OK
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
# 从左边进入,在 list02 中的 2 元素之前插入 java
127.0.0.1:6379[1]> linsert list02 before 2 java
(integer) 7
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "java"
4) "2"
5) "3"
6) "4"
7) "5"
# 从左边进入,在 list02 中的 2 元素之后插入 redis
127.0.0.1:6379[1]> linsert list02 after 2 redis
(integer) 8
127.0.0.1:6379[1]> lrange list02 0 -1
1) "x"
2) "1"
3) "java"
4) "2"
5) "redis"
6) "3"
7) "4"
8) "5"
列表性能总结:类似添加火车皮一样,头尾操作效率高,中间操作效率惨
与 Java 中的 set 特点类似,不允许重复
# 添加元素(自动排除重复元素)
127.0.0.1:6379[1]> sadd set01 1 2 2 3 3 3
(integer) 3
# 查询 set01 集合
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "2"
3) "3"
# 存在
127.0.0.1:6379[1]> sismember set01 2
(integer) 1
# 不存在
127.0.0.1:6379[1]> sismember set01 5
(integer) 0
注意:1 和 0 不是下标,而是布尔值;1 - true 存在,0 - false 不存在
# 集合中有 3 个元素
127.0.0.1:6379[1]> scard set01
(integer) 3
# 移除 set01 中的元素 2; 1 表示移除成功
127.0.0.1:6379[1]> srem set01 2
(integer) 1
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "3"
127.0.0.1:6379[1]> sadd set01 1 2 3 4 5 6 7 8 9
(integer) 9
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
# 从 set01 中随机获取 3 个元素
127.0.0.1:6379[1]> srandmember set01 3
1) "2"
2) "3"
3) "9"
# 从 set01 中随机获取 5 个元素
127.0.0.1:6379[1]> srandmember set01 5
1) "8"
2) "9"
3) "3"
4) "1"
5) "6"
127.0.0.1:6379[1]> smembers set01
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
# 随机移除一个元素
127.0.0.1:6379[1]> spop set01
"8"
# 随机移除一个元素
127.0.0.1:6379[1]> spop set01
"1"
127.0.0.1:6379[1]> SADD set01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> SADD set02 x y z
(integer) 3
# 将 set01 中的元素 3 移动到 set02 中
127.0.0.1:6379[1]> smove set01 set02 3
(integer) 1
127.0.0.1:6379[1]> smove set01 set02 5
(integer) 1
127.0.0.1:6379[1]> SMEMBERS set01
1) "1"
2) "2"
3) "4"
交集 - sinter
并集 - sunion
差集 - sdiff
127.0.0.1:6379[1]> SADD set01 1 2 3 4 5
(integer) 5
127.0.0.1:6379[1]> SADD set02 2 a 1 b 3
(integer) 5
127.0.0.1:6379[1]> SINTER set01 set02
1) "1"
2) "2"
3) "3"
127.0.0.1:6379[1]> SUNION set01 set02
1) "b"
2) "1"
3) "a"
4) "5"
5) "2"
6) "3"
7) "4"
# 在 set01 中存在,在 set02 中不存在
127.0.0.1:6379[1]> sdiff set01 set02
1) "4"
2) "5"
# 在 set02 中存在,在 set01 中不存在
127.0.0.1:6379[1]> sdiff set02 set01
1) "b"
2) "a"
类似 java 里面的 Map
Key / Value(键值对)模式不变,但 Value(值)又是一个键值对
# 添加 user,值为 id=1001
127.0.0.1:6379[1]> HSET user id 1001
(integer) 1
127.0.0.1:6379[1]> HGET user
(error) ERR wrong number of arguments for 'hget' command
# 查询 user,必须指明具体的字段
127.0.0.1:6379[1]> HGET user id
"1001"
# 添加学生 student,属性一堆
127.0.0.1:6379[1]> HMSET student id 101 name tom age 22
OK
# 获取学生名字
127.0.0.1:6379[1]> HGET student name
"tom"
# 获取学生名字和年龄
127.0.0.1:6379[1]> HMGET student name age
1) "tom"
2) "22"
# 获取学生全部信息
127.0.0.1:6379[1]> HGETALL student
1) "id"
2) "101"
3) "name"
4) "tom"
5) "age"
6) "22"
# 删除学生年龄属性
127.0.0.1:6379[1]> HDEL student age
(integer) 1
127.0.0.1:6379[1]> HGETALL student
1) "id"
2) "101"
3) "name"
4) "tom"
127.0.0.1:6379[1]> HGETALL student
1) "id"
2) "101"
3) "name"
4) "tom"
# student 属性的数量,id 和 name 共两个属性
127.0.0.1:6379[1]> HLEN student
(integer) 2
# student 中是否存在 name 属性
127.0.0.1:6379[1]> HEXISTS student name
(integer) 1
# student 中是否存在 age 属性
127.0.0.1:6379[1]> HEXISTS student age
(integer) 0
# 获取 student 所有的属性名
127.0.0.1:6379[1]> HKEYS student
1) "id"
2) "name"
# 获取 student 所有属性的值(内容)
127.0.0.1:6379[1]> HVALS student
1) "101"
2) "tom"
127.0.0.1:6379[1]> hmset student id 101 name tom age 22
OK
# 自增整数 2
127.0.0.1:6379[1]> HINCRBY student age 2
(integer) 24
127.0.0.1:6379[1]> HGET student age
"24"
127.0.0.1:6379[1]> HMSET user id 1001 money 1000
OK
# 自增小数 5.5
127.0.0.1:6379[1]> HINCRBYFLOAT user money 5.5
"1005.5"
127.0.0.1:6379[1]> HGET user money
"1005.5"
# 添加失败,因为 age 已存在
127.0.0.1:6379[1]> HSETNX student age 18
(integer) 0
# 添加成功,因为 sex 不存在
127.0.0.1:6379[1]> HSETNX student sex male
(integer) 1
127.0.0.1:6379[1]> hgetall student
1) "id"
2) "101"
3) "name"
4) "tom"
5) "age"
6) "24"
7) "sex"
8) "male"
需求:
充 10 元可享 vip1;
充 20 元可享 vip2;
充 30 元可享 vip3;
...
127.0.0.1:6379> ZADD zset01 10 vip1 20 vip2 30 vip3 40 vip4 50 vip5
(integer) 5
# 查询数据
127.0.0.1:6379> zrange zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
5) "vip5"
# 带着分数查询数据
127.0.0.1:6379> zrange zset01 0 -1 withscores
1) "vip1"
2) "10"
3) "vip2"
4) "20"
5) "vip3"
6) "30"
7) "vip4"
8) "40"
9) "vip5"
10) "50"
(
:不包含limit
:跳过几个截取几个# 20 <= score <= 40
127.0.0.1:6379> ZRANGEBYSCORE zset01 20 40
1) "vip2"
2) "vip3"
3) "vip4"
# 20 <= score < 40
127.0.0.1:6379> ZRANGEBYSCORE zset01 20 (40
1) "vip2"
2) "vip3"
# 20 < score < 40
127.0.0.1:6379> ZRANGEBYSCORE zset01 (20 (40
1) "vip3"
# 10 <= score <= 40,共返回四个,跳过前 2 个,取 2 个
127.0.0.1:6379> ZRANGEBYSCORE zset01 10 40 limit 2 2
1) "vip3"
2) "vip4"
# 20 <= score <= 40,共返回四个,跳过前 2 个,取 1 个
127.0.0.1:6379> ZRANGEBYSCORE zset01 10 40 limit 2 1
1) "vip3"
# 移除 vip5
127.0.0.1:6379> ZREM zset01 vip5
(integer) 1
127.0.0.1:6379> zrange zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
# 集合中元素的个数
127.0.0.1:6379> ZCARD zset01
(integer) 4
# 分数在 20 ~ 30之间,共有几个元素
127.0.0.1:6379> ZCOUNT zset01 20 30
(integer) 2
# vip3 在集合中的下标(从上向下)
127.0.0.1:6379> ZRANK zset01 vip3
(integer) 2
# 通过元素获得对应的分数
127.0.0.1:6379> ZSCORE zset01 vip2
"20"
127.0.0.1:6379> ZRANGE zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
127.0.0.1:6379> ZREVRANK zset01 vip3
(integer) 1
# 顺序查询
127.0.0.1:6379> ZRANGE zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
# 逆序查询
127.0.0.1:6379> ZREVRANGE zset01 0 -1
1) "vip4"
2) "vip3"
3) "vip2"
4) "vip1"
# 逆序查询分数在 30 ~ 20 之间的(注意,先写大值,再写小值)
127.0.0.1:6379> ZREVRANGEBYSCORE zset01 30 20
1) "vip3"
2) "vip2"
# 如果小值在前,则结果为 null(empty list or set)
127.0.0.1:6379> ZREVRANGEBYSCORE zset01 20 30
(empty list or set)
Redis DataBase
在指定的时间间隔内,将内存中的数据集的快照写入磁盘;
默认保存在 /usr/local/bin 中,文件名 dump.rdb。
自动备份
Redis 是内存数据库,当每次用完 Redis,关闭 Linux 时,Redis 会自动将数据备份到一个文件中:/usr/local/bin/dump.rdb
1. 默认的自动备份策略不利于测试,所以修改 redis.conf 文件中的自动备份策略
vim /opt/redis-5.0.4/redis.conf
进入 vim 编辑器的底行模式,输入 /SNAPSHOTTING 并点击回车键进行搜索,修改 SNAPSHOTTING 下的备份策略:
save 900 1 # 900 秒内,至少变更 1 次,才会自动备份
save 120 10 # 120 秒内,至少变更 10 次,才会自动备份
save 60 10000 # 60 秒内,至少变更 10000 次,才会自动备份
如果只是用 Redis 的缓存功能,不需要持久化,那么可以注释掉所有的 save 行来停用保存功能,可以直接一个空字符串来实现停用:save ""
重启 Redis-Server
/usr/local/bin/redis-server /opt/redis-5.0.4/redis.conf
2. 使用 shutdown 关闭 Redis 来模拟关机;关机之前和关机之后,对比 dump.rdb 文件的更新时间
[root@localhost bin]# redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> keys *
1) "zset01"
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> SHUTDOWN
not connected> exit
注意:当使用 shutdown 命令,Redis 会自动将数据库备份,所以 dump.rdb 文件创建时间更新了
3. 启动 Redis,要在 120 秒内改变 10 条数据,再查看 dump.rdb 文件的更新时间(开两个终端窗口,方便查看)
cd /usr/local/bin
ll | grep dump
4. 120 秒内改变 10 条数据这一动作触发了备份指令,目前,/usr/local/bin/dump.rdb 文件中保存了 10 条数据,将 dump.rdb 拷贝一份 dump10.rdb,此时两个文件中都保存 10 条数据
5. 数据已经备份了,这时清空全部数据库 flushall,再次 shutdown 模拟关机
6. 再次启动 Redis,发现数据消失了,dump.rdb 文件中的内容并没有恢复到 Redis 中
因为,当保存 10 条以上的数据时,数据备份起来了;
然后删除数据库,备份文件中的数据,也没问题;
但是,这个 shutdown 命令一旦执行,就会立刻备份,将删除之后的空数据库生成备份文件,将之前 10 条数据的备份文件覆盖掉了;所以,自动恢复失败。
为了解决这个问题,就要将备份文件再备份。
7. 将 dump.rdb 文件删除,将 dump10.rdb 重命名为 dump.rdb
8. 重新启动 redis 服务,登录 redis,数据 10 条全部恢复
手动备份
之前自动备份,必须更改好多数据,例如上边,改变了十多条数据,才会自动备份;
如果只保存一条数据想立刻备份,就需要每次操作完成,执行命令 save 就会立刻备份。
127.0.0.1:6379> set k1 x1
OK
127.0.0.1:6379> SAVE
OK
与 RDB 相关的配置
查看配置文件:
vim /opt/redis-5.0.4/redis.conf
快照 SNAPSHOTTING 下的配置解释如下
优:适合大规模数据恢复,对数据完整性和一致性要求不高。
劣:一定间隔备份一次,意外 down 掉,就失去最后一次快照的所有修改。
Append Only File
以日志的形式记录每个写操作;
将 Redis 执行过的写指令全部记录下来(读操作不记录);
只允许追加文件,不可以改写文件;
Redis 在启动之初会读取该文件从头到尾执行一遍,这样来重新构建数据。
开启 AOF
1)为了避免失误,最好将 redis.conf 总配置文件备份一下,然后再修改内容如下:
appendonly yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
2)重新启动 Redis,以新配置文件启动
redis-server /opt/redis-5.0.4/redis.conf
3)连接 Redis,加数据,删库,退出
4)查看当前文件夹 /usr/local/bin 多一个 AOF 文件,看看文件中的内容,保存的都是写操作
-- 文件中的删库语句要删除,否则数据恢复不了。
-- 编辑这个文件,如果不是 root 权限则要底行模式输入 wq! 强制执行
5)最后只需要重新连接,数据恢复成功
AOF 和 RDB 的优先级
查看 redis.conf 文件,AOF 和 RDB 两种备份策略可以同时开启,那系统会怎样选择?
1)编辑 appendonly.aof,修改为乱码,保存退出
2) 启动 Redis 失败
所以是 AOF 优先载入来恢复原始数据;因为 AOF 比 RDB 数据保存的完整性更高。
3)修复 AOF 文件,删除不符合 Redis 语法规范的代码
reids-check-aof --fix appendonly.aof
与 AOF 相关的配置
查看配置文件:
vim /opt/redis-5.0.4/redis.conf
文件追写模式 APPEND ONLY MODE 下的配置解释如下:
RDB:只用作后备用途,建议 15 分钟备份一次就好。
AOF:
可以一次执行多个命令,是一个命令组,一个事务中,所有命令都会序列化(排队),不会被插队;一个队列中,一次性,顺序性,排他性的执行一系列命令。
三特性:
三步走:
与关系型数据库事务相比:
开启事务,加入队列,一起执行,并成功
# 开启事务
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> MULTI
OK
127.0.0.1:6379> SET k1 v1111
QUEUED
127.0.0.1:6379> SET k2 v2222
QUEUED
# 放弃操作
127.0.0.1:6379> DISCARD
OK
# 还是原来的值
127.0.0.1:6379> get k1
"v1"
一句报错,全部取消,恢复到原来的值
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k4 v4
QUEUED
# 一句报错
127.0.0.1:6379> setvcdnkaj
(error) ERR unknown command `setvcdnkaj`, with args beginning with:
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> keys *
1) "k1"
2) "k3"
3) "k2"
追究责任,谁报的错,找谁去
127.0.0.1:6379> MULTI
OK
# 虽然 v1 不能 ++,但是加入队列并没有报错,类似 java 中的通过编译
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v5
QUEUED
# 真正执行的时候,报错
127.0.0.1:6379> EXEC
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
127.0.0.1:6379> KEYS *
1) "k2"
2) "k1"
3) "k3"
4) "k4"
5) "k5"
Watch 命令用于监视一个 (或多个) key ,如果在事务执行之前这个 (或这些) key 被其他命令所改动,那么事务将被打断。
模拟收入与支出。
# 收入 100 元
127.0.0.1:6379> set in 100
OK
# 支出 0 元
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> multi
OK
# 收入 -20
127.0.0.1:6379> decrby in 20
QUEUED
# 支出 +20
127.0.0.1:6379> incrby out 20
QUEUED
# 结果,没问题
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
在开启事务之前,先执行 watch 监控收入 in:
127.0.0.1:6379> watch in
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby in 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
在 exec 之前,开启了另一个窗口(线程),对监控的 in 做了修改:
127.0.0.1:6379> set in 1000
OK
127.0.0.1:6379> get in
"1000"
第一个窗口的事务将被打断(失效),类似于“乐观锁”:
127.0.0.1:6379> exec
(nil)
一旦执行了 exec 命令,那么之前加的所有监控自动失效。
进程间的一种消息通信模式:发送者 publish 发送消息,订阅者 subscribe 接收消息。
例如:微信订阅号。
订阅一个或多个频道。
一个窗口作为订阅者 Subscriber
127.0.0.1:6379> clear
# 1.订阅三个频道
127.0.0.1:6379> SUBSCRIBE cctv1 cctv2 cctv3
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cctv1"
3) (integer) 1
1) "subscribe"
2) "cctv2"
3) (integer) 2
1) "subscribe"
2) "cctv3"
3) (integer) 3
另一个窗口作为发送者 Publisher
# 2.发送消息
127.0.0.1:6379> PUBLISH cctv3 NBA
(integer) 1
127.0.0.1:6379> PUBLISH cctv1 good
(integer) 1
第一个窗口的订阅者收到消息:
# 3.接收到推送过来的信息
1) "message"
2) "cctv3"
3) "NBA"
1) "message"
2) "cctv1"
3) "good"
就是 Redis 集群的策略。
配从(库)不配主(库):从库可以选择谁是主库,但主库不能选择从库。
读写分离:主机写,从机读。
1)准备三台服务器(192.168.186.128,192.168.186.129,192.168.186.130),并修改 redis.conf 配置文件。
2)启动三台 Redis,并查看每台机器的角色,都是 Master。
[root@localhost redis-5.0.4]# /usr/local/bin/redis-cli
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_replid:68eda0fe7309a2dca0a1d4f7ac8ca53682c42142
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
3)测试开始
首先,将三个机器的 Redis 数据库全都清空,第一台添加值:
127.0.0.1:6379> MSET k1 v1 k2 v2
OK
其余两台机器,复制(找主库):
127.0.0.1:6379> SLAVEOF 192.168.186.128 6379
OK
第一台再添加值:
127.0.0.1:6379> set k3 v3
OK
这个测试可以看出:
可以获得 slave 之前的 k1 和 k2,只要跟了主库,主库之前的数据也会立刻同步。
可以获得 slave 之后的 k3,只要跟了大哥,数据会立刻同步。
主机(128 master)可以添加成功,从机(129 和 130 是 slave)失败,从机只负责读取 数据,无权写入数据,这就是“读写分离”。
主机显示两个从机:
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.186.130,port=6379,state=online,offset=70,lag=1
slave1:ip=192.168.186.129,port=6379,state=online,offset=70,lag=1
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70
4. 主机 128 SHUTDOWN,从机 129 和 130 仍然是 slave,并显示他们的 master 已离线:
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.186.128
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:808
master_link_down_since_seconds:21
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:f1f8b79327c718104a335e6a259511266d687e7c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:808
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:808
5. 主机 128 重启,从机 129 和 130 仍然是 slave,并显示他们的 master 已上线:
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.186.128
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
6. 关闭一个从机 129,主机 128 没有变化,只是显示少了一个 slave:
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.186.130,port=6379,state=online,offset=266,lag=1
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:266
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:266
7. 而重启归来的从机自立门户成为了 master,不和原来的集群在一起了:
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_replid:db4bc824a9b2da127ba6108ce8ff513179c1b609
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
一个主机理论上可以多个从机,但是这样的话,这个主机会很累。
可以使用 java 面向对象继承中的传递性来解决这个问题,减轻主机的负担。
129 跟随 128:
127.0.0.1:6379> SLAVEOF 192.168.186.128 6379
OK
130 跟随 129:
127.0.0.1:6379> SLAVEOF 192.168.186.129 6379
OK
128:
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.186.129,port=6379,state=online,offset=1120,lag=0
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1120
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1120
129:
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.186.128
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:1106
slave_priority:100
slave_read_only:1
connected_slaves:1
slave0:ip=192.168.186.130,port=6379,state=online,offset=1106,lag=0
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1106
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:953
repl_backlog_histlen:154
130:
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.186.129
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:1092
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:dc6fc5fcaf449d33dad91ecabe3ba28d07dd1926
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1092
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1092
1 个主机,2 个从机,当 1 个主机挂掉了,只能从 2 个从机中再次选 1 个主机。
手动选主机。
模拟测试:1 为 master,2 和 3 为 slave,当 1 挂掉后,2 选为 master,3 跟 2。
关闭主机 128,从机 129 执行命令成为主机:
slaveof no one
从机 130 执行命令跟随新主机 129:
SLAVEOF 192.168.186.129 6379
当 128 再次回归,128 和 129 已经形成新的集群,和 128 没有任何的关系了,所以 128 成为了光杆司令。
1)Slave 服务器链接 Master 服务器,并发送同步请求。
2)Master 服务器启动后台的存盘进程,同时会收集所有写(Write)的命令集。
3)Master 服务器发送快照到 Slave 服务器。
4)Slave 服务器载入快照。
5)Master 服务器发送缓存写(Write)命令。
6)Slave 服务器执行命令。
完成上面几个步骤后就完成了 Slave 服务器数据初始化的所有操作,Slave 服务器此时可以接收来自用户的读请求。
但只要是重新连接 Master,一次性(全量复制)同步将自动执行。
Redis 主从同步策略:主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。
如果有需要,Slave 在任何时候都可以发起全量同步。
Redis 的策略是,无论如何,首先会尝试进行增量同步;如不成功,则要求 Slave 进行全量同步。
哨兵 Sentinel 是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
模拟测试:
1)128 为主服务器,129 和 130 为从服务器。
2)每一台服务器中在 /usr/local/bin 目录下创建一个配置文件 sentinel.conf,名字要正确,并编辑 sentinel.conf。
sentinel monitor 被监控主机名(自定义) ip port 票数
128 服务器 sentinel.conf:
sentinel monitor redis128 192.168.186.128 6379 1
129 服务器 sentinel.conf:
sentinel monitor redis129 192.168.186.129 6379 1
130 服务器 sentinel.conf:
sentinel monitor redis130 192.168.186.130 6379 1
3)启动服务的顺序:启动 128 服务器为 Master -> 启动 129 和 130 服务器为 Master -> 打开新的三个窗口分别启动 Redis 的哨兵 Sentinel 128、129、130。
/usr/local/bin/redis-sentinel sentinel.conf
4)将 128 挂掉,后台自动发起投票,选出新的主服务器。
127.0.0.1:6379> shutdown
not connected> exit
5)查看最后的分配:129 成为了新的主服务器,130 还是从服务器。
6)如果之前的主服务器再次归来:128 再次归来,自己成为了 master,和 129 平等;过了几秒之后,被哨兵检测到了 128 号机的归来,128 将变为 Slave。
42486:X 04 Oct 2021 10:56:08.098 # +sdown slave 192.168.186.128:6379 192.168.186.128 6379 @ redis128 192.168.186.129 6379
42486:X 04 Oct 2021 10:57:41.459 # -sdown slave 192.168.186.128:6379 192.168.186.128 6379 @ redis128 192.168.186.129 6379
42486:X 04 Oct 2021 10:57:51.434 * +convert-to-slave slave 192.168.186.128:6379 192.168.186.
所有的写操作都是在 master 上完成的,然后再同步到 slave 上,所以两台机器之间通信会有延迟。
当系统很繁忙的时候,延迟问题会加重。
Slave 机器数量增加,问题也会加重。
执行 vim /opt/redis-5.0.4/redis.conf 直接查看所有配置信息的详解