Redis

431 阅读57分钟

概述:

Redis 是一个开源(BSD 许可)的内存数据结构存储,用作数据库、缓存、消息代理和流引擎。Redis 提供数据结构,例如 字符串、散列、列表、集合、 带范围查询的排序集合、位图、超日志、地理空间索引。Redis 内置了复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久性,并通过以下方式提供高可用性Redis SentinelRedis Cluster的自动分区。

您可以 对这些类型运行原子操作,例如附加到字符串; 增加哈希值;将元素推入列表;计算集交、并 、差;或获取排序集中排名最高的成员。

为了达到最佳性能,Redis 使用 内存中的数据集。根据您的用例,Redis 可以通过定期将数据集转储到磁盘将每个命令附加到基于磁盘的日志来持久化您的数据。如果您只需要一个功能丰富的网络内存缓存,您也可以禁用持久性。

Redis 支持异步复制,具有快速非阻塞同步和自动重新连接以及网络拆分上的部分重新同步。

Redis的作用:

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

2.效率高,可用高速缓存

3.发布订阅系统

4.地图信息分析

5.计时器,计数器(浏览量) 等等。。。

Redis 下载:

Linux下载:

1、从官网下载安装包

wget https://github.com/redis/redis/archive/7.0.0.tar.gz

2、解压

tar -zxvf 7.0.0.tar.gz

3、进入redis目录

4、安装基本环境

# 第一步
yum install gcc-c++

# 第二步:
make

5、redis的默认安装路径,在usr/local/bin目录下面。

Redis简单使用:

官方命令库:redis.io/commands/

Redis安装后。系统默认将redis-server等运行文件放在 /usr/local/bin目录下的。redis启动是需要依赖redis-conf件的,所以启动redis-server服务之前,需要将原来的redis-conf文件复制一份到/usr/local/bin目录下。

Redis是不区分大小写的!!!!!!

1、启动redis服务

redis-server

2、进入redis:

redis-cli -p `端口号`   # 通过指定端口号启动

redis-cli  # 默认的启动端口号为:6379

20220519135903.png

3、选择数据库:

# redis默认有16个数据库,我们可以通过select index来选择
select index # index 为数据库索引,从 0 开始到 15

20220519140441.png

4、操作数据:

Redis是一个存放 Key:Vilue 类型的数据的数据库,所以我们在插入数据的时候直接通过 key 和 value插入即可

插入:

# 向redis中插入数据:
set key value
例:插入key为 name :value为 ferry的一条数据
set name ferry
注:一个键值对的键和值是一一对应的,一个键只对应一个值,如果重复对同一个key赋值,则新的值会覆盖原先的值

20220519140934.png

查看:

# 在 redis 中查看数据:
get key   # 直接get 他的 key即可查看他的value(值)
keys *    # 查看所有的key
例:get name

20220519141230.png

查看指定的key是否存在:

exist key   # key 为要查看的key

127.0.0.1:6379> keys *   # 查看全部key
1) "pwd"
2) "name"
3) "age"
127.0.0.1:6379> exists name  # 查看 key为 name 的数据是否存在
(integer) 1                  # 表示 key 为 name 的数据存在

查看key的数据类型:

127.0.0.1:6379> TYPE pwd  # 查看pwd的数据类型
string                    # 数据类型为string

删除:

Move:

删除一条数据

127.0.0.1:6379> keys *      # 先查看所有的key 
1) "name"                   # 可以看到 现在我们有两条数据他们的key分别为 "name" 和 "age"
2) "age"
127.0.0.1:6379> move age 1  # 删除第一个数据库里面的 key 为 age 的这一条数据
(integer) 1
127.0.0.1:6379> keys *      # 再次查看所有的key
1) "name"                   # 只有name了,age被删了
127.0.0.1:6379> 

Flushdb:

删除一个数据库的所有数据

127.0.0.1:6379> flushdb  #  清除当前数据库的所有数据
OK
127.0.0.1:6379> keys *
(empty array) 

flushall :

删除全部数据库的所有数据

flushall # 清除全部数据库的所有数据 

设置过期时间:

expire key `时间` # 设置在 指定 key 对应的数据有效时间(按秒计数)
ttl key          # 查看 指定key 剩余有效时间

127.0.0.1:6379> EXPIRE name 5  # 设置 key=name对应的数据的有效时间为 5 秒
(integer) 1
127.0.0.1:6379> ttl name       # 查看这条数据剩余有效时间
(integer) 3                    # 剩余有效时间为 3 秒
127.0.0.1:6379> ttl name
(integer) 1                    # 剩余有效时间为 1 秒
127.0.0.1:6379> ttl name
(integer) 0                    # 剩余有效时间为 0 秒
127.0.0.1:6379> ttl name
(integer) -2                   # 剩余有效时间为 -2 秒,则表示这条数据已经失效,被删除
127.0.0.1:6379> keys *         # 查看所有key
1) "pwd"
2) "age"                       # 此时已经没有key=name这条数据

Redis的五大数据类型:

String类型(字符串):

1、常规操作:

127.0.0.1:6379> set name ferry         # 设置值
OK
127.0.0.1:6379> get name               # 获取值
"ferry"
127.0.0.1:6379> keys *                 # 查看所有key
1) "pwd"
2) "name"
3) "age"
127.0.0.1:6379> EXISTS name            # 检查key是否存在
(integer) 1
127.0.0.1:6379> APPEND name 2000       # 在指定key的值后面追加字符
(integer) 9
127.0.0.1:6379> get name               # 获取值
"ferry2000"
127.0.0.1:6379> STRLEN name            # 获取字符串的长度
(integer) 9

2、自增操作:

# 实现自增操作:INCR
27.0.0.1:6379> set num 0               # 设置unm 初始值为 0 
OK
127.0.0.1:6379> get num                # 查看num 的值
"0"
127.0.0.1:6379> INCR num               # 使num的值自增1,即:num++(默认是自增 1 ,当然也可以自定义步长)
(integer) 1
127.0.0.1:6379> get num                # 查看num自增后的值
"1"                                    # num 由原来的 0 变成 1
127.0.0.1:6379> INCR num               # 使num的值再次自增1,即 num++          
(integer) 2                            # num 由原来的 1 变成 2
127.0.0.1:6379> get num
"2"

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

# 自定义步长 INCRBY
127.0.0.1:6379> INCRBY num 10          # 使num自增 10 , 即:num+=10
(integer) 12
127.0.0.1:6379> get num                # 查看num的值
"12"                                   # 自增了 10

3、自减操作:

# 自减 DECR
# 自定义步长:DECRBY

127.0.0.1:6379> get num
"12"
127.0.0.1:6379> DECR num      # 自减
(integer) 11
127.0.0.1:6379> get num
"11"
127.0.0.1:6379> DECR num      # 自减
(integer) 10
127.0.0.1:6379> get num
"10"
127.0.0.1:6379> DECRBY num 5  # 自定义步长
(integer) 5
127.0.0.1:6379> get num
"5"

4、截取字符串:

# 截取字符串 GETRANGE key `开始的下标` `结束的下标`   ---字符串的下标从 0 开始---
127.0.0.1:6379> set name ferry       # 设置name 的值
OK
127.0.0.1:6379> get name             # 获取 name 的值
"ferry"
127.0.0.1:6379> GETRANGE name 0 2    # 截取 name 的字符串,从第 0 个到第 2 个:
"fer"
127.0.0.1:6379> GETRANGE name 0 -1   # 截取全部的字符串
"ferry"

5、替换字符串:

# 替换字符串 SETRANGE key `开始的下标` `要替换的内容`  ----从指定下标开始的哪一个字符开始的n个字符替换的,要替换的内容

127.0.0.1:6379> set age  0123456       # 设置 age 的值
OK
127.0.0.1:6379> get age                # 获取 age 的值
"0123456" 
127.0.0.1:6379> SETRANGE age 1 xxx     # 替换从指定位置开始的字符串,
(integer) 7
127.0.0.1:6379> get age                # 获取 age 的值
"0xxx456"                              # 从 下标为 1 开始的连续的 3 个字符被替换成了 xxx
127.0.0.1:6379> 

6、setex和setnx:

# setex (set with expire)     设置值的同时,设置过期时间

# setnx (set if not exist)    设置一个不存在的值--如果key不存在就设置,如果key已经存在就不设置(一般在分布式锁中会经常使用)
##################################

# setex
27.0.0.1:6379> setex name 60 ferry    # 设置name的值为 ferry ,有效时间为60秒
OK
127.0.0.1:6379> ttl name              # 查看name剩余有效时间
(integer) 57                          # 剩余有效时间为57秒
127.0.0.1:6379> get name              # 在name的有效时间内,可以查看name的值
"ferry"
127.0.0.1:6379> ttl name              # 查看name剩余有效时间
(integer) 40                          # 剩余有效时间为40秒
127.0.0.1:6379> ttl name             
(integer) 25                          # 剩余有效时间为25秒
127.0.0.1:6379> ttl name
(integer) 2                           # 剩余有效时间为2秒
127.0.0.1:6379> ttl name
(integer) -2                          # 剩余有效时间为-2秒,表示此时name已经失效
127.0.0.1:6379> get name              # 再次查看name的值
(nil)                                 # 发现 name 已经被删除了

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

# setnx
27.0.0.1:6379> keys *                  # 查看所有的key
(empty array)                          # key 为 空
127.0.0.1:6379> setnx name ferry       # 设置 name 的值为 ferry
(integer) 1                            # 设置成功
127.0.0.1:6379> keys *                 # 查看所有的key
1) "name"
127.0.0.1:6379> get name               # 查看name 的值
"ferry"
127.0.0.1:6379> setnx name xxxxxxxxxx  # 设置 name的值为xxxxxxxxxx
(integer) 0                            # 设置失败(因为前面已经设置过了 name)
127.0.0.1:6379> get name               # 查看name的值
"ferry"                                # name的值仍然为 ferry

7、mset和mget:

# mset(mset key1 value1 key2 value2 key3 value3 ...)  同时设置多个值  

# mget(key1 key2 key3 ...)  同时获取多个值

27.0.0.1:6379> keys *                      # 查看所有的keys
(empty array)
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3     # 同时设置 k1 = v1,k2 = v2,k3 = v3
OK                                         # 设置成功
127.0.0.1:6379> keys *                     # 查看所有的 key
1) "k3"
2) "k1"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3              # 获取所有的值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> mget k1                    # 获取k1的值
1) "v1"

##############################
# 同时设置多个值也适用于 setex

127.0.0.1:6379> keys *               # 查看所有数据
1) "k3"
2) "k1"
3) "k2"
127.0.0.1:6379> msetnx k4 v4 k5 v5 k6 v6       # 通过msetnx设置 k4 k5 k6
(integer) 1
127.0.0.1:6379> keys *                         # 查看所有key
1) "k3"
2) "k2"
3) "k6"
4) "k1"
5) "k4"
6) "k5"
127.0.0.1:6379> mget k4 k5 k6                  # 同时查看k4 k5 k6 的值
1) "v4"
2) "v5"
3) "v6"
127.0.0.1:6379> msetnx k6 v6 k7 v7             # 通过mstnx设置 k6 k7
(integer) 0                                    # k6 已经存在,所以设置失败
127.0.0.1:6379> keys *                         # 查看所有数据
1) "k3"
2) "k2"
3) "k6"
4) "k1"
5) "k4"
6) "k5"

8、对象 :

set user:1{name:zhangshan,age:2}                                #设置一个对象user:1,值为json字符串来保存的一个对象

127.0.0.1:6379> mset user:1:name zhangshan user:1:age 2            # 设置对象user:1的name为zhangshan , age为2
OK                        
127.0.0.1:6379> keys *                                             # 获取全部的key
1) "user:1:age"
2) "user:1:name"
127.0.0.1:6379> mget user:1:age user:1:name                        # 查看user:1 的name和age
1) "2"
2) "zhangshan"

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

9、组合命令

# getset  先get在set
127.0.0.1:6379> getset name ferry  # 如果name不存在则返回nil(空),然后再设置name的值为ferry
(nil)
127.0.0.1:6379> get name           # 获取 name 的值
"ferry"                            # 为我们设置的值
127.0.0.1:6379> getset name FERRY  # 如果name存在则返回name原来的值,然后再对name设置新的值
"ferry"                            # 返回name的值为原来设置的值
127.0.0.1:6379> get name           # 再次获取name的值
"FERRY"                            # 此时 name 的值为新的值

10、String类型的数据的使用场景:

图片.png

图片取自: blog.csdn.net/weixin_4539…

List类型:

在 Redis 里面,可以把 List 当成队列阻塞队列使用。

list 实际是一个链表,左右都可以插入值。

如果 key 不存在,创建新的链表。

如果移除了所有元素,空链表也代表不存在。

在两边插入或者改动值,效率最高;操作中间元素,效率相对低一些。

1、赋值:

LPUSH : 向左插入

20220520105259.png

#LPUSH key value[]    ---这里的key 就相当于表名 ,value 就向相当于表中的内容,可以同时插入多条数据。

127.0.0.1:6379> LPUSH list l1           # 从左插入一条数据
(integer) 1
127.0.0.1:6379> LPUSH list l2
(integer) 2
127.0.0.1:6379> LPUSH list l3
(integer) 3
127.0.0.1:6379> LPUSH list l4 l5 l6     # 从左 插入多条数据
(integer) 6
######################################################################

# 查看数据
# LRANGE key `起始下标` `结束下标` -----下标从 0 开始 

127.0.0.1:6379> LRANGE list 0 1         # 查看第1 和 第2 条数据
1) "l6"
2) "l5"
127.0.0.1:6379> LRANGE list 0 -1        # 查看全部数据  0 -1
1) "l6"
2) "l5"
3) "l4"
4) "l3"
5) "l2"
6) "l1"

LPUSH : 向右插入

20220520173914.png

127.0.0.1:6379> RPUSH list 0 1 2 3 4      # 向右插入数据
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1     # 查看全部数据
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
127.0.0.1:6379> LRANGE list 0 1         # 查看下标为0 和 1 的数据
1) "0"
2) "1"
127.0.0.1:6379>

从中间插入 Linsert

# insert 从list中的某个值的 前面或后面插入一个值

27.0.0.1:6379> LRANGE list 0 -1                   # 查看list中的值
1) "4"
2) "3"
3) "2"
4) "1"
5) "0"
127.0.0.1:6379> LINSERT list before 4 five        # 在 4 的前面加上一个 five
(integer) 6
127.0.0.1:6379> LRANGE list 0 -1                  # 再次查看list中的值
1) "five"										  # five添加成功
2) "4"
3) "3"
4) "2"
5) "1"
6) "0"
127.0.0.1:6379> LINSERT list after 0 xxx          # 在 0 的 后面加上 xxx
(integer) 7 
127.0.0.1:6379> LRANGE list 0 -1                  # 再次查看list中的值
1) "five"
2) "4"
3) "3"
4) "2"
5) "1"
6) "0"
7) "xxx"                                         # xxx 添加成功!!
127.0.0.1:6379> 

小结:

  • Redis中的list数据类型相当于一个链表,
  • 如果 key 不存在,则创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有值,空链表也就代表不存在了
  • 如果在两边插入或者改动值,效率最高,中间元素,相对来书效率低一点。
  • 它 既可以当作队列使用也可以当作来使用。

Set(集合):

set的值是无序且不重复的!!! (无序,唯一)

1、set 集合中添加元素

127.0.0.1:6379> sadd set 0 1 2 hello "world"      # 向集合中添加元素
(integer) 5
127.0.0.1:6379> SMEMBERS set                      # 查看集合中的所有元素
1) "2"
2) "hello"
3) "1"
4) "0"
5) "world"

2、查看set集合中的元素

127.0.0.1:6379> SISMEMBER set hello         # 检查指定的元素是否存在于集合中 
(integer) 1                                        # hello 在集合中
127.0.0.1:6379> SISMEMBER set hel          
(integer) 0                                       # hel不在集合中
27.0.0.1:6379> SCARD set                          # 查看指定集合(set)中有几条元素。
(integer) 5
127.0.0.1:6379> 

3、随机获取集合中的元素

127.0.0.1:6379> sadd set 0 1 2 3 4 5 6 7 8 9 10       # 先向集合set中插入元素
(integer) 11
127.0.0.1:6379> SMEMBERS set                          #查看 set 集合中的元素
 1) "0"
 2) "1"
 3) "2"
 4) "3"
 5) "4"
 6) "5"
 7) "6"
 8) "7"
 9) "8"
10) "9"
11) "10"
127.0.0.1:6379> SRANDMEMBER set                    # 随机获取set中的一个元素
"0"
127.0.0.1:6379> SRANDMEMBER set                    # 随机获取set中的一个元素
"5"
127.0.0.1:6379> SRANDMEMBER set                    # 随机获取set中的一个元素 
"3"
127.0.0.1:6379> SRANDMEMBER set                    # 随机获取set中的一个元素 
"8"
127.0.0.1:6379> SRANDMEMBER set 2                  # 随机获取set中的两个元素
1) "2"
2) "4"
127.0.0.1:6379> SRANDMEMBER set 2                  # 随机获取set中的两个元素
1) "1"
2) "0"
127.0.0.1:6379> SRANDMEMBER set 2                  # 随机获取set中的两个元素
1) "0"
2) "8"
127.0.0.1:6379> SRANDMEMBER set 2                  # 随机获取set中的两个元素
1) "5"
2) "8"
127.0.0.1:6379> 

4、删除set集合中的元素

# 移除集合中指定的元素
127.0.0.1:6379> SMEMBERS set            # 先查看set中有哪些元素。
1) "2"
2) "1"
3) "hello"
4) "0"
127.0.0.1:6379> SREM set hello         # 移除set中的 hello 元素。
(integer) 1
127.0.0.1:6379> SMEMBERS set           # 再次查看set中的元素,可看到set被移除。
1) "2"
2) "1"
3) "0"

# 随机移除集合中的元素
127.0.0.1:6379> SMEMBERS sete         # 查看集合中的所有元素
 1) "0"
 2) "1"
 3) "2"
 4) "3"
 5) "4"
 6) "5"
 7) "6"
 8) "7"
 9) "8"
10) "9"
127.0.0.1:6379> SCARD sete           # 查看集合中的元素个数
(integer) 10                         # 此时共有 10 个元素
127.0.0.1:6379> SPOP sete            # 随机删除一个  元素
"0"                                  # 随机移除了 0 
127.0.0.1:6379> SPOP sete
"8"
127.0.0.1:6379> SPOP sete
"3"
127.0.0.1:6379> SMEMBERS sete        # 再次查看集合中的所有元素
1) "1"
2) "2"
3) "4"
4) "5"
5) "6"
6) "7"
7) "9"
127.0.0.1:6379> SCARD sete          # 再次查看集合中的元素个数
(integer) 7                         # 移除了 3 个元素后只剩下 7 个元素
127.0.0.1:6379> 

5、将元素在不同的集合之间移动

127.0.0.1:6379> SADD set1 0 1 2 3 4 5                # 创建集合set1
(integer) 6
127.0.0.1:6379> SMEMBERS set1 
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
6) "5"
127.0.0.1:6379> SADD set2 a b c d e f               # 创建集合 set2 
(integer) 6
127.0.0.1:6379> SMEMBERS set2
1) "c"
2) "d"
3) "f"
4) "a"
5) "b"
6) "e"

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

# smove `源头集合` `目标集合` 要移动的值
127.0.0.1:6379> SMOVE set1 set2 0                   # 将集合set1 中的元素 0 ,移动到集合 set2 中去 
(integer) 1
127.0.0.1:6379> SMEMBERS set1                       # 查看集合 set1 中的元素
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
127.0.0.1:6379> SMEMBERS set2                       # 查看集合 set2 中的元素
1) "d"
2) "f"
3) "a"
4) "b"
5) "c"
6) "e"
7) "0"

6、差集、交集、并集

127.0.0.1:6379> sadd s1 a b c d e          # 创建集合 s1
(integer) 5
127.0.0.1:6379> sadd s2 c s w a b          # 创建集合 s2
(integer) 5
127.0.0.1:6379> SMEMBERS s1                # 查看集合s1 的元素
1) "b"
2) "a"
3) "c"
4) "d"
5) "e"
127.0.0.1:6379> SMEMBERS s2                # 查看集合s2 的元素
1) "a"
2) "s"
3) "w"
4) "c"
5) "b"

# 差集:SDIFF
127.0.0.1:6379> SDIFF s1 s2        # s1 与 s2 的差集
1) "e"
2) "d"

127.0.0.1:6379> SDIFF s2 s1        # s2 与 s1 的差集
1) "s"
2) "w"


# 交集:SINTER
127.0.0.1:6379> SINTER s1 s2      # 集合s1 与 集合s2 的交集
1) "b"
2) "a"
3) "c"

# 并集:SUNION                    
127.0.0.1:6379> SUNION s1 s2      # 集合 s1 和 集合s2 的并集
1) "s"
2) "b"
3) "a"
4) "c"
5) "d"
6) "e"
7) "w"

Hash(哈希):

Map集合,key-map!!,这是一个Map集合。Hash类型的数据本质上和String是没有太大区别的,仍然是一个key-value键值对。

1、插入元素

127.0.0.1:6379> hset hash name ferry age 123       # 插入两个个Hash类型的键值对
(integer) 2
127.0.0.1:6379> hget hash name                     # 获取 name 的值 
"ferry"
127.0.0.1:6379> hget hash age                      # 获取 age 的值
"123"
127.0.0.1:6379> hset hash name FERRY               # 再次给name赋值,name已经存在再赋值会覆盖原来的值
(integer) 0
127.0.0.1:6379> hget hash name                     # 再次获取name的值,原来的值已被覆盖
"FERRY"
127.0.0.1:6379> hmget hash name age                # 获取 name和age的值
1) "FERRY"
2) "123"
127.0.0.1:6379> HGETALL hash                       # 获取hash表中的所有键值对
1) "name"     # 键
2) "FERRY"    # 值
3) "age"      # 键
4) "123"      # 值

#############################
# 获取 hash 中有多少个键值对
127.0.0.1:6379> HLEN hash
(integer) 2
127.0.0.1:6379> 

Zset(有序集合):

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

1、添加值

127.0.0.1:6379> ZADD zset 100 a                # 添加一个值
(integer) 1 
127.0.0.1:6379> ZADD zset 200 b 300 c 400 d    # 添加多个值
(integer) 3

2、排序

# ZRANGEBYSCORE zset min max           # 按照从小到大的顺序排序
127.0.0.1:6379> ZRANGEBYSCORE zset -inf +inf   # 从负无穷 到 正无穷的顺序排序
1) "a"
2) "b"
3) "c"
4) "d"
127.0.0.1:6379> ZRANGEBYSCORE zset -inf +inf withscores
1) "a"
2) "100"
3) "b"
4) "200"
5) "c"
6) "300"
7) "d"
8) "400"
127.0.0.1:6379> ZRANGEBYSCORE zset -inf 300 withscores  # 查找小于等于300的值
1) "a"
2) "100"
3) "b"
4) "200"
5) "c"
6) "300"
#######################
# ZREVRANGE zset start stop          从大到小排序
127.0.0.1:6379> ZREVRANGE zset 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> ZREVRANGE zset 0 -1 withscores
1) "c"
2) "300"
3) "b"
4) "200"
5) "a"
6) "100"
127.0.0.1:6379> 

3、移除

127.0.0.1:6379> ZRANGE zset 0 -1      # 查看zset中的全部值
1) "a"
2) "b"
3) "c"
4) "d"
127.0.0.1:6379> ZREM zset d           # 移除 zset 中的 d
(integer) 1
127.0.0.1:6379> ZRANGE zset 0 -1      # 再次查看 zset 中的全部值
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> 

4、查找

127.0.0.1:6379> ZRANGE zset 0 -1               # 查看zset中的值
1) "a"
2) "b"
3) "c"
4) "d"
127.0.0.1:6379> ZCARD zset                     # 查看zset中有多少个数据
(integer) 3
127.0.0.1:6379> ZCOUNT zset 100 300            # 查看指定区内的数据个数(闭区间)
(integer) 3                               

三种特殊数据类型:

用处:定位,附近的人,打车距离1

1、GEOADD:添加地理信息

将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。这些数据将会存储到sorted set,这样的目的是为了方便使用GEORADIUS或者GEORADIUSBYMEMBER命令对数据进行半径查询等操作。

该命令以采用标准格式的参数x,y,所以经度必须在纬度之前。这些坐标的限制是可以被编入索引的,区域面积可以很接近极点但是不能索引。具体的限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:

  • 有效的经度从-180度到180度。
  • 有效的纬度从-85.05112878度到85.05112878度。

当坐标位置超出上述指定范围时,该命令将会返回一个错误。

# 添加中国城市 经 纬度 信息到sorted set元素的数目,但不包括已更新score的元素。
#               GEOADD    KEY     `经度`  `维度`  `标识` 
127.0.0.1:6379> GEOADD china:city 106.54 29.58 chongqing                            # 添加重庆的信息
(integer) 1
127.0.0.1:6379> GEOADD china:city 116.40 39.90 beijing 104.07 30.67 chengdu         # 添加北京和成都
(integer) 2
127.0.0.1:6379> GEOADD china:city 113.26 23.10 guangzhou 114.10 22.54 shenzhen      # 添加广州和深圳
(integer) 2

2、GEODIST:地理距离

作用:根据经纬度,查看两个地方的直线距离

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

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

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

# 获取 广州 到 北京的直线距离 
#               GEODIST    key     `起点`     `终点` `单位`
127.0.0.1:6379> GEODIST china:city guangzhou beijing     # 获取 广州到北京的直线距离,单位默认为 m:米
"1891820.6571"
127.0.0.1:6379> GEODIST china:city guangzhou beijing M   # 设置单位为 m:米
"1891820.6571"
127.0.0.1:6379> GEODIST china:city guangzhou beijing KM  # 设置单位为 km:千米
"1891.8207"
127.0.0.1:6379> GEODIST china:city guangzhou beijing FT  # 设置单位为 mi:英里
"6206760.6861"
127.0.0.1:6379> GEODIST china:city guangzhou beijing MI  # 设置单位为 ft:英尺
"1175.5258"

3、GEOHASH:哈希

作用:返回一个或多个位置元素的11位的hash值

# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,则表示两个地方的距离越近!反之亦然!
127.0.0.1:6379> GEOHASH china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm7b22u5cz0"

4、GEOPOS:位置

key里返回所有给定位置元素的位置(经度和纬度)。

# 从 key 中 返回指定位置的经纬度信息
#               GEOPOS   KEY    `指定的位置`
127.0.0.1:6379> GEOPOS china:city beijing   # 返回北京的经纬度信息
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
127.0.0.1:6379> 

5、GEORADIUS:地理半径

作用:通过指定的经纬度为中心,查找指定半径内的所有元素。如:查找 附近的人 之类的操作,就是运用这个原理。

#               GEORADIUS   KEY      `指定经度` `指定纬度` `指定半径` `半径单位` `其他操作(可以同时跟多个参数)`
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km  # 在china:city中查找位于经度:110 维度:30这个地方半径500km内的城市
1) "chongqing"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 在china:city中查找位于经度:110 维度:30这个地方半径1000km内的城市
1) "chongqing"
2) "chengdu"
3) "shenzhen"
4) "shenzhenclear"
5) "guangzhou"

其他操作:

可以同时有多个操作。

(1)WITHCOORD:坐标
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km WITHCOORD  # 查找并输出被查找到城市的经纬度信息
1) 1) "chongqing"
   2) 1) "106.54000014066696167"    # 经度
      2) "29.57999948859571049"     # 纬度
(2)WITHDIST:距离
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km WITHDIST
1) 1) "chongqing"
   2) "337.2242"                    # 距离中心位置的距离,单位与半径单位一致。

6、GEORADIUSBYMEMBER:距离

这个命令和GEORADIUS命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS那样, 使用输入的经度和纬度来决定中心点。

# 根据指定城市为中心来查找,指定半径内的所有城市--(包括它本身)。-----也可以结合前面的一些其他操作
#               GEORADIUSBYMEMBER    KEY    `指定城市` `半径` `半径单位`
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 200 km  # 在china:city中查找 以北京 为中心半径200km内的城市
1) "beijing"                                                 # 在china:city中只找到 北京 
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1500 km # 查找 以北京为中心 ,半径1500km内的城市
1) "chongqing"
2) "beijing"

7、总结:

GEO的底层原理其实就是ZSET类型的数据,所以对于我们录入的GEO数据也可以用zset类型的命令来操作。

127.0.0.1:6379> ZRANGE china:city 0 -1          # 查看zset类型的数据中的,全部元素。
1) "chongqing"
2) "chengdu"
3) "shenzhen"
4) "shenzhenclear"
5) "guangzhou"
6) "beijing"
# 对于china:city的操作还可以使用zset的相关命令来实现对china:city进行增删改查的操作。

Hyperloglog:超级日志

1、基数?

基数(cardinal number)在数学上,是集合论中刻画任意集合大小的一个概念。两个能够建立元素间一一对应的集合称为互相对等集合。例如3个人的集合和3匹马的集合可以建立一一对应,是两个对等的集合。

A{1,2,3,4,5,6}

B{1,2,3,4,5,6,6}

基数 = 6

基数:在一个集合中不重复的元素的个数,可以接收误差。

2、命令:

Redis 中有三个 HyperLogLog 命令:PFADDPFCOUNTPFMERGE

127.0.0.1:6379> PFADD log1 0 1 2 3 4            # 向log1中添加元素
(integer) 1
127.0.0.1:6379> PFCOUNT log1                    # 查看log1中元素的个数
(integer) 5
127.0.0.1:6379> PFADD log2 2 3 5 4 6            # 向log2中添加元素
(integer) 1
127.0.0.1:6379> PFCOUNT log2                    # 查看log2中元素的个数
(integer) 5
127.0.0.1:6379> PFMERGE log3 log1 log2          # 将log1和log2的并集,存放到log3中去
OK
127.0.0.1:6379> PFCOUNT log3                    # 查看log3中的元素个数
(integer) 7                                     # {0,1,2,3,4,5,6} ---刚好七个元素
127.0.0.1:6379> 

BitMaps:位图

位储存

假设:模拟每周每天打卡情况;

#  这里 0 1 2 3 4 可换成其他的参数,其含义为自定义
127.0.0.1:6379> SETBIT week 0 1        # 0 表示 星期天 ; 1 表示 已打卡
(integer) 0
127.0.0.1:6379> SETBIT week 1 1        # 1 表示 星期一 ; 1 表示 已打卡
(integer) 0
127.0.0.1:6379> SETBIT week 2 1        #  ...
(integer) 0
127.0.0.1:6379> SETBIT week 3 0        #  ...
(integer) 0
127.0.0.1:6379> SETBIT week 4 1        #  ...
(integer) 0
127.0.0.1:6379> SETBIT week 5 0        #  ...
(integer) 0
127.0.0.1:6379> SETBIT week 6 0        # 6 表示 星期六 ; 0 表示 未打卡
(integer) 0

查看某一天是否打卡:

127.0.0.1:6379> getbit week 0          # 查看星期天是否打卡
(integer) 1                            # 表示 已打卡
127.0.0.1:6379> getbit week 6          # 查看星期六是否打卡
(integer) 0                            # 表示 未打卡
127.0.0.1:6379> 

统计打卡天数:

127.0.0.1:6379> BITCOUNT week          # 查看一共有多少天打卡
(integer) 4                            # 表示一周中有4天在打卡
127.0.0.1:6379> 

四、事务

事务四个特性

  • 原子性:事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性:事务在完成时,必须是所有的数据都保持一致状态。
  • 隔离性:并发事务执行之间无影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性。
  • 持久性:一旦事务完成,数据库的改变必须是持久化的。

Redis事务的本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!

Redis事务没有隔离级别的概念

所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!

Redis单挑命令是保证原子性的,但是事务不保证原子性!!

Redis的事务:

  • 开启事务(MULTI)
  • 命令入队(...)
  • 执行事务(exec)

一个事务:

####  一个事务开始   ####
127.0.0.1:6379> MULTI                # 开启Redis事务
OK   
####  命令入队  ####
127.0.0.1:6379(TX)> set k1 v1        # 添加一个事务
QUEUED                               # 添加成功
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec             # 执行事务
1) OK
2) OK
3) "v2"
4) OK                                # 按照添加的事务顺序执行命令
####  一个事务结束   ####
127.0.0.1:6379> 

取消事务:

####  一个事务开始   ####
27.0.0.1:6379> MULTI
OK
####  命令入队  ####
127.0.0.1:6379(TX)> set k1 v1        # 添加一个事务
QUEUED                               # 添加成功
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> DISCARD          # 取消事务
OK
####  一个事务结束   ####
127.0.0.1:6379> get k4               # 被取消的事务不会被执行
(nil) 
127.0.0.1:6379> 

编译异常:

代码有问题,命令有错误!事务中的所有命令都不会被执行!

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1 k2 v2 k3 v3 k4 v4
QUEUED
127.0.0.1:6379(TX)> getset k3    #错误的命令
(error) ERR wrong number of arguments for 'getset' command  # 报异常
127.0.0.1:6379(TX)> get k5
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> EXEC   # 事务执行报报错!
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1     # 所有的命令都不会执行
(nil)
127.0.0.1:6379> 

运行异常:

如果事务队列中存在语法性错误,那么执行命令的时候,其他命令是可以正常执行的,错误命令会抛异常。

127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> incr k1                              # 命令没有问题,但是语法上错误的命令。 
QUEUED
127.0.0.1:6379(TX)> mset k1 v2 k2 v2 k3 v3 
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (error) ERR value is not an integer or out of range   # 第一条命令报错了,但是其他命令依旧可以执行成功
2) OK
127.0.0.1:6379> mget k1 k2 k3
1) "v2"
2) "v2"
3) "v3"
127.0.0.1:6379> 

监控:

悲观锁: (性能低,影响效率!!)

  • 很悲观,认为任何时候都会出现问题,无论做什么都会上锁!

乐观锁: ()

  • 很乐观,认为任何时候都不会出现问题,所以不会上锁!在更新的时候去判断一下,在此期间是否有人修改过这个数据。
  • 获取version
  • 更新的时候比较version

正常执行成功:

127.0.0.1:6379> set money 100 
OK
127.0.0.1:6379> WATCH money              # 开始监视 money
OK
127.0.0.1:6379> MULTI                    # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 10 
QUEUED
127.0.0.1:6379(TX)> INCRBY money 30
QUEUED
127.0.0.1:6379(TX)> EXEC                # 执行成功,事务正常结束,事务执行期间数据没有改动。
1) (integer) 90
2) (integer) 120
127.0.0.1:6379>

多线程修改之后,使用watch当作redis的乐观锁:

127.0.0.1:6379> set money 100 
OK
127.0.0.1:6379> WATCH money              # 开始监视 money
OK
127.0.0.1:6379> MULTI                    # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 10 
QUEUED
127.0.0.1:6379(TX)> INCRBY money 30
QUEUED
127.0.0.1:6379(TX)> EXEC                # 在执行事务之前,数据被修改,导致执行失败!!
(nil)
127.0.0.1:6379> 

五、Jedis

Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能写成漂亮的代码。

使用:

1、导入依赖

    <dependencies>
        <!--Redis-->
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.2.2</version>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.3</version>
        </dependency>
    </dependencies>

2、编码测试:

连接数据库:

public class TestPing {
    public static void main(String[] args) {
        //new Jedis即可
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //jedis的所有方法就是,对应在redis中的所有命令。
        System.out.println(jedis.ping());
    }
}

输出:

image-20220524163857552.png

常用API:

与常用数据类型的操作命令是一致的,完全一样!!!!!!!!!!!!!

五大数据类型:

  • String
public class TestString {
    public static void main(String[] args) {
        // 连接数据库
        Jedis jedis = new Jedis("127.0.0.1", 6379);

        System.out.println("清空数据 : "+jedis.flushDB());
        System.out.println("判断某个键(name)是否存在 : "+jedis.exists("name"));
        System.out.println("新增<'name','ferry'>键值对 : "+jedis.set("name","ferry"));
        System.out.println("新增<'pwd','123'>键值对 : "+jedis.set("pwd","123"));
        System.out.println("系统中所有的键值对如下 : ");
        Set<String> keys = jedis.keys("*");
        System.out.println(keys);


        System.out.println("删除pwd : "+jedis.del("pwd"));
        System.out.println("判断pwd是否存在 : "+jedis.exists("pwd"));
        System.out.println("查看name的数据类型 : "+jedis.type("name"));
        System.out.println("随机返回key中的一个值 : "+jedis.randomKey());
        System.out.println("重命名name : "+jedis.rename("name","username"));
        System.out.println("获取更改后的name : "+jedis.get("username"));
        System.out.println("按索引查询 : "+jedis.select(0));
    }
}
  • Set

...

  • List

...

  • Zset

...

  • Hash

...

三大特殊数据类型:

  • geo

...

  • bitMap

...

  • Hyperloglog

...

断开连接:

直接jedis.close()即可;

public class TestPing {
    public static void main(String[] args) {
        //new Jedis即可
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //jedis的所有方法就是,对应在redis中的所有命令。
        System.out.println(jedis.ping());

        jedis.close();//关闭数据库连接
    }
}

事务操作:

仍然与使用命令操作完全一致,没有什么差别!!!

public class TestTX {
    public static void main(String[] args) {
        // 连接数据库
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        jedis.select(1);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello","世界");
        jsonObject.put("name","ferry");
        jsonObject.put("pwd","123");
        //开启事务
        Transaction multi = jedis.multi();
        String string = jsonObject.toJSONString();
​
        //事务入队
        try{
            multi.set("user1",string);
            multi.set("user2",string);
            multi.set("user3",string);
            multi.exec(); //执行事务
        }catch (Exception e){
            multi.discard();//如果抛异常则取消(放弃)事务
            e.printStackTrace();
        }finally {
            System.out.println(jedis.mget("user1", "user2"));
            jedis.close();//最后关闭连接
        }
    }
}

结果:

image-20220524172237080.png

image-20220524172256930.png

六、SpringBoot整合:

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-redis</artifactId> 
</dependency>

2、配置连接

我们在使用SpringBoot整合Redis的时候,不在使用jedis,我们使用的是lettuce,它采用netty,实例可以在多个线程享,不存在线程不安全的情况,可以减少线程数量,更新NIO模式,而jedis,采用直连,多个线程操作是不安全的,要避免不安全,需要使用jedis pool间接持,更像BIO模式。

# Redis配置
# 基本配置 #
spring.redis.host=127.0.0.1
spring.redis.port=6379
# ... 其他配置

3、测试

我们在SpringBoot中去操作Redis的时候,与使用jedis时稍有不同,但是操作redis的方法仍然与Redis中的命令完全一致。

(1)、清除数据库:

// 在 sprigBoot中对于Redis中的连接对象的操作,需要先获取连接。然后再进行操作。
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();  //获取连接
connection.flushDb();         // 通过 connection来执行flushDB
connection.watch6();           // 通过 connection来执行watch()
/**
    * 指定缓存失效时间
    * @param key 键
    * @param time 时间(秒)
    */
    public boolean expire(String key, long time) {
        try{
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 根据key 获取过期时间
       * @param key 键 不能为null
       * @return 时间(秒) 返回0代表为永久有效
       */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
       * 判断key是否存在
       * @param key 键
       * @return true 存在,false 不存在
       */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 删除缓存
       * @param key 可以传一个值 或多个
       */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }

(2)、基本数据类型:

String:

 /**
       * 普通缓存获取
       * @param key 键
       * @return 值
       */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
       * 普通缓存放入
       * @param key  键
       * @param value 值
       * @return true成功 false失败
       */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 普通缓存放入并设置时间
       * @param key  键
       * @param value 值
       * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
       * @return true成功 false 失败
       */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time,
                        TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 递增
       * @param key  键
       * @param delta 要增加几(大于0)
       */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
       * 递减
       * @param key  键
       * @param delta 要减少几(小于0)
       */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

List:

  /**
       * 获取list缓存的内容
       *
       * @param key  键
       * @param start 开始
       * @param end  结束 0 到 -1代表所有值
       */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
       * 获取list缓存的长度
       *
       * @param key 键
       */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
       * 通过索引 获取list中的值
       *
       * @param key  键
       * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0
     时,-1,表尾,-2倒数第二个元素,依次类推
       */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
       * 将list放入缓存
       *
       * @param key  键
       * @param value 值
       */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 将list放入缓存
       * @param key  键
       * @param value 值
       * @param time 时间(秒)
       */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 将list放入缓存
       *
       * @param key  键
       * @param value 值
       * @return
       */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 将list放入缓存
       *
       * @param key  键
       * @param value 值
       * @param time 时间(秒)
       * @return
       */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 根据索引修改list中的某条数据
       *
       * @param key  键
       * @param index 索引
       * @param value 值
       * @return
       */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 移除N个值为value
       *
       * @param key  键
       * @param count 移除多少个
       * @param value 值
       * @return 移除的个数
       */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count,
                    value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

Set:

 /**
       * 根据key获取Set中的所有值
       * @param key 键
       */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
       * 根据value从一个set中查询,是否存在
       *
       * @param key  键
       * @param value 值
       * @return true 存在 false不存在
       */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 将数据放入set缓存
       *
       * @param key  键
       * @param values 值 可以是多个
       * @return 成功个数
       */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
       * 将set数据放入缓存
       *
       * @param key  键
       * @param time  时间(秒)
       * @param values 值 可以是多个
       * @return 成功个数
       */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
       * 获取set缓存的长度
       *
       * @param key 键
       */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
       * 移除值为value的
       *
       * @param key  键
       * @param values 值 可以是多个
       * @return 移除的个数
       */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

Hash:

    /**
       * HashGet
       * @param key 键 不能为null
       * @param item 项 不能为null
       */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
       * 获取hashKey对应的所有键值
       * @param key 键
       * @return 对应的多个键值
       */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
       * HashSet
       * @param key 键
       * @param map 对应多个键值
       */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * HashSet 并设置时间
       * @param key 键
       * @param map 对应多个键值
       * @param time 时间(秒)
       * @return true成功 false失败
       */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 向一张hash表中放入数据,如果不存在将创建
       *
       * @param key  键
       * @param item 项
       * @param value 值
       * @return true 成功 false失败
       */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 向一张hash表中放入数据,如果不存在将创建
       *
       * @param key  键
       * @param item 项
       * @param value 值
       * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
       * @return true 成功 false失败
       */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
       * 删除hash表中的值
       *
       * @param key 键 不能为null
       * @param item 项 可以使多个 不能为null
       */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
       * 判断hash表中是否有该项的值
       *
       * @param key 键 不能为null
       * @param item 项 不能为null
       * @return true 存在 false不存在
       */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
       * hash递增 如果不存在,就会创建一个 并把新增后的值返回
       *
       * @param key 键
       * @param item 项
       * @param by  要增加几(大于0)
       */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
       * hash递减
       *
       * @param key 键
       * @param item 项
       * @param by  要减少记(小于0)
       */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

七、Redis.config详解:

1、单位:

image-20220525155929411.png

# units are case insensitive so 1GB 1Gb 1gB are all the same.

配置文件units单位对大小写不敏感。

2、包含:

image-20220525160200949.png

include它可以 引入其他的配置文件。

3、网络配置:

# 绑定的ip,此时只能本地访问,若需要实现啊远程访问,可以把ip设置成一个 * 表示所有人都可以访问,或者指定一个ip。
bind 127.0.0.1 -::1     
# 保护模式
protected-mode yes        
# 默认端口设置
port 6379                 

4、通用配置(GENERAL):

# 以守护进程的方式运行,默认式no,我们需要手动设置成yes来开启。----就是可以在后台运行。
daemonize yes

# 如果以后台的方式运行,我们就需要指定一个 pid 文件!
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 /usr/local/bin/redis-log.log   # 设置日志的存放位置


# 数据库的数量,默认是16个数据库
databases 16

# 是否总是显示logo
always-show-logo no

5、快照(SNAPSHOTTING):

持久化,在规定的时间内执行了多少次操作,则会持久化到文件 .rdb,.aof

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

# 这个可以自己定义
save 900 1     # 如果再900秒内,如果至少有 1 个key进行了修改,我们就进行持久化操作。
save 300 1     # 如果再300秒内,如果至少有 1 个key进行了修改,我们就进行持久化操作。
save 60 10000  # 如果再60秒内,如果至少有 10000 个key进行了修改,我们就进行持久化操作。

stop-writes-on-bgsave-error yes     # 当持久化出错时,是否要继续工作

rdbcompression yes                  # 是否压缩 rdb 文件,这时需要消耗CPU资源。

rdbchecksum yes                     # 保存 rdb 文件的时候,进行错误检查校验!

dir ./                              # rdb 文件保存的位置

6、主从复制(REPLICATION)

见后面详解

7、安全配置(SECURITY):

在这里可以设置Redis的密码,Redis默认时没有密码的!!!

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> CONFIG get requirepass           # 查看Redis的密码
1) "requirepass"
2) ""                                            # 密码为空
127.0.0.1:6379> CONFIG set requirepass "123456"  # 设置Redis密码为 123456
OK
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                      # 使用 auth 命令和密码登录 Redis
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> 

8、客户端限制

maxclients 10000  # 设置能连接上Redis的最大客户端的数量
maxmemory <bytes> # Redis 配置最大的内存容量
maxmemory-policy noeviction # 内存到达上限之后的处理策略

noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。 大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )。
allkeys-lru: 所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
allkeys-random: 所有key通用; 随机删除一部分 key。
volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。
volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。

9、Append only模式 AOF配置

appendonly no    # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分情况下,rdb完全够用。

appendfilename "appendonly.aof"  # 

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

八、Redis持久化:

为什么要持久化

Redis是一个基于内存的数据库,如果我们的redis服务器突然宕机,那么redis中的数据就会全部丢失。
通常我们恢复数据就需要通过后端去从数据库里面恢复,但是数据库的性能不足,当数据少的时候还好,
一旦要恢复的数据过多那么就会有问题:

1、数据恢复起来会给数据库带来很大的压力。

2、数据库的性能不如Redis。

从而导致数据恢复的的过程会非常的缓慢,且耗费资源。所以我们就需要将Redis中的数据持久化,来防止从数据库里面恢复数据。

Redis持久化的两种方式

RDB(Redis DataBase):

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

Redis会单独创建一个fork子进程来进行持久化,先将数据写入一个临时文件,持久化结束后,用这个临时文件替代上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。

如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是特别敏感,那么RDB方式比AOF更加高效能。RDB的缺点就是最后一次持久化的数据可能会丢失。

在主从复制中,rdb就是备用的,放在从机上面

图片.png

有时候在生产环境 我们会将dump.rdb进行备份

在rdb保存的文件时dump.rdb 都是在我们的配置文件中的快照模块进行配置的

image-20220527151829490.png

触发机制

1:save(save 900 1)的规则满足的情况下

2:执行flushall命令

3:退出redis

会 生成一个dump.rdb文件

如何恢复rdb文件

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

2:查看rdb存放的位置 1.png

优点

  1. 使用大场景的数据恢复
  2. 对数据的完整性不高

缺点

1:需要一定的时间间隔进行操作,如果redis意外宕机,那么最后一次修改数据就没有了

2:fork进程的时候,会占用一定的内存空间

AOF(Append Only File):

将我们所有的命令都记录下来,恢复的时候 就把这个文件全部再执行一遍

图片.png

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

AOF保存的是appendonly.aof文件

image-20220530164150282.png

默认是不开启的 需要手动开启(appendonly yes),重启,即可生效

如果aof文件有错位的话,这时候redis是启动不起来的。

redis提供了一个工具 redis-check-aof --fix (使用这个命令即可修复aof文件)

如果文件正常,重启即可恢复

优点:

  1. 每一次修改都会同步。
  2. 每秒同步一次,可能会丢失一秒的数据
  3. 从不同步,效率最高。

缺点:

  1. 相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
  2. aof运行效率也要比rdb慢。所以redis默认配置就是rdb持久化

重写规则说明(了解即可)

aof默认就是文件的无线追加,文件会越来越大

如果文件大于64m,就会fork一个新的进程来将我们的文件进行重写。

九、Redis发布订阅:

要求更多的话可以使用消息队列(Kafka等MQ)

Redis发布订阅(pub/sub)是一种消息通信模式:发布者发送消息,订阅者接收消息。(如微博、微信、关注系统等)

Redis客户端可以订阅任意数量的频道

订阅/发布消息图

消息发送者、消息订阅者、频道

image-20220530170324550.png

# 基础命令

SUBSCRIBE channel1 channel2		#订阅频道
PUBLISH channel message			#发布信息
UNSUBSCRIBE channel1 channel2	#退订频道
PSUBSCRIBE pattern1 pattern2	#订阅一个或多个符合给定模式的频道
PUBSUB suncommand				#查看订阅与发布系统的状态
PUNSUBSCRIBE pattern1 pattern2	#退订所给模式的频道

# 这些命令被广泛用于构建即时通信应用,比如网络聊天室、实时广播和实时通信等

订阅:

image-20220530172641022.png

后台发布消息:

image-20220530172753194.png

客户端接收消息:

image-20220530172837754.png

原理

Redis是使用C实现的,通过分析Redis源码里面的pubsub.c文件,即可了解发布和订阅机制的底层实现。

Redis是通过PUBLISH、SUBSCRIBE、PSUBCRIBE等命令实现发布和订阅功能

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

用过PUBLISH命令向订阅者发布消息,redis-server会使用给定的频道作为键,在它所维护的频道子弟班中查找订阅了这个频道的所有客户端链表,遍历这个链表,将消息发布给所有订阅者。

使用场景:

  • 实时消息系统
  • 实时聊天(频道当做聊天室,将信息回显给所有人即可)
  • 订阅、关注系统

稍微复杂的场景,就会使用消息中间件MQ。

十、主从复制:

主从复制,读写分离!80%的情况都是在进行读操作。减缓服务器的压力,架构中经常使用,最少都需要一主二从!

1、概念:

主从复制,就是指一台redis服务器上的数据复制到其他的redis服务器上。前者称为主节点(master/leader),后者称为从节点(slave/follower)。数据的复制是单向的,只能由主节点到从节点。主节点写,从节点读。

默认情况下,每台redis服务器都是主节点,且一个主节点可以有多个从节点或没有从节点,但一个从节点只能有一个主节点。

2、作用:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
  • 故障恢复:主节点出现问题后,可以由从节点提供服务,实现快速的故障恢复。实际上是一种服务冗余
  • 负载均衡:主从复制中,配合读写分离,可以由主节点提供写服务,从节点提供读服务,分担服务器负载,可以大大提高redis服务器的并发量
  • 高可用(集群)基石:主从复制还是哨兵和集群能够实施的基础

一般来说,将Redis运用于工程项目,只使用一台redis是万万不能的(可能会宕机),原因如下:

  • 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
  • 从容量上,单个redis服务器容量有限,一般来说,单台redis的最大使用内存不应该超过20G

image-20220530174610565.png

3、环境配置:

只需要配置从库,不需要配置主库。

127.0.0.1:6379> info replication  # 查看当前库的信息
# Replication
role:master     # 角色
connected_slaves:0    # 从机数量 为 0 
master_failover_state:no-failover  # 无故障转移
master_replid:c5a3e5456dd3d67af6951bd287561cdee02c56c7
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

配置:

配置三个从机(6380,6381,6382),一个主机(6379)。

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

1、将redis.conf复制三份并命名。

image-20220530180028382.png

2、更改redis的配置文件信息:

# 1 、更改端口号
port 6380

# 2、更改pidfile 为对应的端口号
pidfile /var/run/redis_6380.pid

# 3、更改日志路径
logfile /usr/local/bin/redis-6380-log.log

# 4、更改dump文件名
dbfilename dump6380.rdb

3、启动Redis服务

image-20220531120450676.png

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

image-20220531120725688.png

image-20220531120708238.png

4、建立主从关系:

我们建立主从关系时,只需要在从机配置即可;由从机来认定一个主机。(这里使用命令建立的关系为 暂时的

#               SLAVEOF  `主机地址` `主机redis服务的端口号`
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379       # 设置当前redis服务的主机为 ip:127.0.0.1 port:6379 的redis

查看主机(6379)信息:

127.0.0.1:6379> info replication              # 查看主机信息
# Replication
role:master                                   # 角色为 master (主机)
connected_slaves:1                            # 拥有一个从机

# 子节点的ip为:127.0.0.1,端口为:6380,状态为:在线  。。。
slave0:ip=127.0.0.1,port=6380,state=online,offset=3094,lag=0  
master_failover_state:no-failover
master_replid:39a1f2c619f85c0d50fd9c74b08573bd4cef25af
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:3094
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:3094

查看从机(6380)的信息:

127.0.0.1:6380> info replication         # 查看主机信息
# Replication
role:slave                               # 角色为 slave (从机)
master_host:127.0.0.1                    # 主机 ip 为 : 127.0.0.1
master_port:6379                         # 主机 端口 为 : 6379
master_link_status:up                    # 主机 状态 为 :在线
master_last_io_seconds_ago:1             #  . . .
master_sync_in_progress:0
slave_read_repl_offset:3164
slave_repl_offset:3164
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0                       
master_failover_state:no-failover
master_replid:39a1f2c619f85c0d50fd9c74b08573bd4cef25af
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:3164
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:3150

这里我们使用命令建立的主从关系是暂时的,而不是永久的;如果我们需要建立永久的主从关系,就需要在Redis的配置文件中进行配置。

5、在Redis配置文件中配置

在Redis的配置文件中,找到 REPLICATION 的配置,然后将主机的 IP 和 端口号 配置再保存,之后这一个redis服务一启动就是 从机。

image-20220531131137943.png

例:

image-20220531131659678.png

6、主从复制中的一些细节

  • 此时主机可以既可以写入数据也可以读取数据,而从机只能读取数据不能写入数据;
  • 主机中的所有信息和数据,都会自动被从机保存。
        ############################   主机写入数据   ############################
127.0.0.1:6379> flushdb       # 先清空数据
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 v1     # 写入一条数据(此时该数据会自动被保存到它的 所有从机中去!!!)
OK
127.0.0.1:6379> get k1        # 读取数据
"v1"
127.0.0.1:6379> 

        ############################   从机可直接读取数据   ############################
127.0.0.1:6380> get k1        # 从机可直接读取,主机写入的数据。
"v1"
127.0.0.1:6380> set k2 v2     # 从机去尝试写入数据
(error) READONLY You can't write against a read only replica. # 报错,说明只能由主机写入数据,而从机不能写入
127.0.0.1:6380> 

        ############################   从机可直接读取数据   ############################
127.0.0.1:6381> get k1        # 从机可直接读取,主机写入的数据。
"v1"
127.0.0.1:6381> 

        ############################   从机可直接读取数据   ############################
127.0.0.1:6382> get k1        # 从机可直接读取,主机写入的数据。
"v1"
127.0.0.1:6382> 
  • 当主机断开连接后(宕机后),从机依然是连接到主机且能够正常工作的,只是不能有新的数据写入了。
127.0.0.1:6379> SHUTDOWN  # 关闭 主机连接
not connected> quit

image-20220531133453284.png

127.0.0.1:6380> info replication 
# Replication 
role:slave                  # 此时仍然为 从机
master_host:127.0.0.1
master_port:6379
master_link_status:down     # 主机已经断开连接
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_read_repl_offset:6870
slave_repl_offset:6870
master_link_down_since_seconds:4
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:9c85228c425c7c39205c95d80e55e61761823998
master_replid2:39a1f2c619f85c0d50fd9c74b08573bd4cef25af
master_repl_offset:6870
second_repl_offset:6815
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:15
repl_backlog_histlen:6856
127.0.0.1:6380> get k1     # 此时 从机 仍然能够 获取到值
"v1"

image-20220531133907289.png

  • 当主机再次连接后,仍然可以像之前一样正常工作。
127.0.0.1:6379> set k2 v2    # 主机重新连接后,写入数据
OK
127.0.0.1:6379> 


127.0.0.1:6380> get k2       # 主机写入数据后,从机可以直接获取数据
"v2"
127.0.0.1:6380> 

image-20220531134421003.png

  • 如果是使用命令建立的主从关系的话,当从机宕机后再次连接回来;原来作为从机的这一个redis服务,将会默认变为一个没有从机的主机。这也就体现了通过命令建立的主从关系是暂时的,以及每一个Redis服务启动后都默认为主机的特点。

image-20220531135328437.png

重启 6380 端口的Redis服务后:

image-20220531135757335.png

如果这时重新建立上主从关系,则可以马上从主机中获取到所有数据。

image-20220531140249406.png

可直接获取主机中的所有数据:

image-20220531140455800.png

7、复制原理:

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

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

全量复制:而Slave服务在接收到数据库文件数据后,将其盘并加载到内存中。(就是将主机中的数据全部复制到从机中)

增量复制:Master继续将新的所有搜集到的修改命令一次传给slave,完全同步。(就是主机写入新的数据后,从机立马将从机写入的数据依次复制到从机中。)

但是只要是Slave重新连接Master,一次完全同步(全量复制)将自动执行。表示,我们主机中的数据一定可以复制到从机中。

补充:

sync命令 用于强制被改变的内容立刻写入磁盘,更新超块信息。

在Linux/Unix系统中,在文件或数据处理过程中一般先放到内存缓冲区中,等到适当的时候再写入磁盘,以提高系统的运行效率。sync命令则可用来强制将内存缓冲区中的数据立即写入磁盘中。用户通常不需执行sync命令,系统会自动执行update或bdflush操作,将缓冲区的数据写 入磁盘。只有在update或bdflush无法执行或用户需要非正常关机时,才需手动执行sync命令。

8、链式主从复制

image-20220531145136336.png

此时依旧是可以实现,主从复制的操作的。

但是如果在这个过程中我们的主机宕机了,怎么才能选出一个新的 主机呢?

在这种主从复制模式下,如果主机宕机了,我们可以使用SLAVEOF NO ONE来让自己成为一个新的主机。其他的节点就可以手动连接到最新的这个节点(手动操作!),但如果在这个时候原来的主机修复了,那就需要再次手动的重新去连接。

SLAVEOF NO ONE   # 设置当前节点,没有主节点,则自己上位成为主节点。

十一、哨兵模式

1、概述:

就像前面的"链式主从复制"模式中,如果主机宕机了;那么就需要手动的来选出一个新的主机。这会使得这个过程更加费时费力。还会导致在修复的过程中,服务不可使用。所以这是一种非常没有效率的方法,这时我们就需要一个更加科学,高效的方法来完成这个过程。所以就出现了 Sentinel (哨兵)架构来解决这个问题。

2、任务:

2.1、监控(Monitoring)

哨兵会不断地检查主节点和从节点是否运作正常。

2.2、通知(Notification):

当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。

2.3、自动故障转移(Automatic failover):

当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为服从新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

就是将重新选举新的主服务器的过程由手动改为自动了。

2.4、配置提供者(Configuration provider):

客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

3、流程图:

image-20220531155346362.png

流程解读:

建立一个哨兵(Sentinel)集群,来一起监控所有的服务器;同时哨兵之间也相互监控防止某一个哨兵失去连接。其中每一个哨兵都可以监控所有服务器。如果,主服务器宕机了,此时只是被哨兵1检测到了异常,系统并不会立刻进行自动故障转移操作,此刻仅仅只是哨兵1主观的认为主服务器不可用,这种只由哨兵集群中某一个哨兵认定主服务器不可使用的现象称为主观下线。当其他哨兵也检测到主服务器不可用,并且检测到这个异常的哨兵到达一定数量时,那么哨兵之间将会进行一次投票的过程(哨兵会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。)来判断此时的主服务器是否真的不可用了,如果赞成票数是大于等于哨兵配置文件中的 quorum 配置项, 则可以判定此时的主服务器确实不能用了,这种由哨兵集群共同决定主服务器是否下线的现象称为客观下线。之后再进行自动故障转移(Automatic failover) ,选举出新的主服务器。

4、测试:

目前测试的环境是一主(6379)二从(6380,6381)!

4.1、配置哨兵配置文件(Sentinel.conf)

# Sentinel monitor `被管理的主机名称` host port 1   
sentinel monitor mymaster 127.0.0.1 6379 2

注意:

  • 这里的主机名称自定义,只是用来表示当前主机的名称。
  • 这个数字 1 表示主机宕机后,哨兵投票结果中认定主机宕机的哨兵的个数。(至少一个哨兵认为主机宕机,才进行自动故障转移(Automatic failover) 操作!!!

4.2、启动哨兵

redis-sentinel xconfig/sentinel.conf

image-20220601112945542.png

启动成功。

4.3、让6379宕机

image-20220601115340578.png

哨兵开始工作:

image-20220601115820990.png

6380成功当选 新的主机:

image-20220601115947879.png

6379重新连接后,只能沦为从机:

image-20220601120216007.png

5、哨兵模式的优缺点:

优点:

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

缺点:

  1. Redis不好在线扩容。一旦集群容量到达上限,在线扩容就非常麻烦。
  2. 实现哨兵模式的配置其实是很麻烦的,里面有很多选择。

6、哨兵模式的全部配置

# Example sentinel.conf
# 配置哨兵sentinel实例运行的端口 默认26379
port 26379

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

# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# 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无法正常启动成功。

#通知脚本
# shell编程
# 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的缓存穿透和雪崩

缓存穿透(大量请求根本不存在的key)

概念

用户查询一个数据,发现redis数据库没有,也就是缓存没有命中,于是向持久层数据库查询,发现也没有,于是此次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求数据库,这会给持久层数据库造成很大压力,也就相当于出现了缓存穿透。

解决方案

1.布隆过滤器

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

2.对空值进行缓存

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

这种方法会存在两个问题:

  1. 存储空值会消耗大量的空间
  2. 即使对控制设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间的窗口不一致,这对需要保存一直性的业务会有影响

3.设置白名单

缓存击穿(redis中一个热点key过期(大量用户访问该热点key,但是热点key过期))

概念

缓存击穿是指一个key非常热点(如微博热搜),在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿透缓存,直接请求数据库。这就像在一个品章上凿开了一个洞。

解决方案

1.设置热点数据永不过期

从缓存层面来说,没有设置过期时间,就不会出现热点key过期后产生的问题

2.加互斥锁

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

缓存雪崩(redis中大量key集体过期)

概念

在某个时间段,缓存集中过期失效(redis宕机也会导致)。

其实缓存集中过期不是非常致命,而是缓存服务器某个节点宕机或者断网才是致命的,很有可能瞬间就把数据库压垮。 (和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。)

解决方案

1.redis高可用

多设几台redis,即使一台挂掉之后其他的也可以继续工作。其实就是搭建集群(异地多活)

2.限流降级(使用锁机制)

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

3.数据预热

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

运行中出现的问题:

# 问题 1:
Error moving temp DB file temp-3758.rdb on the final destination dump.rdb (in server root dir /usr/local/bin): Permission denied

====
报错提示:在最终目标 dump.rdb(在服务器根目录 /usr/local/bin 中)移动临时 DB 文件 temp-3758.rdb 时出错:权限被拒绝

ps -u my_account -o pid,rss,command | grep redis  #查看redis进程的pid  my_account是你的用户名

kill -9 the_pid #杀死 指定pid的进程  the_pid是我们第一步查到的redis的进程号

原因:在根目录下面也有一个dump.rdb文件(导致这个问题原因,具体我也不清楚),而我们在redis-conf文件中dir的默认配置是 dir ./表示启动server时候的当前目录,也就是说之前测试线启动redis服务是在 / 目录下启动的。

20220519134717.png

这里可以看到在根路径下和我们启动服务的路径下都有一个 dump.rdb文件。

解决:

方法一:将redis-conf文件中的dir配置由dir ./改成 dir /后重新启动redis服务。

20220519135322.png 方法二:直接删掉根目录下的哪一个 dump.rdb

20220519135524.png