Redis详细教程-学习笔记

Redis

概述

Redis是什么?

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

Redis详细教程-学习笔记_第1张图片

Redis详细教程-学习笔记_第2张图片

Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。免费和开源!是当下最热门的NoSQL技术之一!也被人们称之为结构化数据库!

Redis能干嘛?

1、内存存储、持久化,内存中史断电即失,所以持久化很重要(RDB、AOF)

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

3、发布订阅系统

4、地图信息分析

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

6、……

特性

1、多样的数据类型

2、持久化

3、集群

4、事务

……

Windows下载Redis

1、下载

地址:https://github.com/tporadowski/redis/releases

Redis详细教程-学习笔记_第3张图片

2、解压

Redis详细教程-学习笔记_第4张图片

3、启动

双击 redis-server.exe

Redis详细教程-学习笔记_第5张图片

4、连接

不要关闭启动程序,双击打开 redis-cli.exe,输入ping 命令,如下图所示就算连接成功

Redis详细教程-学习笔记_第6张图片

5、测试使用

设值、取值,如图:

Redis详细教程-学习笔记_第7张图片

Liunx中下载安装

image-20210805105230713

1、下载

下载地址:https://redis.io/

Redis详细教程-学习笔记_第8张图片

Redis详细教程-学习笔记_第9张图片

2、上传

把 Linux 的 Redis 压缩包上传到 Liunx 服务器之中

Redis详细教程-学习笔记_第10张图片

3、解压

Redis详细教程-学习笔记_第11张图片

tar zxvf 需要解压的文件

Redis详细教程-学习笔记_第12张图片

4、安装环境

Redis详细教程-学习笔记_第13张图片

yum install gcc-c++

查看版本

Redis详细教程-学习笔记_第14张图片

5、编译

进入 redis-6.2.5 文件中,进行编译

# 编译
make

image-20210807205538292

成功

Redis详细教程-学习笔记_第15张图片

redis默认安装路径 /usr/local/bin

Redis详细教程-学习笔记_第16张图片

6、移动配置文件

Redis详细教程-学习笔记_第17张图片

# 在bin目录下创建一个文件
mkdir config
# 复制 redis.conf 文件到创建的目录下
cp 自己redis.conf配置文件的路径 config
7、修改配置文件
vim redis.conf

Redis详细教程-学习笔记_第18张图片

8、启动
redis-server config/redis.conf

image-20210807211636546

9、测试连接
#启动客户端
redis-cli -p 6379
#测试连接
ping
#设置键值
set name world
#获取值
get name
#查看所有key
keys *

Redis详细教程-学习笔记_第19张图片

10、查看进程
ps -ef|grep redis

Redis详细教程-学习笔记_第20张图片

11、关闭

image-20210807212620587

# 关闭redis
shutdown
# 退出
exit

再次查看进程

image-20210807212915521

性能测试

redis-benchmark 是一个压力测试工具,官方自带的性能测试工具,命令参数如下图(来自菜鸟教程):

Redis详细教程-学习笔记_第21张图片

redis-benchmark -h localhost -p 6379 -c 100 -n 100000

Redis详细教程-学习笔记_第22张图片

Redis详细教程-学习笔记_第23张图片

基础知识

Redis 命令不区分大小写

Redis默认有16个数据库

Redis详细教程-学习笔记_第24张图片

默认使用的是第0个数据库,可以使用 select 进行切换

Redis详细教程-学习笔记_第25张图片

# 切换数据库
select 3
# 查看DB大小 
dbsize

Redis详细教程-学习笔记_第26张图片

# 查看所有key
keys *
#清楚当前数据库
flushdb

Redis详细教程-学习笔记_第27张图片

# 清除所有数据库
flushall

Redis详细教程-学习笔记_第28张图片

Redis 是单线程的

官方表示,Redis 是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就是用单线程了!

Redis 是C语言写的,官方提供的数据为 100000+ 的QPS,完全不比同样是使用键值对的 Memecache 差!

Redis 为什么单线程还这么快

1、误区1:高性能的服务器一定是多线程的?

2、误区2:多线程(CPU上下文会切换)一定比单线程效率高?

对CPU>内存>硬盘的速度要有所了解

核心:Redis 是将所有数据全部都放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案。

Redis-Key

# 判断key是否存在
EXISTS name
# 移除key
move name 1
# 设置超时 单位是秒/s
expire name 10
# 查看当前key剩余时间
ttl name
# 查看当前key的数据类型
type name

Redis详细教程-学习笔记_第29张图片

Redis详细教程-学习笔记_第30张图片

Redis详细教程-学习笔记_第31张图片

Redis详细教程-学习笔记_第32张图片

其他的 Redis 命令可以去官网了解学习

Redis详细教程-学习笔记_第33张图片

基本数据类型

String(字符串)

1、追加
# 追加字符串,如果当前key不存在,就相当于set key
append key1 "hello"
# 获取字符串长度
strlen key1

Redis详细教程-学习笔记_第34张图片

2、步长自增
# 自增1
incr views
# 自减1
decr views
# 设置步长,指定增量
incrby views 10
# 设置步长,指定减量
decrby views 10

Redis详细教程-学习笔记_第35张图片

3、截取
# 截取字符串
getrange key1 0 4
# 获取全部的字符串
getrange key1 0 -1

Redis详细教程-学习笔记_第36张图片

4、替换
# 替换指定位置开始的字符串
setrange name 1 xx

Redis详细教程-学习笔记_第37张图片

5、超时
# 设置一个key,30秒后过期
setex key2 30 "hello"
# 如果当前key不存在,就创建该key,如果该key存在,就创建失败
setnx mykey "asas"

Redis详细教程-学习笔记_第38张图片

6、批量
# 同时设置多个值
mset k1 v1 k2 v2 k3 v3
# 同时获取多个值
mget k1 k2 k3
# 原子性操作,要么一起成功,要么一起失败
msetnx k1 v1 k4 v4

Redis详细教程-学习笔记_第39张图片

7、对象
# 设置对象
mset user:1:name zhangsan user:1:age 18
# 获取对象值
mget user:1:name user:1:age

Redis详细教程-学习笔记_第40张图片

8、getset
# 如果不存在,则返回 nil
getset db redis
# 如果存在,则获取原来的值,并设置新的值
getset db mongidb

Redis详细教程-学习笔记_第41张图片

数据结构是相同的!

String类似的使用场景:value除了是字符串还可以是数字,用途举例:

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象存储缓存

List(列表)

Redis 命令不区分大小写

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

所有 list 命令都是用 l 开头的

1、添加
# 将一个或多个值,插入列表头部
lpush list one
lpush list two
lpush list three
# 获取list中全部值
lrange list 0 -1
# 通过区间获取绝体的值
lrange list 0 1
# 将一个或多个值,插入列表尾部
rpush list rigth

Redis详细教程-学习笔记_第42张图片

2、移除
# 移除list的第一个元素
lpop list
# 移除list的最后一个元素
rpop list
# 移除list集合中指定个数的值
lrem list 1 aa
lrem list 2 bb
# 移除列表最后一个元素,将它移动到新的列表中
rpoplpush mylist myotherlist

Redis详细教程-学习笔记_第43张图片

Redis详细教程-学习笔记_第44张图片

Redis详细教程-学习笔记_第45张图片

3、下标
# 通过下标获取值
lindex list 0

Redis详细教程-学习笔记_第46张图片

4、长度
# 获得列表的长度
llen list

Redis详细教程-学习笔记_第47张图片

5、截断
# 通过下标截取指定的长度,截取之后,只剩下被截取的元素
ltrim mylist 1 1(或2)

Redis详细教程-学习笔记_第48张图片

6、更新
# 更新指定下标的值
lset list 0 item

Redis详细教程-学习笔记_第49张图片

7、插入
# 从 "world" 的前面插入 "other"
linsert mylist before "world" "other"
# 从 "hello" 的前面插入 "new"
linsert mylist after "hello" "new"

Redis详细教程-学习笔记_第50张图片

小结

  • 它实际上是一个链表,before Node after,left,right都可以插入值
  • 如果key不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有值,空链表,也代表不存在
  • 在两边插入或者改动值,效率最高,中间元素,相对来说效率会低一点

应用:

消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop)

Set(集合)

Redis的Set是string类型无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据

1、添加
# 在set集合中添加元素
sadd myset "hello"
# 查看指定set的所有值
smembers myset
# 判断某一个值是不是在set集合中,存在返回1,不存在则返回0
sismember myset hello

Redis详细教程-学习笔记_第51张图片

2、获取
# 获取set集合中的元素个数
scard myset

image-20210810094249329

3、移除
# 移除set集合中的指定元素
srem myset hello2
# 随机删除一些set集合中的元素
spop myset

Redis详细教程-学习笔记_第52张图片

Redis详细教程-学习笔记_第53张图片

4、抽选
# 随机从set集合中抽选出一个元素
SRANDMEMBER myset
# 随机从set集合中抽选出指定个数的元素
SRANDMEMBER myset 3

Redis详细教程-学习笔记_第54张图片

6、移动
# 将一个指定的值,移动到另外一个set集合
smove myset myset2 "sakura"

Redis详细教程-学习笔记_第55张图片

7、数字集合类
  • 差集
  • 交集
  • 并集
# 差集
sdiff key1 key2
# 交集
sinter key1 key2
# 并集
sunion key1 key2

Redis详细教程-学习笔记_第56张图片

Hash(散列表)

Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象,尤其是用户信息之类的,经常变动的信息

1、创建获取
#set 一个具体的 key-value
hset myhash key1 "yunduan"
# 获取一个字段值
hget myhash key1
# set 多个 key-value
hmset myhash key1 "hello" key2 "world"
# 获取多个字段值
 hmget myhash key1 key2
# 获取全部的数据,以 键值对 形式展示
hgetall myhash
# 获取hash表的字段数量
hlen myhash
# 只获得所有 key
hkeys myhash
#只获得所有value
hvals myhash

Redis详细教程-学习笔记_第57张图片

Redis详细教程-学习笔记_第58张图片

Redis详细教程-学习笔记_第59张图片

2、移除
# 删除指定的key指端,对应的value也会消失
hdel myhash key1

image-20210810202503885

3、判断
# 判断指定字段是否存在
hexists myhash key1
# 如果存在则可以设置,不存在则不能设置
HSETNX myhash key5 hello

image-20210810203329619

image-20210810204406603

4、增减
# 增加指定大小
hincrby myhash key4 1

Redis详细教程-学习笔记_第60张图片

Zset(有序集合)

1、创建获取
# 创建一个或多个值
zadd myset 1 "one" 2 "two" 3 "three"
# 获取所有值
zrange myset 0 -1
# 获取集合中的个数
zcard myset
# 获取指定区间的成员的数量

Redis详细教程-学习笔记_第61张图片

image-20210810211251411

Redis详细教程-学习笔记_第62张图片

2、排序
# 显示全部的用户,按成绩从小到大排列
ZRANGEBYSCORE salary -inf +inf
# 显示所有的用户,并附带成绩
ZRANGEBYSCORE salary -inf +inf withscore
# 显示全部的用户,按成绩从大到小排列
ZREVRANGE salary 0 -1 withscores

Redis详细教程-学习笔记_第63张图片

image-20210810211658264

3、移除
# 移除有序集合中指定元素
zrem salary "zhumengshi"

Redis详细教程-学习笔记_第64张图片

应用案例:

  • set排序 存储班级成绩表 工资表排序!
  • 普通消息,1.重要消息 2.带权重进行判断
  • 排行榜应用实现,取Top N测试

三种特殊类型

1、Geospatial(地理位置)

中文文档:http://www.redis.cn/

Redis详细教程-学习笔记_第65张图片

image-20210811101942121

开发手册文档:https://cloud.tencent.com/developer/section/1374018

经纬度查询网址:http://www.toolzl.com/tools/gps.html

geoadd

规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入

有效的经度从-180度到180度。

有效的纬度从-85.05112878度到85.05112878度。

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

# geoadd 添加地理位置
geoadd china:city 116.35 40.40 beijing 121.46 31.19 shanghai 120.21 30.25 hangzhou 112.94 28.23 changsha 114.06 22.54 shenzhen

Redis详细教程-学习笔记_第66张图片

getpos

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

获得当前定位:一定是一个坐标值

# 获取指定城市的经度和纬度
GEOPOS china:city shenzhen hangzhou shanghai

Redis详细教程-学习笔记_第67张图片

geodist

返回两个给定位置之间的距离。

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

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

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。
# 查看两地之间的距离
GEODIST china:city shenzhen hangzhou km

Redis详细教程-学习笔记_第68张图片

georadius

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

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

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

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

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

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

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

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

我附近的人(获得附近的人的地址,定位):通过半径来查询

# 以 110 30 为中心,寻找方圆1000km内的城市
georadius china:city 110 30 1000 km
# # 以 110 30 为中心,寻找方圆1000km内的城市 并展示与中心点的距离以及所在经纬度,指定展示个数
GEORADIUS china:city 110 30 1000 km withdist withcoord count 1

Redis详细教程-学习笔记_第69张图片

georadiusbymember

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

指定成员的位置被用作查询的中心。

# 找出位于指定元素周围的其他元素
GEORADIUSBYMEMBER china:city changsha 1000 km withdist withcoord count 3

Redis详细教程-学习笔记_第70张图片

geohash

返回一个或多个位置元素的 Geohash 表示。

通常使用表示位置的元素使用不同的技术,使用Geohash位置52点整数编码。由于编码和解码过程中所使用的初始最小和最大坐标不同,编码的编码也不同于标准。此命令返回一个标准的Geohash,在维基百科和geohash.org网站都有相关描述

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

# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么距离则更近
GEOHASH china:city hangzhou shenzhen

image-20210811120353823

GEO 底层的实现原理其实就是Zset,完美可以使用Zset命令来操作geo

Redis详细教程-学习笔记_第71张图片

2、Hyperloglog(基数统计)

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。

因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

其底层使用string数据类型

什么是基数?

数据集中不重复的元素的个数。

应用场景:

网页的访问量(UV):一个用户多次访问,也只能算作一个人。

传统实现,存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog就能帮助我们利用最小的空间完成。

测试
# 添加一组元素
PFADD mykey1 a b c d e f j i j
# 统计 mykey 中的基数数量
PFCOUNT mykey1
# 合并两组 mykey (并集),重复的只算一个
PFMERGE mykey3 mykey1 mykey2

Redis详细教程-学习笔记_第72张图片

如果允许容错,那么可以使用Hyperloglog

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

3、BitMaps(位图)

使用位存储,信息状态只有 0 和 1

Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。

应用场景

签到统计、状态统计

使用 bitmap 来记录周一到周日的打卡记录

1 :打卡,0:未打卡

setbit sign 0 1

Redis详细教程-学习笔记_第73张图片

查看某一天是否有打卡

getbit sign 2

image-20210811141939048

统计打卡的天数

bitcount sign

image-20210811142129623

事务

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

Redis事务本质:一组命令的集合。

----------------- 队列 set set set 执行 -------------------

事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。

一次性
顺序性
排他性
Redis事务没有隔离级别的概念

Redis事务操作过程

  • 开启事务(multi
  • 命令入队
  • 执行事务(exec

所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成

正常执行事务

# 开启事务
multi
# 执行事务
exec

Redis详细教程-学习笔记_第74张图片

放弃事务

# 取消事务
discard

Redis详细教程-学习笔记_第75张图片

编译型异常

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

Redis详细教程-学习笔记_第76张图片

运行时异常

如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的 —> 所以不保证事务原子性,错误命令抛出异常

Redis详细教程-学习笔记_第77张图片

监控(Watch)

悲观锁:
  • 很悲观,认为什么时候都会出现问题,无论作什么都会加锁
乐观锁:
  • 很乐观,认为什么时候都不会出现问题,所以不会上锁,更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 获取version
  • 更新的时候比较version
Redis监视测试
# 监视 money 对象
watch money

事务正常结束,数据期间没有发生变动,这个时候就正常执行成功

Redis详细教程-学习笔记_第78张图片

测试多线程修改值,使用watch 可也当作Redis的乐观锁操作

Redis详细教程-学习笔记_第79张图片

开启一个新窗口,连接新的客户端对money进行修改

Redis详细教程-学习笔记_第80张图片

执行之前,另外一个线程修改了我们的money值,就会导致事务执行失败

Redis详细教程-学习笔记_第81张图片

解决方案:如果修改失败,获取最新的值就好

1、如果发现事务执行失败,就先解锁

2、获取最新的值,再次监视

3、对比间的的值是否发生了变化,如果没有变化,那么可以执行成功,如果变了则执行失败

Redis详细教程-学习笔记_第82张图片

Jedis

jedis:是Redis官方推荐的java连接开发工具,使用java操作Redis中间件,如果你要使用java操作Redis,那么一定垚对jedis十分熟悉

测试

1、导入依赖

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

Redis详细教程-学习笔记_第83张图片

2、编码测试
  • 连接数据库
  • 操作命令
  • 断开连接

开启Redis服务

Redis详细教程-学习笔记_第84张图片

public class Test {

    public static void main(String[] args) {
        /**
         * 1、new Jedis 对象
         */
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //jedis 所有的命令就是我们之前用的数据类型指令,所以它很重要
        
        System.out.println(jedis.ping());
    }
}

Redis详细教程-学习笔记_第85张图片

测试事务
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

/**
 * @author World
 * @since 2021/8/12 17:41
 */
public class TestMulti {
    public static void main(String[] args) {
        //创建客户端连接服务端,redis服务端需要被开启
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello", "world");
        jsonObject.put("name", "java");
        //开启事务
        Transaction multi = jedis.multi();
        String result = jsonObject.toJSONString();
        try{
            //向redis存入一条数据
            multi.set("json", result);
            //再存入一条数据
            multi.set("json2", result);
            //这里引发了异常,用0作为被除数
            //int i = 100/0;
            //如果没有引发异常,执行进入队列的命令
            multi.exec();
        }catch(Exception e){
            e.printStackTrace();
            //如果出现异常,回滚
            multi.discard();
        }finally{
            System.out.println(jedis.get("json"));
            System.out.println(jedis.get("json2"));
            //最终关闭客户端
            jedis.close();
        }
    }
}

Redis详细教程-学习笔记_第86张图片

String
import redis.clients.jedis.Jedis;

import java.util.concurrent.TimeUnit;

public class TestString {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);

        jedis.flushDB();
        System.out.println("===========增加数据===========");
        System.out.println(jedis.set("key1","value1"));
        System.out.println(jedis.set("key2","value2"));
        System.out.println(jedis.set("key3", "value3"));
        System.out.println("删除键key2:"+jedis.del("key2"));
        System.out.println("获取键key2:"+jedis.get("key2"));
        System.out.println("修改key1:"+jedis.set("key1", "value1Changed"));
        System.out.println("获取key1的值:"+jedis.get("key1"));
        System.out.println("在key3后面加入值:"+jedis.append("key3", "End"));
        System.out.println("key3的值:"+jedis.get("key3"));
        System.out.println("增加多个键值对:"+jedis.mset("key01","value01","key02","value02","key03","value03"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03","key04"));
        System.out.println("删除多个键值对:"+jedis.del("key01","key02"));
        System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));

        jedis.flushDB();
        System.out.println("===========新增键值对防止覆盖原先值==============");
        System.out.println(jedis.setnx("key1", "value1"));
        System.out.println(jedis.setnx("key2", "value2"));
        System.out.println(jedis.setnx("key2", "value2-new"));
        System.out.println(jedis.get("key1"));
        System.out.println(jedis.get("key2"));

        System.out.println("===========新增键值对并设置有效时间=============");
        System.out.println(jedis.setex("key3", 2, "value3"));
        System.out.println(jedis.get("key3"));
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(jedis.get("key3"));

        System.out.println("===========获取原值,更新为新值==========");
        System.out.println(jedis.getSet("key2", "key2GetSet"));
        System.out.println(jedis.get("key2"));

        System.out.println("获得key2的值的字串:"+jedis.getrange("key2", 2, 4));
    }
}

Redis详细教程-学习笔记_第87张图片

Set
import redis.clients.jedis.Jedis;

public class TestSet {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("============向集合中添加元素(不重复)============");
        System.out.println(jedis.sadd("eleSet", "e1","e2","e4","e3","e0","e8","e7","e5"));
        System.out.println(jedis.sadd("eleSet", "e6"));
        System.out.println(jedis.sadd("eleSet", "e6"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("删除一个元素e0:"+jedis.srem("eleSet", "e0"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("删除两个元素e7和e6:"+jedis.srem("eleSet", "e7","e6"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("随机的移除集合中的一个元素:"+jedis.spop("eleSet"));
        System.out.println("随机的移除集合中的一个元素:"+jedis.spop("eleSet"));
        System.out.println("eleSet的所有元素为:"+jedis.smembers("eleSet"));
        System.out.println("eleSet中包含元素的个数:"+jedis.scard("eleSet"));
        System.out.println("e3是否在eleSet中:"+jedis.sismember("eleSet", "e3"));
        System.out.println("e1是否在eleSet中:"+jedis.sismember("eleSet", "e1"));
        System.out.println("e1是否在eleSet中:"+jedis.sismember("eleSet", "e5"));
        System.out.println("=================================");
        System.out.println(jedis.sadd("eleSet1", "e1","e2","e4","e3","e0","e8","e7","e5"));
        System.out.println(jedis.sadd("eleSet2", "e1","e2","e4","e3","e0","e8"));
        System.out.println("将eleSet1中删除e1并存入eleSet3中:"+jedis.smove("eleSet1", "eleSet3", "e1"));//移到集合元素
        System.out.println("将eleSet1中删除e2并存入eleSet3中:"+jedis.smove("eleSet1", "eleSet3", "e2"));
        System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1"));
        System.out.println("eleSet3中的元素:"+jedis.smembers("eleSet3"));
        System.out.println("============集合运算=================");
        System.out.println("eleSet1中的元素:"+jedis.smembers("eleSet1"));
        System.out.println("eleSet2中的元素:"+jedis.smembers("eleSet2"));
        System.out.println("eleSet1和eleSet2的交集:"+jedis.sinter("eleSet1","eleSet2"));
        System.out.println("eleSet1和eleSet2的并集:"+jedis.sunion("eleSet1","eleSet2"));
        System.out.println("eleSet1和eleSet2的差集:"+jedis.sdiff("eleSet1","eleSet2"));//eleSet1中有,eleSet2中没有
        jedis.sinterstore("eleSet4","eleSet1","eleSet2");//求交集并将交集保存到dstkey的集合
        System.out.println("eleSet4中的元素:"+jedis.smembers("eleSet4"));
    }
}

Redis详细教程-学习笔记_第88张图片

List
import redis.clients.jedis.Jedis;

public class TestList {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        System.out.println("===========添加一个list===========");
        jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap");
        jedis.lpush("collections", "HashSet");
        jedis.lpush("collections", "TreeSet");
        jedis.lpush("collections", "TreeMap");
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部
        System.out.println("collections区间0-3的元素:"+jedis.lrange("collections",0,3));
        System.out.println("===============================");
        // 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈
        System.out.println("删除指定元素个数:"+jedis.lrem("collections", 2, "HashMap"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("删除下表0-3区间之外的元素:"+jedis.ltrim("collections", 0, 3));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections列表出栈(左端):"+jedis.lpop("collections"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections添加元素,从列表右端,与lpush相对应:"+jedis.rpush("collections", "EnumMap"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("collections列表出栈(右端):"+jedis.rpop("collections"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("修改collections指定下标1的内容:"+jedis.lset("collections", 1, "LinkedArrayList"));
        System.out.println("collections的内容:"+jedis.lrange("collections", 0, -1));
        System.out.println("===============================");
        System.out.println("collections的长度:"+jedis.llen("collections"));
        System.out.println("获取collections下标为2的元素:"+jedis.lindex("collections", 2));
        System.out.println("===============================");
        jedis.lpush("sortedList", "3","6","2","0","7","4");
        System.out.println("sortedList排序前:"+jedis.lrange("sortedList", 0, -1));
        System.out.println(jedis.sort("sortedList"));
        System.out.println("sortedList排序后:"+jedis.lrange("sortedList", 0, -1));
    }
}

Redis详细教程-学习笔记_第89张图片

Hash
import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.Map;

public class TestHash {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        Map<String,String> map = new HashMap<String,String>();
        map.put("key1","value1");
        map.put("key2","value2");
        map.put("key3","value3");
        map.put("key4","value4");
        //添加名称为hash(key)的hash元素
        jedis.hmset("hash",map);
        //向名称为hash的hash中添加key为key5,value为value5元素
        jedis.hset("hash", "key5", "value5");
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));//return Map
        System.out.println("散列hash的所有键为:"+jedis.hkeys("hash"));//return Set
        System.out.println("散列hash的所有值为:"+jedis.hvals("hash"));//return List
        System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 6));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:"+jedis.hincrBy("hash", "key6", 3));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("删除一个或者多个键值对:"+jedis.hdel("hash", "key2"));
        System.out.println("散列hash的所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("散列hash中键值对的个数:"+jedis.hlen("hash"));
        System.out.println("判断hash中是否存在key2:"+jedis.hexists("hash","key2"));
        System.out.println("判断hash中是否存在key3:"+jedis.hexists("hash","key3"));
        System.out.println("获取hash中的值:"+jedis.hmget("hash","key3"));
        System.out.println("获取hash中的值:"+jedis.hmget("hash","key3","key4"));
    }
}

Redis详细教程-学习笔记_第90张图片

Redis配置文件详解

启动的时候,就通过配置文件来启动

单位

Redis详细教程-学习笔记_第91张图片

包含 INCLUDES

Redis详细教程-学习笔记_第92张图片

网络 NETWORK

# 绑定的IP
bind 127.0.0.1
# 保护模式
protected-mode yes
# 端口设置
port 6379

Redis详细教程-学习笔记_第93张图片

Redis详细教程-学习笔记_第94张图片

通用 GENERAL

Redis详细教程-学习笔记_第95张图片

Redis详细教程-学习笔记_第96张图片

Redis详细教程-学习笔记_第97张图片

Redis详细教程-学习笔记_第98张图片

image-20210812200331338

Redis详细教程-学习笔记_第99张图片

快照 SNAPSHOTTING

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

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

Redis详细教程-学习笔记_第100张图片

Redis详细教程-学习笔记_第101张图片

Redis详细教程-学习笔记_第102张图片

Redis详细教程-学习笔记_第103张图片

安全 SECURITY

Redis详细教程-学习笔记_第104张图片

# 获取密码
config get requirepass
# 设置密码
config set requirepass "111"
# 使用密码登录
auth 111

image-20210813202301471

Redis详细教程-学习笔记_第105张图片

限制 CLIENTS

Redis详细教程-学习笔记_第106张图片

Redis详细教程-学习笔记_第107张图片

Redis详细教程-学习笔记_第108张图片

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模式,aof 配置

Redis详细教程-学习笔记_第109张图片

Redis详细教程-学习笔记_第110张图片

Redis持久化

Redis是内存数据库,如果不讲内存中的数据库状态保存到磁盘,那么一旦服务器退出,服务器中的数据库状态也会小时,所以Redis提供了持久化功能

RDB(Redis DataBase)

什么是RDB

Redis详细教程-学习笔记_第111张图片

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

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

有时候在生产环境中,我们会将这个文件进行备份

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

Redis详细教程-学习笔记_第112张图片

# 删除文件
rm -rf dump.rdb

Redis详细教程-学习笔记_第113张图片

Redis详细教程-学习笔记_第114张图片

在一分钟内修改了五次key,rdb文件再次生成

Redis详细教程-学习笔记_第115张图片

Redis详细教程-学习笔记_第116张图片

因为rdb文件还在,就算重新启动服务,数据还是保存着,这就是持久化

Redis详细教程-学习笔记_第117张图片

再次删除rdb文件,再在客户端内执行 FLUSHALL 命令,它会再次生成一个rdb文件

Redis详细教程-学习笔记_第118张图片

Redis详细教程-学习笔记_第119张图片

触发机制
  • save的规则满足的情况下,会自动触发rdb规则
  • 执行flushall命令,也会触发我们的rdb规则
  • 退出redis,也会战胜rdb文件

备份就自动生成一个 dump.rdb

恢复

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

2、查看需要存在的位置

config get dir

如果这个目录下存在 dump.rdb 文件,启动就会自动恢复其中的数据

image-20210813210234725

优点
  • 适合大规模的数据恢复
  • 对数据的完整性要求不高
缺点
  • 需要一定的时间间隔进程操作,如果redis意外宕机了,这个最后一次修改数据就没有了
  • fork进程的时候,会占用一定的内存空间

注意:测试完成之后,配置文件中配置的 rdb 触发条件记得恢复默认

AOF(Append Only File)

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

Redis详细教程-学习笔记_第120张图片

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

aof保存的是appendonly.aof 文件

Redis详细教程-学习笔记_第121张图片

它默认是不开启的,我们需要手动进行配置,我们只需要将 appendonly 改为yes就开启了aof

重启redis就可以生效了

Redis详细教程-学习笔记_第122张图片

Redis详细教程-学习笔记_第123张图片

设值,退出

Redis详细教程-学习笔记_第124张图片

编辑 aof 文件

vim appendonly.aof

Redis详细教程-学习笔记_第125张图片

修改aof文件

Redis详细教程-学习笔记_第126张图片

再次连接客户端,连接失败

Redis详细教程-学习笔记_第127张图片

如果这个aof文件有错误,这时候redis是启动不起来的,我们需要修复这个aof文件

redis提供了一个工具 redis-check-aof --fix

Redis详细教程-学习笔记_第128张图片

查看aof文件,之前被修改过的字符没有了

Redis详细教程-学习笔记_第129张图片

再次启动服务连接客户端进行测试,连接成功

Redis详细教程-学习笔记_第130张图片

优点和缺点
appendonly yes  # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分的情况下,rdb完全够用
appendfilename "appendonly.aof"

# appendfsync always # 每次修改都会sync 消耗性能
appendfsync everysec # 每秒执行一次 sync 可能会丢失这一秒的数据
# appendfsync no # 不执行 sync ,这时候操作系统自己同步数据,速度最快
优点
  • 每一次修改都会同步,文件的完整性会更加好
  • 没秒同步一次,可能会丢失一秒的数据
  • 从不同步,效率最高
缺点
  • 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢!
  • Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化
注意:测试完成后记得把配置修改回来,默认使用rdb,把yes改成no

Redis详细教程-学习笔记_第131张图片

如何选择使用哪种持久化方式?

一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。

如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

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

扩展

1、RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 材议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式

  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。

5、性能建议

  • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份1次就够了,只保留save 9001这条规则。

  • 如果Enable AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的I0,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。

  • 如果不Enable AOF,仅靠Master-Slave Replcation实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave 中的RDB文件,载入较新的那个,微博就是这种架构。

Redis发布订阅

Redis发布定于(pub/sub)是一种消息通信模式:发送者发送消息,订阅者接受消息

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

Redis详细教程-学习笔记_第132张图片

Redis详细教程-学习笔记_第133张图片

命令

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

Redis详细教程-学习笔记_第134张图片

测试

订阅一个频道

SUBSCRIBE world

image-20210816151958462

发布消息到指定频道

PUBLISH world "hello world"

Redis详细教程-学习笔记_第135张图片

实时接收

Redis详细教程-学习笔记_第136张图片

原理

Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现,籍此加深对 Redis的理解。

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

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

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

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

使用场景
  • 实时消息系统
  • 实时聊天
  • 订阅,关注系统

Redis主从复制

概念

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

默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。主从复制的作用主要包括:

1、数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

2、故障恢复∶当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3、负载均衡∶在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

4、高可用基石∶除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

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

1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;

2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。对于这种场景,我们可以使如下这种架构︰

Redis详细教程-学习笔记_第137张图片

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

环境配置

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

info replication

Redis详细教程-学习笔记_第138张图片

开启四个窗口,复制四个配置文件

Redis详细教程-学习笔记_第139张图片

修改配置文件

  • 修改端口,根据redis(79,80,81).conf 来,分别是6379,6380,6381

Redis详细教程-学习笔记_第140张图片

  • 修改pid

Redis详细教程-学习笔记_第141张图片

  • 修改日志文件名称(注意:另外两个修改时根据端口号来,后面两个数字代表端口63(79,80,81))

Redis详细教程-学习笔记_第142张图片

  • 修改rdb文件名

Redis详细教程-学习笔记_第143张图片

  • 允许后台运行

Redis详细教程-学习笔记_第144张图片

启动服务

不同配置文件启动服务,连接客户端时记得修改端口

Redis详细教程-学习笔记_第145张图片

查看进程

Redis详细教程-学习笔记_第146张图片

一主二从

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

一般情况加只用配置从机就好了

一主(79)二从(80,81)

配置
SLAVEOF 127.0.0.1 6379

81端口服务也是一样的操作

Redis详细教程-学习笔记_第147张图片

在主机中查看相关信息

Redis详细教程-学习笔记_第148张图片

真实的主从配置应该在配置文件中配置,这样的化时永久的,我们这里使用的时命令,是暂时的

Redis详细教程-学习笔记_第149张图片

细节

主机可以写,从机不能写,只能读,主机中所有信息和数据,都会自动被从机保存

Redis详细教程-学习笔记_第150张图片

Redis详细教程-学习笔记_第151张图片

测试

主机断开连接,从机依旧连接到主机

Redis详细教程-学习笔记_第152张图片

Redis详细教程-学习笔记_第153张图片

如果是使用命令行配置的主从,这个时候如果重启了,就会变回主机,再次设置为原来的从机,立马就可以从主机中获取数据

复制原理

Slave启动成功姓接到master后会发送一个sync命令

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

全量复制︰而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步

但是只要是重新连接master,一次完全同步(全量复制)将被自动执行

哨兵模式

概述

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑 哨兵模式

Redis从2.8开始正式提供了 Sentinel 架构来解决这个问题

能够后台监控主机是都故障,如果故障了根据投票数自动将从库转换为主库

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

Redis详细教程-学习笔记_第154张图片

作用

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

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

Redis详细教程-学习笔记_第155张图片

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

测试

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

image-20210817094650758

2、添加配置

  • 数字1表示 :当一个哨兵主观认为主机断开,就可以客观认为主机故障,然后开始选举新的主机。
# 哨兵核心配置
# sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1

Redis详细教程-学习笔记_第156张图片

3、启动哨兵

redis-sentinel config/sentinel.conf

Redis详细教程-学习笔记_第157张图片

4、测试

关闭主机

image-20210817100723061

稍等一会儿,哨兵会自动监控并打印日志,选举一个新的主机

如果master节点断开了,这个时候就会从从机中随机选择一个服务器(有一个投票算法)

Redis详细教程-学习笔记_第158张图片

这时候可以看到我们6380端口变成了主机

Redis详细教程-学习笔记_第159张图片

再次启动6379服务并连接

image-20210817102012294

稍等一会儿看哨兵日志

Redis详细教程-学习笔记_第160张图片

我们再看看 6379 端口服务的信息,他变成了 6380 的从机

Redis详细教程-学习笔记_第161张图片

如果主机回来,只能归并到新的主机下,当作从机,这就是哨兵模式的规则

哨兵模式

优点
  • 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
  • 主从可以切换,故障可以转移,息肉的可用性就会更好
  • 哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点
  • Redis 不好在线扩容,集群容量一旦到达上限,在线扩容十分麻烦
  • 实现哨兵模式的配置其实是很麻烦的,里面有很多选择

哨兵模式完整配置

# 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    
sentinel monitor mymaster 127.0.0.1 6379 1
 
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass  
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
 
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds  
sentinel down-after-milliseconds mymaster 30000
 
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs  
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  
sentinel failover-timeout mymaster 180000
 
# SCRIPTS EXECUTION
 
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
 
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script  
  sentinel notification-script mymaster /var/redis/notify.sh
 
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
#       
# 目前总是“failover”,
# 是“leader”或者“observer”中的一个。 
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script  
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

Redis缓存穿透和雪崩

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

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

Redis详细教程-学习笔记_第162张图片

缓存穿透

概念

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

解决方案

1、布隆过滤器

​ 对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。

Redis详细教程-学习笔记_第163张图片

2、缓存空对象

​ 一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。

Redis详细教程-学习笔记_第164张图片

这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间

即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿

概念

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

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

解决方案

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

​ 这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。

2、加互斥锁(分布式锁)

​ 在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。

缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。

产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

Redis详细教程-学习笔记_第165张图片

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

解决方案

1、redis高可用

​ 这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群(异地多活)

2、限流降级

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

3、数据预热

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

b站 狂神说 Redis学习笔记
视频链接:https://www.bilibili.com/video/BV1S54y1R7SB?p=1

你可能感兴趣的:(redis,linux)