Redis最新CSDN

Redis 的安装与配置

Redis 的安装

安装前的准备工作

A. 安装 gcc
	由于 Redis 是由 C/C++语言编写的,而从官网下载的 Redis 安装包是需要编译后才可安装的,
	所以对其进行编译就必须要使用相关编译器。对于 C/C++语言的编译器,使用最多的是gcc 与 gcc-c++,
	而这两款编译器在 CentOS7 中是没有安装的,所以首先要安装这两款编译器。GCC,GNU Compiler Collection,GNU 编译器集合。

image-20231031170713682

B. 下载 Redis

redis 的官网为: http://redis.io。点击下面的链接可以直接进行下载

C. 上传到 Linux

安装 Redis

A. 解压 Redis

将 Redis 解压到/opt/apps 目录中。如果报错的话,将redis的tar包写成全路径

image-20231031170938185

进入到/opt/apps 目录中再将 Redis 解压包目录更名为 redis(不更名也无所谓)。

在这里插入图片描述

B. 编译
编译过程是根据 Makefile 文件进行的,而 Redis 解压包中已经存在该文件了。所以可以直接进行编译了。

在这里插入图片描述

进入到解压目录中,然后执行编译命令 make。

image-20231031171106715

当看到如下提示时,表示编译成功。

image-20231031171127710

C. 安装

在 Linux 中对于编译过的安装包执行 make install 进行安装。

在这里插入图片描述

可以看到,共安装了三个组件:redis 服务器、客户端与一个性能测试工具 benchmark。

D. 查看 bin 目录

安装完成后,打开/usr/local/bin 目录,可以看到出现了很多的文件。

image-20231031171222028

通过 echo $PATH 可以看到,/usr/local/bin 目录是存在于该系统变量中的,这样这些命令就可以在任意目录中执行了。

image-20231031171255559

Redis 启动与停止

A. 前台启动

在任意目录执行 redis-server 命令即可启动 Redis。这种启动方式会占用当前命令行窗口。

image-20231031171351861

再开启一个会话窗口,可以查看到当前的 Redis 进程,默认端口号为 6379。

image-20231031171406405

通过 Ctrl + C 命令可以停止 Redis。

B. 命令式后台启动
	使用 nohub 命令,最后再添加一个&符,可以使要启动的程序在后台以守护进程方式运行。
	这样的好处是,进程启动后不会占用一个会话窗口,且其还会在当前目录,
	即运行启动命令的当前目录中创建一个 nohup.out 文件用于记录 Redis 的操作日志。

在这里插入图片描述

C. Redis 的停止

通过 redis-cli shutdown 命令可以停止 Redis。

在这里插入图片描述

D. 配置式后台启动
使用 nohup 命令可以使 Redis 后台启动,但每次都要键入 nohup 与&符,比较麻烦。
可以通过修改 Linux 中 Redis 的核心配置文件 redis.conf 达到后台启动的目的。
redis.conf 文件在Redis 的安装目录根下。
将 daemonize 属性值由 no 改为 yes,使 Redis 进程以守护进程方式运行。
修改后再启动 Redis,就无需再键入 nohup 与&符了,但必须要指定启动所使用的 Redis配置文件。这是为什么呢?
使用 nohup redis-server &命令启动 Redis 时,启动项中已经设置好了 Redis 各个参数的默认值,Redis 会按照这些设置的参数进行启动。
但这些参数是可以在配置文件中进行修改的,修改后,需要在启动命令中指定要加载的配置文件,这样,配置文件中的参数值将覆盖原默认值。
Redis 已经给我们提供好了配置文件模板,是 Redis 安装目录的根目录下的 redis.conf 文件。
由于刚刚对 redis.conf 配置文件做了修改,所以在开启 Redis 时需要显示指出要加载的配置文件。配置文件应紧跟在 redis-server 的后面。

image-20231031171944475

连接前的配置

A. 绑定客户端 IP

Redis 可以通过修改配置文件来限定可以访问自己的客户端 IP。
只允许当前主机访问当前的 Redis,其它主机均不可访问。
所以,如果不想限定访问的客户端,只需要将该行注释掉即可。

在这里插入图片描述

B. 关闭保护模式

默认保护模式是开启的。其只允许本机的客户端访问,即只允许自己访问自己。
但生产中应该关闭,以确保其它客户端可以连接 Redis。

在这里插入图片描述

C. 设置访问密码

为 Redis 设置访问密码,可以对要读/写 Redis 的用户进行身份验证。没有密码的用户可以登录 Redis,但无法访问。

密码设置

访问密码的设置位置在 redis.conf 配置文件中。默认是被注释掉的,没有密码

在这里插入图片描述

没有通过密码登录的用户,无法读/写 Redis。

在这里插入图片描述

使用密码
对于密码的使用,有两种方式:
​	登录时未使用密码,则访问时先输入密码;
​	登录时直接 使用密码登录,访问时无需再输入密码。
登录时未使用密码

在这里插入图片描述

登录时使用密码

在这里插入图片描述

退出时使用密码

在这里插入图片描述

D. 禁止/重命名命令

	后面要学习两个非常危险的命令:flushal 与 flushdb。
	它们都是用于直接删除整个 Redis数据库的。若让用户可以随便使用它们,可能会危及数据安全。
	Redis 可以通过修改配置文件来禁止使用这些命令,或重命名这些命令,改成管理员自己知道的密码。
	以下配置,禁用了 flushall 与 flushdb 命令。

在这里插入图片描述

Redis 配置文件详解

	Redis 的核心配置文件 redis.conf 在安装根目录下,默认包含 2000 多行。
	这些内容根据 功能被划分为了很多部分。下面将一些重要部分进行介绍。

A. 基础说明

在这里插入图片描述

这部分主要是给出一些说明,包含三部分意思:
	​ 第 1-6 行用于说明,如果要启动 Redis,需要指出配置文件的路径。
	​ 第 8-16 行用于说明当前配置文件中可以使用的的容量单位及意义。
	​ 第 18 行用于说明这些容量单位没有大小写之分

B. includes

image-20231031173050947

	指定要在当前配置文件中包含的配置文件。
	这样做的目的主要是便于配置信息管理:可以将不同场景的配置都进行单独定义,
		然后在当前核心配置文件中根据不同场景选择包含进不同的配置文件。

C. modules

image-20231031173139981

Redis 配置文件中可以通过加载不同的第三方模块,来增强、扩展 Redis 的功能。

D. network

image-20231031173211391

Network 配置模块是比较重要的部分,主要进行网络相关的配置。其中较重要的有:
	​bind
	​protected-mode
    ​port
    ​tcp-backlog
    ​timeout
    ​tcp-keepalive
bind

image-20231031173359941

	指定可以访问当前 Redis 服务的客户端 IP,默认只允许本地访问,
	即当前 Redis 自己访问自己。为了使所有其它客户端都可访问,一般要将其注释掉。
protected-mode

image-20231031173423934

默认保护模式是开启的。其只允许本机的客户端访问,即只允许自己访问自己。
但生产中应该关闭,以确保其它客户端可以连接 Redis
port

image-20231031173501672

Redis 监听的连接端口号,默认 6379。
tcp-backlog

在这里插入图片描述

	tcp-backlog 是一个 TCP 连接的队列,其主要用于解决高并发场景下客户端慢连接问题。
	这里设置的值就是这个队列的长度。该队列与 TCP 连接的三次握手有关。不同的 Linux 内核,backlog 队列中存放的元素(客户端连接)类型是不同的。
	​ 'Linux 内核 2.2 版本之前',该队列中存放的是已完成了第一次握手的所有客户端连接,其中就包含已完成三次握手的客户端连接。
			当然,此时的 backlog 队列中的连接也具有两种状态:
				未完成三次握手的连接状态为 SYN_RECEIVED,
				已完成三次握手的连接状态为 ESTABLISHED。
				只有 ESTABLISHED 状态的连接才会被 Redis 处理。
	​ 'Linux 内核 2.2 版本之后' TCP 系统中维护了两个队列:SYN_RECEIVED 队列与 ESTABLISHED队列。
			SYN_RECEIVED 队列中存放的是未完成三次握手的连接,
			ESTABLISHED 队列中存放的是已完成三次握手的连接。
			此时的 backlog 就是 ESTABLISHED 队列。
	查看 Linux 内核版本:

在这里插入图片描述

	TCP 中的 backlog 队列的长度在 Linux 中由内核参数 somaxconn 来决定。
	所以,在 Redis中该队列的长度由 Redis 配置文件设置与 somaxconn 来共同决定:取它们中的最小值。
	查看当前 Linux 内核中 somaxconn 的值。

在这里插入图片描述

生产环境下(特别是高并发场景下),backlog 的值最好要大一些,否则可能会影响系统性能。
修改/etc/sysctl.conf 文件,在文件最后添加如下内容:

在这里插入图片描述

修改过后可以重启虚拟机,也可以通过执行如下命令来使新的修改生效

在这里插入图片描述

timeout

image-20231031173804505

空闲超时。当客户端与 Redis 间的空闲时间超过该时长后,连接自动断开。
单位秒。默认值为 0,表示永远不超时。
tcp-keepalive

image-20231031173826126

该配置主要用于设置 Redis 检测与其连接的所有客户端的存活性时间间隔,
单位秒。一般是在空闲超时 timeout 设置为 0 时进行配置

E. general

daemonize

image-20231031173907520

该配置可以控制 Redis 启动是否采用守护进程方式,即是否是后台启动。
yes 是采用后台启动。
pidfile

image-20231031173926067

	该配置用于指定 Redis 运行时 pid 写入的文件,无论 Redis 是否采用守护进程方式启动,pid 都会写入到该配置的文件。
	注意,如果没有配置 pid 文件,不同的启动方式,pid 文件的产生效果是不同的:
		​ 采用守护进程方式启动(后台启动,daemonize 为 yes):pid 文件为/var/run/redis.pid。
		​ 采用前台启动(daemonize 为 no):不生产 pid 文件
loglevel

在这里插入图片描述

配置日志的级别。Redis 中共有四个级别,由低到高依次是:
	​ debug:可以获取到很多的信息,一般在开发和测试时使用。
	​ verbose:可以获取到很多不太有用的信息,但不像 debug 级别那么多。
	​ notice:可以获取到在生产中想获取到的适当多的信息,默认级别。
	​ warning:只记录非常重要/关键的信息。
logfile

image-20231031174107409

	指定日志文件。如果设置为空串,则强制将日志记录到标准输出设备(显示器)。
	如果使用的是守护进程启动方式,设置为空串,则意味着会将日志发送到设备/dev/null(空设备)。
databases

image-20231031174127188

	设置数据库的数量。默认数据库是 0 号数据库。
	可以使用 select <dbid>在每个连接的基础上选择一个不同的数据库,其中 dbid 是介于 0 和'databases'-1'之间的数字。

F. security

image-20231031174152677

用户设置 ACL 权限、Redis 访问密码相关配置。该模块中最常用的就是 requirepass 属性

在这里插入图片描述

设置客户端访问密码。注释掉后则没有密码。

G. clients

在这里插入图片描述

	该模块用于设置与客户端相关的属性,其中仅包含一个属性 maxclients。
	maxclients 用于设置 Redis 可并发处理的客户端连接数量,默认值为 10000。
	如果达到了该最大连接数,则会拒绝再来的新连接,并返回一个异常信息:已达到最大连接数。
	注意,该值不能超过 Linux 系统支持的可打开的文件描述符最大数量阈值。
	查看该阈值的方式如下。修改该值,可以通过修改/etc/secutiry/limits.conf 文件(自己查)

在这里插入图片描述

H. memory management

该配置可以控制最大可用内存及相关内容移除问题。

maxmemory

image-20231031174341341

	将内存使用限制设置为指定的字节数。当达到内存限制时,Redis 将根据选择的逐出策略 maxmemory-policy 尝试删除符合条件的 key。
	如果不能按照逐出策略移除 key,则会给写操作命令返回 error,但对于只读的命令是没有影响的。
maxmamory-policy

在这里插入图片描述

	该属性用于设置,当达到 maxmemory 时,Redis 将如何选择要移除的内容。
	当然,如果没有符合相应策略的内容要删除,则在执行写入命令时会给出 errors 的响应。Redis 中共支持 8 种移除策略:
	​ volatile-lru:使用近似 LRU 算法移除,仅适用于设置了过期时间的 key。
	​ allkeys-lru:使用近似 LRU 算法移除,可适用于所有类型的 key。
	​ volatile-lfu:使用近似 LFU 算法移除,仅适用于设置了过期时间的 key。
	​ allkeys-lfu:使用近似 LFU 算法移除,可适用于所有类型的 key。
	​ volatile-random:随机移除一个 key,仅适用于设置了过期时间的 key。
	​ allkeys-random:随机移除一个 key,可适用于所有类型的 key。
	​ volatile-ttl:移除距离过期时间最近的 key。
	​ noeviction:不移除任何内容,只是在写操作时返回一个错误,默认值
maxmemory-samples

在这里插入图片描述

	该属性用于指定挑选要删除的 key 的样本数量。
	样本的选择采用的是 LRU 算法,其不能修改。但从样本中再选择要移除的 key,则采用的是 maxmamory-policy 指定的策略。
maxmemory-eviction-tenacity

image-20231031174550109

设置移除容忍度。数值越小表示容忍度越低,需要移除的数据移除延迟越小;
数值越大表示容忍度越高,需要移除的数据移除延迟越大。

I. threaded I/O

image-20231031174621780

该配置模块用于配置 Redis 对多线程 IO 模型的支持。

io-threads

image-20231031174658045

该属性用于指定要启用多线程 IO 模型时,要使用的线程数量。
查看当前系统中包含的 CPU 数量

在这里插入图片描述

io-threads-do-reads

image-20231031174736428

该属性用于启用多线程 IO 模型中的多线程处理读请求的能力

Redis命令

Redis基本命令

心跳命令 ping

键入 ping 命令,会看到 PONG 响应,则说明该客户端与 Redis 的连接是正常的。该命令 亦称为心跳命令。

DB 切换 select

可以通过 select db 索引来切换 DB

查看 key 数量 dbsize

dbsize 命令可以查看当前数据库中 key 的数量。

删除当前库中数据 flushdb

flushdb 命令仅仅删除的是当前数据库中的数据,不影响其它库。

删除所有库中数据命令 flushall

flushall 命令可以删除所有库中的所有数据。所以该命令的使用一定要慎重。

Key 操作命令

Redis 中存储的数据整体是一个 Map,其 key 为 String 类型,而 value 则可以是 String、 Hash 表、List、Set 等类型。

keys
'格式'KEYS pattern
'功能':查找所有符合给定模式 pattern 的 key,pattern 为正则表达式。
'说明'KEYS 的速度非常快,但在一个大的数据库中使用它可能会阻塞当前服务器的服务。
所以生产环境中一般不使用该命令,而使用 scan 命令代替。
exists
'格式'EXISTS key
'功能':检查给定 key 是否存在。
'说明':若 key 存在,返回 1 ,否则返回 0
del
'格式'DEL key [key ...]
'功能':删除给定的一个或多个 key 。不存在的 key 会被忽略。
'说明':返回被删除 key 的数量。
rename
'格式'RENAME key newkey
'功能':将 key 改名为 newkey。
'说明':当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。
当 newkey 已经存在时, RENAME 命令将覆盖旧值。改名成功时提示 OK ,失败时候返回一个错误。
move
'格式'MOVE key db
'功能':将当前数据库的 key 移动到给定的数据库 db 当中。
'说明':如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,
或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。移动成功返回 1 ,失败则返回 0
type
'格式'TYPE key
'功能':返回 key 所储存的值的类型。
'说明':返回值有以下六种
 none (key 不存在)
 string (字符串)
 list (列表)
 set (集合)
 zset (有序集)
 hash (哈希表)
expire 与 pexpire
'格式'EXPIRE key seconds
'功能':为给定 key 设置生存时间。当 key 过期时(生存时间为 0),它会被自动删除。
expire 的时间单位为秒,pexpire 的时间单位为毫秒。在 Redis 中,带有生存时间的 key被称为“易失的”(volatile)'说明':生存时间设置成功返回 1。若 key 不存在时返回 0 。rename 操作不会改变 key的生存时间。
ttl 与 pttl
'格式'TTL key
'功能'TTL, time to live,返回给定 key 的剩余生存时间。
'说明':其返回值存在三种可能:
    当 key 不存在时,返回 -2 。
	当 key 存在但没有设置剩余生存时间时,返回 -1 。
    否则,返回 key 的剩余生存时间。ttl 命令返回的时间单位为秒,而 pttl 命令返回的时间单位为毫秒。
persist
'格式'PERSIST key
'功能':去除给定 key 的生存时间,将这个 key 从“易失的”转换成“持久的”。
'说明':当生存时间移除成功时,返回 1;若 key 不存在或 key 没有设置生存时间,则返回 0
randomkey
'格式'RANDOMKEY
'功能':从当前数据库中随机返回(不删除)一个 key。
'说明':当数据库不为空时,返回一个 key。当数据库为空时,返回 nil。
scan
'格式'SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
'功能':用于迭代数据库中的数据库键。其各个选项的意义为:
	cursor:本次迭代开始的游标。
	pattern :本次迭代要匹配的 key 的模式。
	count :本次迭代要从数据集里返回多少元素,默认值为 10 。
	type:本次迭代要返回的 value 的类型,默认为所有类型。
'说明':使用间断的、负数、超出范围或者其他非正常的游标来执行增量式迭代不会造成服务器崩溃。
'相关命令':另外还有 3 个 scan 命令用于对三种类型的 value 进行遍历。
	hscan:属于 HashValue 操作命令集合,用于遍历当前 db 中指定 Hash 表的所有 field-value 对。
	sscan:属于 SetValue 操作命令集合,用于遍历当前 db 中指定 set 集合的所有元素
	zscan:属于 ZSetValue 操作命令集合,用于遍历当前 db 中指定有序集合的所有元素(数值与元素值)

String 型 Value 操作命令

一个 String 类型的 Value 最大是 512M 大小。

set
'格式'SET key value [EX seconds | PX milliseconds] [NX|XX]
'功能'SET 除了可以直接将 key 的值设为 value 外,还可以指定一些参数。
	EX seconds:为当前 key 设置过期时间,单位秒。等价于 SETEX 命令。
	PX milliseconds:为当前 key 设置过期时间,单位毫秒。等价于 PSETEX 命令。
	NX:指定的 key 不存在才会设置成功,用于添加指定的 key。等价于 SETNX 命令。
	XX:指定的 key 必须存在才会设置成功,用于更新指定 key 的 value。
'说明':如果 value 字符串中带有空格,则该字符串需要使用双引号或单引号引起来,否则会认为 set 命令的参数数量不正确,报错。
setex 与 psetex
'格式'SETEX/PSETEX key seconds value
'功能':set expire,其不仅为 key 指定了 value,还为其设置了生存时间。setex 的单位为秒,psetex 的单位为毫秒。
'说明':如果 key 已经存在, 则覆写旧值。该命令类似于以下两个命令,不同之处是,SETEX 是一个原子性操作,
	   关联值和设置生存时间两个动作会在同一时间内完成,该命令在 Redis 用作缓存时,非常实用。
SET key value       
EXPIRE key seconds # 设置生存时间   --- 这两行合成上面一行代码
setnx
'格式'SETNX key value
'功能'SET if Not eXists,将 key 的值设为 value ,
       当且仅当 key 不存在。若给定的 key已经存在,则 SETNX 不做任何动作。成功,返回 1,否则,返回 0'说明':该命令等价于 set key value nx
getset
'格式'GETSET key value
'功能':将给定 key 的值设为 value ,并返回 key 的旧值。
'说明':当 key 存在但不是字符串类型时,返回一个错误;当 key 不存在时,返回 nil 。
mset 与 msetnx
'格式'MSET/MSETNX key value [key value ...]
'功能':同时设置一个或多个 key-value 对。
'说明':如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,
	   请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。MSET/MSETNX 是一个原子性(atomic)操作,
       所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况不可能发生。该命令永不失败
mget
'格式'MGET key [key ...]
'功能':返回所有(一个或多个)给定 key 的值。
'说明':如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。
append
'格式'APPEND key value
'功能':如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾。
	   如果 key 不存在, APPEND 就简单地将给定 key 设为 value ,就像执行 SET key value 一样。
'说明':追加 value 之后, key 中字符串的长度。
incr 与 decr
'格式'INCR key 或 DECR key
'功能':increment,自动递增。将 key 中存储的数字值增一。
 	   decrement,自动递减。将 key 中存储的数字值减一。
'说明':如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行增一/减一操作。如果值不能表示为数字,那么返回一个错误提示。如果执行正确,则返回增一/减一后的值。
incrby 与 decrby
'格式'INCRBY key increment 或 DECRBY key decrement
'功能':将 key 中存储的数字值增加/减少指定的数值,这个数值只能是整数,可以是负数,但不能是小数。
'说明':如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行增/减操作。如果值不能表示为数字,那么返回一个错误提示。如果执行正确,则返回增/减后的值。
incrbyfloat
'格式'INCRBYFLOAT key increment
'功能':为 key 中所储存的值加上浮点数增量 increment 。
'说明':与之前的说明相同。没有 decrbyfloat 命令,但 increment 为负数可以实现减操作效果。
strlen
'格式'STRLEN key
'功能':返回 key 所储存的字符串值的长度。
'说明':当 key 储存的不是字符串值时,返回一个错误;当 key 不存在时,返回 0
getrange
'格式'GETRANGE key start end
'功能':返回 key 中字符串值的子字符串,字符串的截取范围由 start 和 end 两个偏移量决定,包括 start 和 end 在内。
'说明':end 必须要比 start 大。支持负数偏移量,表示从字符串最后开始计数,-1 表示最后一个字符,-2 表示倒数第二个,以此类推。
setrange —
'格式'SETRANGE key offset value
'功能':用 value 参数替换给定 key 所储存的字符串值 str,从偏移量 offset 开始。
'说明':当 offset 值大于 str 长度时,中间使用零字节\x00 填充,即 0000 0000 字节填充;对于不存在的 key 当作空串处理。
位操作命令
名称中包含 BIT 的命令,都是对二进制位的操作命令,
例如,setbit、getbit、bitcount、bittop、bitfield,这些命令不常用。
典型应用场景

Value 为 String 类型的应用场景很多,这里仅举这种典型应用场景的例子:

数据缓存
Redis 作为数据缓存层,MySQL 作为数据存储层。
应用服务器首先从 Redis 中获取数据, 如果缓存层中没有,则从 MySQL 中获取后先存入缓存层再返回给应用服务器。
计数器
在 Redis 中写入一个 value 为数值型的 key 作为平台计数器、视频播放计数器等。
每个有效客户端访问一次,或视频每播放一次,都是直接修改 Redis 中的计数器,然后再以异步方式持久化到其它数据源中,
例如持久化到 MySQL。
共享 Session

image-20230808202308252

可以将系统中所有用户的 Session 数据全部保存到 Redis 中

限速器

现在很多平台为了防止 DoS(Denial of Service,拒绝服务)攻击,一般都会限制一个 IP 不能在一秒内访问超过 n 次。而 Redis 可以可以结合 key 的过期时间与 incr 命令来完成限速 功能,充当限速器。注意,其无法防止 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击。

// 客户端每提交一次请求,都会执行下面的代码
// 等价于 set 192.168.192.55 1 ex 60 nx
// 指定新 ip 作为 key 的缓存过期时间为 60 秒
Boolean isExists = redis.set(ip, 1,EX 60,NX);
if(isExists != null || redis.incr(ip) <= 5) {  每个ip每分钟限流五次
 // 通过
} else {
// 限流
}

Hash 型 Value 操作命令

Hash 表就是一个映射表 Map,也是由键-值对构成,为了与整体的 key 进行区分,这里的键称为 field,值称为 value。注意,Redis 的 Hash 表中的 field-value 对均为 String 类型。

hset
'格式'HSET key field value
'功能':将哈希表 key 中的域 field 的值设为 value 。
'说明':如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。
	   如果域 field 已经存在于哈希表中,旧值将被覆盖。
	   如果 field 是哈希表中的一个新建域,并且值设置成功,返回 1 。
	   如果哈希表中域 field 已经存在且旧值已被新值覆盖,返回 0
hget
'格式'HGET key field
'功能':返回哈希表 key 中给定域 field 的值。
'说明':当给定域不存在或是给定 key 不存在时,返回 nil 。
hmset
'格式'HMSET key field value [field value ...]
'功能':同时将多个 field-value (-)对设置到哈希表 key 中。
'说明':此命令会覆盖哈希表中已存在的域。如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。如果命令执行成功,返回 OK 。
       当 key 不是哈希表(hash)类型时,返回一个错误。
hmget
'格式'HMGET key field [field ...]
'功能':按照给出顺序返回哈希表 key 中一个或多个域的值。
'说明':如果给定的域不存在于哈希表,那么返回一个 nil 值。因为不存在的 key 被当作一个空哈希表来处理,
	   所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。
hgetall
'格式'HGETALL key
'功能':返回哈希表 key 中所有的域和值。
'说明':在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。
	   若 key 不存在,返回空列表。
	   若 key 中包含大量元素,则该命令可能会阻塞 Redis 服务。
	   所以生产环境中一般不使用该命令,而使用 hscan 命令代替。
hsetnx
'格式'HSETNX key field value
'功能':将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。
'说明':若域 field 已经存在,该操作无效。如果 key 不存在,一个新哈希表被创建并执行 HSETNX 命令。
hdel
'格式'HDEL key field [field ...]
'功能':删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
'说明':返回被成功移除的域的数量,不包括被忽略的域。
hexits
'格式'HEXISTS key field
'功能':查看哈希表 key 中给定域 field 是否存在。
'说明':如果哈希表含有给定域,返回 1 。如果不含有给定域,或 key 不存在,返回 0
hincrby 与 hincrbyfloat
'格式'HINCRBY key field increment
'功能':为哈希表 key 中的域 field 的值加上增量 increment 。
	   hincrby 命令只能增加整数值,而 hincrbyfloat 可以增加小数值。
'说明':增量也可以为负数,相当于对给定域进行减法操作。
	   如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。
	   如果域 field 不存在,那么在执行命令前,域的值被初始化为 0。对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。
hkeys 与 hvals
'格式'HKEYS key 或 HVALS key
'功能':返回哈希表 key 中的所有域/值。
'说明':当 key 不存在时,返回一个空表。
hlen
'格式'HLEN key
'功能':返回哈希表 key 中域的数量。
'说明':当 key 不存在时,返回 0
hstrlen
'格式'HSTRLEN key field
'功能':返回哈希表 key 中, 与给定域 field 相关联的值的字符串长度(string length)。
'说明':如果给定的键或者域不存在, 那么命令返回 0
应用场景

​ Hash 型 Value 非常适合存储对象数据。key 为对象名称,value 为描述对象属性的 Map, 对对象属性的修改在 Redis 中就可直接完成。其不像 String 型 Value 存储对象,那个对象是 序列化过的,例如序列化为 JSON 串,对对象属性值的修改需要先反序列化为对象后再修改, 修改后再序列化为 JSON 串后写入到 Redis。

List 型 Value 操作命令

​ Redis 存储数据的 Value 可以是一个 String 列表类型数据。即该列表中的每个元素均为 String 类型数据。列表中的数据会按照插入顺序进行排序。不过,该列表的底层实际是一个 无头节点的双向链表,所以对列表表头与表尾的操作性能较高,但对中间元素的插入与删除 的操作的性能相对较差。

lpush/rpush
'格式'LPUSH key value [value ...]RPUSH key value [value ...]
'功能':将一个或多个值 value 插入到列表 key 的表头/表尾(表头在左表尾在右)
'说明':如果有多个 value 值,
	   对于 lpush 来说,各个 value 会按从左到右的顺序依次插入到表头;
	   对于 rpush 来说,各个 value 会按从左到右的顺序依次插入到表尾。
	   如果 key不存在,一个空列表会被创建并执行操作。
	   当 key 存在但不是列表类型时,返回一个错误。执行成功时返回列表的长度。
llen
'格式'LLEN key
'功能':返回列表 key 的长度。
'说明':如果 key 不存在,则 key 被解释为一个空列表,返回 0 。
	   如果 key 不是列表类型,返回一个错误。
lindex
'格式'LINDEX key index
'功能':返回列表 key 中,下标为 index 的元素。列表从 0 开始计数。
'说明':如果 index 参数的值不在列表的区间范围内(out of range),返回 nil 。
lset
'格式'LSET key index value
'功能':将列表 key 下标为 index 的元素的值设置为 value 。
'说明':当 index 参数超出范围,或对一个空列表(key 不存在)进行 LSET 时,返回一个错误。
lrange
'格式'LRANGE key start stop
'功能':返回列表 key 中指定区间[start, stop]内的元素,即包含两个端点。
'说明'List 的下标从 0 开始,即以 0 表示列表的第一个元素,
	   以 1 表示列表的第二个元素,以此类推。也可以使用负数下标,
	   以 -1 表示列表的最后一个元素, 
	   -2 表示列表的倒数第二个元素,以此类推。
	   超出范围的下标值不会引起错误。如果 start 下标比列表的最大下标 还要大,那么 LRANGE 返回一个空列表。
	   如果 stop 下标比最大下标还要大,Redis 将 stop 的值设置为最大下标。
lpushx 与 rpushx
'格式'LPUSHX key value 或 RPUSHX key value
'功能':将值 value 插入到列表 key 的表头/表尾,当且仅当 key 存在并且是一个列表。
'说明':当 key 不存在时,命令什么也不做。若执行成功,则输出表的长度。
linsert
'格式'LINSERT key BEFORE|AFTER pivot value
'功能':将值 value 插入到列表 key 当中,位于元素 pivot 之前或之后。
'说明':当 pivot 元素不存在于列表中时,不执行任何操作,返回-1;
	   当 key 不存在时,key 被视为空列表,不执行任何操作,返回 0;
	   如果 key 不是列表类型,返回一个错误;
	   如果命令执行成功,返回插入操作完成之后,列表的长度。
lpop / rpop
'格式'LPOP key [count]RPOP key [count]
'功能':从列表 key 的表头/表尾移除 count 个元素,并返回移除的元素。
 	   count 默认值 1
'说明':当 key 不存在时,返回 nil
blpop / brpop
'格式'BLPOP key [key ...] timeout 或 BRPOP key [key ...] timeout
'功能'BLPOP/BRPOP 是列表的阻塞式(blocking)弹出命令。
	   它们是 LPOP/RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP/BRPOP 命令阻塞,
	   直到等待 timeout 超时或发现可弹出元素为止。
	   当给定多个 key 参数时,按参数 key的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。
	   timeout 为阻塞时长,单位为秒,其值若为 0,则表示只要没有可弹出元素,则一直阻塞。
'说明':假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长。
	   反之,返回一个含有两个元素的列表,
	   		第一个元素是被弹出元素所属的 key ,
	   		第二个元素是被弹出元素的值。
rpoplpush
'格式'RPOPLPUSH source destination
'功能':命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作:
	将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。
	将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。
如果 source 不存在,值 nil 被返回,并且不执行其他动作。
如果 source 和 destination相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。
brpoplpush
'格式'BRPOPLPUSH source destination timeout
'功能'BRPOPLPUSHRPOPLPUSH 的阻塞版本,当给定列表 source 不为空时,BRPOPLPUSH 的表现和 RPOPLPUSH 一样。
	   当列表 source 为空时, BRPOPLPUSH 命令将阻塞连接,直到等待超时,或有另一个客户端对 source 执行 LPUSHRPUSH 命令为止。
	   timeout 为阻塞时长,单位为秒,其值若为 0,则表示只要没有可弹出元素,则一直阻塞。
'说明':假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长。
	   反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长。
lrem
'格式'LREM key count value
'功能':根据参数 count 的值,移除列表中与参数 value 相等的元素。count 的值可以是以下几种:
	count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
	count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
	count = 0 : 移除表中所有与 value 相等的值。
'说明':返回被移除元素的数量。当 key 不存在时, LREM 命令返回 0 ,因为不存在的 key 被视作空表(empty list)
ltrim
'格式'LTRIM key start stop
'功能':对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
'说明':下标(index)参数 start 和 stop 都以 0 为底,也就是说,
	   以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,
	   以此类推。也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,
	   以此类推。当 key 不是列表类型时,返回一个错误。
	   如果 start 下标比列表的最大下标 end ( LLEN list 减去 1 )还要大,或者 start > stop , LTRIM 返回一个空列表,因为 LTRIM 已经将整个列表清空。
	   如果 stop 下标比 end 下标还要大,Redis 将 stop 的值设置为 end 。
应用场景

Value 为 List 类型的应用场景很多,主要是通过构建不同的数据结构来实现相应的业务 功能。这里仅对这些数据结构的实现方式进行总结,不举具体的例子。

通过 lpush + lpop 可以实现栈数据结构效果:先进后出。
通过 lpush 从列表左侧插入数据,通过 lpop 从列表左侧取出数据。
当然,通过 rpush + rpop 也可以实现相同效果,只不过操作的是列表右侧。
队列
通过 lpush + rpop 可以实现队列数据结构效果:先进先出。
通过 lpush 从列表左侧插入 数据,通过 rpop 从列表右侧取出数据。
当然,通过 rpush + lpop 也可以实现相同效果,只不 过操作的方向正好相反。
阻塞式消息队列
通过 lpush + brpop 可以实现阻塞式消息队列效果。
作为消息生产者的客户端使用 lpush 从列表左侧插入数据,
作为消息消费者的多个客户端使用 brpop 阻塞式“抢占”列表尾部数 据进行消费,保证了消费的负载均衡与高可用性。
brpop 的 timeout 设置为 0,表示只要没 有数据可弹出,就永久阻塞。
动态有限集合
通过 lpush + ltrim 可以实现有限集合。
通过 lpush 从列表左侧向列表中添加数据,通过 ltrim 保持集合的动态有限性。
像企业的末位淘汰、学校的重点班等动态管理,都可通过这 种动态有限集合来实现。
当然,通过 rpush + ltrim 也可以实现相同效果,只不过操作的方向 正好相反。

Set 型 Value 操作命令

​ Redis 存储数据的 Value 可以是一个 Set 集合,且集合中的每一个元素均 String 类型。Set 与 List 非常相似,但不同之处是 Set 中的元素具有无序性与不可重复性,而 List 则具有有序 性与可重复性。

​ Redis 中的 Set 集合与 Java 中的 Set 集合的实现相似,其底层都是 value 为 null 的 hash 表。也正因为此,才会引发无序性与不可重复性。

sadd
'格式'SADD key member [member ...]
'功能':将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
'说明':假如 key 不存在,则创建一个只包含 member 元素作成员的集合。当 key 不是集合类型时,返回一个错误。
smembers
'格式'SMEMBERS key
'功能':返回集合 key 中的所有成员。 
'说明':不存在的 key 被视为空集合。若 key 中包含大量元素,则该命令可能会阻塞 Redis 服务。
	   所以生产环境中一般不使用该命令,而使用 sscan 命令代替。
scard
'格式'SCARD key
'功能':返回 Set 集合的长度
'说明':当 key 不存在时,返回 0
sismember
'格式'SISMEMBER key member
'功能':判断 member 元素是否集合 key 的成员。
'说明':如果 member 元素是集合的成员,返回 1 。如果 member 元素不是集合的成员,或 key 不存在,返回 0
smove
'格式'SMOVE source destination member
'功能':将 member 元素从 source 集合移动到 destination 集合。
'说明':如果 source 集合不存在或不包含指定的 member 元素,则 SMOVE 命令不执行任何操作,仅返回 0 。
	   否则, member 元素从 source 集合中被移除,并添加到destination 集合中去,返回 1。
	   当 destination 集合已经包含 member 元素时, SMOVE命令只是简单地将 source 集合中的 member 元素删除。
	   当 source 或 destination 不是集合类型时,返回一个错误。
srem
'格式'SREM key member [member ...]
'功能':移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略,且返回成功移除的元素个数。
'说明':当 key 不是集合类型,返回一个错误。
srandmember
'格式'SRANDMEMBER key [count]
'功能':返回集合中的 count 个随机元素。count 默认值为 1'说明':若 count 为正数,且小于集合长度,那么返回一个包含 count 个元素的数组,数组中的元素各不相同。
	   如果 count 大于等于集合长度,那么返回整个集合。
	   如果count 为负数,那么返回一个包含 count 绝对值个元素的数组,但数组中的元素可能会出现重复。
spop
'格式'SPOP key [count]
'功能':移除并返回集合中的 count 个随机元素。count 必须为正数,且默认值为 1'说明':如果 count 大于等于集合长度,那么移除并返回整个集合。
sdiff / sdiffstore
'格式'SDIFF key [key ...]SDIFFSTORE destination key [key ...]
'功能':返回第一个集合与其它集合之间的差集。差集,difference。
'说明':这两个命令的不同之处在于,sdiffstore 不仅能够显示差集,还能将差集存储到指定的集合 destination 中。
	   如果 destination 集合已经存在,则将其覆盖。不存在的 key 被视为空集。
sinter / sinterstore
'格式'SINTER key [key ...]SINTERSTORE destination key [key ...]
'功能':返回多个集合间的交集。交集,intersection。
'说明':这两个命令的不同之处在于,sinterstore 不仅能够显示交集,还能将交集存储到指定的集合 destination 中。
	   如果 destination 集合已经存在,则将其覆盖。不存在的 key 被视为空集。
sunion / sunionstore
'格式'SUNION key [key ...]SUNIONSTORE destination key [key ...]
'功能':返回多个集合间的并集。并集,union。
'说明':这两个命令的不同之处在于,sunionstore 不仅能够显示并集,还能将并集存储到指定的集合 destination 中。
	   如果 destination 集合已经存在,则将其覆盖。不存在的 key 被视为空集。
应用场景
动态黑白名单

image-20230808205746240

​ 例如某服务器中要设置用于访问控制的黑名单。如果直接将黑名单写入服务器的配置文 件,那么存在的问题是,无法动态修改黑名单。此时可以将黑名单直接写入 Redis,只要有 客户端来访问服务器,服务器在获取到客户端 IP后先从 Redis的黑名单中查看是否存在该 IP, 如果存在,则拒绝访问,否则访问通过。

有限随机数
有限随机数是指返回的随机数是基于某一集合范围内的随机数,例如抽奖、随机选人。 
通过 spop 或 srandmember 可以实现从指定集合中随机选出元素。
用户画像
	社交平台、电商平台等各种需要用户注册登录的平台,会根据用户提供的资料与用户使用习惯,为每个用户进行画像,即为每个用户定义很多可以反映该用户特征的标签,这些标签就可以使用 sadd 添加到该用户对应的集合中。这些标签具有无序、不重复特征。
	同时平台还可以使用 sinter/sinterstore 根据用户画像间的交集进行好友推荐、商品推荐、
客户推荐等。

有序 Set 型 Value 操作命令

​ Redis 存储数据的 Value 可以是一个有序 Set,这个有序 Set 中的每个元素均 String 类型。 有序 Set 与 Set 的不同之处是,有序 Set 中的每一个元素都有一个分值 score,Redis 会根据 score 的值对集合进行由小到大的排序。其与 Set 集合要求相同,元素不能重复,但元素的 score 可以重复。由于该类型的所有命令均是字母 z 开头,所以该 Set 也称为 ZSet。

zadd
'格式'ZADD key score member [[score member] [score member] ...]
'功能':将一个或多个 member 元素及其 score 值加入到有序集 key 中的适当位置。
'说明':score 值可以是整数值或双精度浮点数。
	   如果 key 不存在,则创建一个空的有序集并执行 ZADD 操作。当 key 存在但不是有序集类型时,返回一个错误。
	   如果命令执行成功,则返回被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。若写入的 member 值已经存在,但 score 值不同,则新的 score 值将覆盖老 score。
zrange 与 zrevrange
'格式'ZRANGE key start stop [WITHSCORES]ZREVRANGE key start stop [WITHSCORES]
'功能':返回有序集 key 中,指定区间内的成员。zrange 命令会按 score 值递增排序,zrevrange命令会按score递减排序。
	   具有相同 score 值的成员按字典序/逆字典序排列。可以通过使用 WITHSCORES 选项,来让成员和它的 score 值一并返回。
'说明':下标参数从 0 开始,即 0 表示有序集第一个成员,
	   以 1 表示有序集第二个成员,以此类推。也可以使用负数下标,-1 表示最后一个成员,-2 表示倒数第二个成员,以此类推。
	   超出范围的下标并不会引起错误。例如,当 start 的值比有序集的最大下标还要大,或是 start > stop 时,ZRANGE 命令只是简单地返回一个空列表。
	   再比如 stop 参数的值比有序集的最大下标还要大,那么 Redis 将 stop 当作最大下标来处理。
	   若 key 中指定范围内包含大量元素,则该命令可能会阻塞 Redis 服务。
	   所以生产环境中如果要查询有序集合中的所有元素,一般不使用该命令,而使用 zscan 命令代替。
zrangebyscore 与 zrevrangebyscore
'格式'ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
'功能':返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或max )的成员。
	   有序集成员按 score 值递增/递减次序排列。具有相同 score 值的成员按字典序/逆字典序排列。可选的 LIMIT 参数指定返回结果的数量及区间(就像 SQL 中的SELECT LIMIT offset, count ),
	   注意当 offset 很大时,定位 offset 的操作可能需要遍历整个有序集,此过程效率可能会较低。可选的 WITHSCORES 参数决定结果集是单单返回有序集的成员,还是将有序集成员及其 score 值一起返回。
'说明':min 和 max 的取值是正负无穷大的。默认情况下,区间的取值使用闭区间 (小于等于或大于等于),也可以通过给参数前增加左括号“(”来使用可选的开区间 (小于或大于)
zcard
'格式'ZCARD key
'功能':返回集合的长度
'说明':当 key 不存在时,返回 0
zcount
'格式'ZCOUNT key min max
'功能':返回有序集 key 中,score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。
zscore
'格式'ZSCORE key member
'功能':返回有序集 key 中,成员 member 的 score 值。
'说明':如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。
zincrby
'格式'ZINCRBY key increment member
'功能':为有序集 key 的成员 member 的 score 值加上增量 increment 。increment 值可以是整数值或双精度浮点数。
'说明':可以通过传递一个负数值 increment ,让 score 减去相应的值。
	   当 key 不存在,或 member 不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member 。
	   当 key 不是有序集类型时,返回一个错误。命令执行成功,则返回 member 成员的新 score 值。
zrank 与 zrevrank
'格式'ZRANK key member 或 ZREVRANK key member
'功能':返回有序集 key 中成员 member 的排名。zrank 命令会按 score 值递增排序,zrevrank 命令会按 score 递减排序。
'说明':score 值最小的成员排名为 0 。如果 member 不是有序集 key 的成员,返回 nil 。
zrem
'格式'ZREM key member [member ...]
'功能':移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。
'说明':当 key 存在但不是有序集类型时,返回一个错误。执行成功,则返回被成功移除的成员的数量,不包括被忽略的成员。
zremrangebyrank
'格式'ZREMRANGEBYRANK key start stop
'功能':移除有序集 key 中,指定排名(rank)区间内的所有成员。
'说明':排名区间分别以下标参数 start 和 stop 指出,包含 start 和 stop 在内。
	   排名区间参数从 0 开始,即 0 表示排名第一的成员, 1 表示排名第二的成员,以此类推。也可以使用负数表示,-1 表示最后一个成员,-2 表示倒数第二个成员,
	   以此类推。命令执行成功,则返回被移除成员的数量。
zremrangebyscore
'格式'ZREMRANGEBYSCORE key min max
'功能':移除有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或max )的成员。
'说明':命令执行成功,则返回被移除成员的数量。
zrangebylex
'格式'ZRANGEBYLEX key min max [LIMIT offset count]
'功能':该命令仅适用于集合中所有成员都具有相同分值的情况。当有序集合的所有成员都具有相同的分值时,有序集合的元素会根据成员的字典序(lexicographical ordering)来进行排序。
	   即这个命令返回给定集合中元素值介于 min 和 max 之间的成员。如果有序集合里面的成员带有不同的分值, 那么命令的执行结果与 zrange key 效果相同。
'说明':合法的 min 和 max 参数必须包含左小括号“(”或左中括号“[”,其中左小括号“(”表示开区间, 而左中括号“[”则表示闭区间。
	   min 或 max 也可使用特殊字符“+”和“-”,分别表示正无穷大与负无穷大。
zlexcount
'格式'ZLEXCOUNT key min max
'功能':该命令仅适用于集合中所有成员都具有相同分值的情况。
	   该命令返回该集合中元素值本身(而非 score 值)介于 min 和 max 范围内的元素数量。
zremrangebylex
'格式'ZREMRANGEBYLEX key min max
'功能':该命令仅适用于集合中所有成员都具有相同分值的情况。
	   该命令会移除该集合中元素值本身介于 min 和 max 范围内的所有元素。
应用场景

有序 Set 最为典型的应用场景就是排行榜,例如音乐、视频平台中根据播放量进行排序的排行榜;电商平台根据用户评价或销售量进行排序的排行榜等。将播放量作为 score,将作品 id 作为 member,将用户评价积分或销售量作为 score,将商家 id 作为 member。使用zincrby 增加排序 score,使用 zrevrange 获取 Top 前几名,使用 zrevrank 查询当前排名,使用zscore 查询当前排序 score 等。

benchmark 测试工具

在Redis安装完毕后会自动安装一个redis-benchmark测试工具,其是一个压力测试工具, 用于测试 Redis 的性能。

在这里插入图片描述

通过 redis-benchmark –help 命令可以查看到其用法:

在这里插入图片描述

其选项 options 非常多,下面通过例子来学习常用的 options 的用法。

测试 1
命令解析

image-20230808211159592

以上命令中选项的意义:
	-h:指定要测试的 RedisIP,若为本机,则可省略
	-p:指定要测试的 Redis 的 port,若为 6379,则可省略
	-c:指定模拟有客户端的数量,默认值为 50
	-n:指定这些客户端发出的请求的总量,默认值为 100000
	-d:指定测试 get/set 命令时其操作的 value 的数据长度,单位字节,默认值为 3。在测试其它命令时该指定没有用处。

以上命令的意义是,使用 100 个客户端连接该 Redis,这些客户端总共会发起 100000 个请求,set/get 的 value 为 8 字节数据。

测试结果分析

该命令会逐个测试所有 Redis 命令,每个命令都会给出一份测试报告,每个测试报告由四部分构成:

测试环境报告

首先就是测试环境:在这里插入图片描述

延迟百分比分布

这是按照百分比进行的统计报告:每完成一次剩余测试量的 50%就给出一个统计数据。

在这里插入图片描述

延迟的累积分布

这是按照时间间隔统计的报告:基本是每 0.1 毫秒统计一次。

在这里插入图片描述

总述报告

这是总述性报告。一般看这个就够了

image-20230809183947132

测试 2

image-20230809184100675

以上命令中选项的意义:
	-t:指定要测试的命令,多个命令使用逗号分隔,不能有空格
	-q:指定仅给出总述性报告

简单动态字符串 SDS

SDS 简介
无论是 Redis 的 Key 还是 Value,其基础数据类型都是字符串。
例如,Hash 型 Value 的field 与 value 的类型、List 型、Set 型、ZSet 型 Value 的元素的类型等都是字符串。
虽然 Redis是使用标准 C 语言开发的,但并没有直接使用 C 语言中传统的字符串表示,而是自定义了一种字符串。 	
这种字符串本身的结构比较简单,但功能却非常强大,称为简单动态字符串,Simple Dynamic String,简称 SDS。
注意,Redis 中的所有字符串并不都是 SDS,也会出现 C 字符串。C 字符串只会出现在字符串“字面常量”中,并且该字符串不可能发生变更。
redisLog(REDIS_WARNNING, “sdfsfsafsafds”);
SDS 结构
	SDS 不同于 C 字符串。C 字符串本身是一个以双引号括起来,以空字符’\0’结尾的字符序列。
	但 SDS 是一个结构体,定义在 Redis 安装目录下的 src/sds.h 中:
	
	
struct sdshdr {
	// 字节数组,用于保存字符串
	char buf[];
	// buf[]中已使用字节数量,称为 SDS 的长度
	int len;
	// buf[]中尚未使用的字节数量
	int free;
}

例如执行 SET country “China”命令时,键 country 与值”China”都是 SDS 类型的,只不过 一个是 SDS 的变量,一个是 SDS 的字面常量。”China”在内存中的结构如下:image-20230809184524316

SDS 的优势
	C 字符串使用 Len+1 长度的字符数组来表示实际长度为 Len 的字符串,字符数组最后以 空字符’\0’结尾,表示字符串结束。
	这种结构简单,但不能满足 Redis 对字符串功能性、安全 性及高效性等的要求。
(1)防止”字符串长度获取”性能瓶颈
	对于 C 字符串,若要获取其长度,则必须要通过遍历整个字符串才可获取到的。对于超 长字符串的遍历,会成为系统的性能瓶颈。 
	但,由于 SDS 结构体中直接就存放着字符串的长度数据,
	所以对于获取字符串长度需要 消耗的系统性能,与字符串本身长度是无关的,不会成为 Redis 的性能瓶颈。
(2)保障二进制安全
	C 字符串中只能包含符合某种编码格式的字符,
	例如 ASCII、UTF-8 等,并且除了字符串末尾外,其它位置是不能包含空字符’\0’的,否则该字符串就会被程序误解为提前结束。
	而在图片、音频、视频、压缩文件、office 文件等二进制数据中以空字符’\0’作为分隔符的情况是很常见的。
	故而在 C 字符串中是不能保存像图片、音频、视频、压缩文件、office 文件等二进制数据的。
	但 SDS 不是以空字符’\0’作为字符串结束标志的,其是通过 len 属性来判断字符串是否结束的。
	所以,对于程序处理 SDS 中的字符串数据,无需对数据做任何限制、过滤、假设,只需读取即可。数据写入的是什么,读到的就是什么。
(3)减少内存再分配次数
	SDS 采用了空间预分配策略与惰性空间释放策略来避免内存再分配问题。
	空间预分配策略是指,每次 SDS 进行空间扩展时,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,以减少内存再分配次数。
	而额外分配的未使用空间大小取决于 空间扩展后 SDS 的 len 属性值。
			如果 len 属性值小于 1M,那么分配的未使用空间 free 的大小与 len 属性值相同。 
			如果 len 属性值大于等于 1M ,那么分配的未使用空间 free 的大小固定是 1M。
	SDS 对于空间释放采用的是惰性空间释放策略。该策略是指,SDS 字符串长度如果缩短,那么多出的未使用空间将暂时不释放,而是增加到 free 中。以使后期扩展 SDS 时减少内存再分配次数。
	如果要释放 SDS 的未使用空间,则可通过 sdsRemoveFreeSpace()函数来释放。
(4)兼容 C 函数
	Redis 中提供了很多的 SDSAPI,以方便用户对 Redis 进行二次开发。
	为了能够兼容 C函数,SDS 的底层数组 buf[]中的字符串仍以空字符’\0’结尾。
	现在要比较的双方,一个是 SDS,一个是 C 字符串,此时可以通过 C 语言函数 strcmp(sds_str->buf,c_str)
常用的 SDS 操作函数

image-20230809185327354

image-20230809185335614

集合的底层实现原理

​ Redis 中对于 Set 类型的底层实现,直接采用了 hashTable。但对于 Hash、ZSet、List 集 合的底层实现进行了特殊的设计,使其保证了 Redis 的高性能。

两种实现的选择
	对于Hash与ZSet集合,其底层的实现实际有两种:压缩列表zipList,与跳跃列表skipList。
	这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。
	只有同时满足以配置文件 redis.conf 中相关集合元素数量阈值与元素大小阈值两个条件,使用的就是压缩列表 zipList,
	只要有一个条件不满足使用的就是跳跃列表 skipList。例如,对于ZSet 集合中这两个条件如下:
		集合元素个数小于 redis.conf 中 zset-max-ziplist-entries 属性的值,其默认值为 128 字节
		每个集合元素大小都小于 redis.conf 中 zset-max-ziplist-value 属性的值,其默认值为 64 字节
zipList

image-20230809190254423

什么是 zipList
	zipList,通常称为压缩列表,是一个经过特殊编码的用于存储字符串或整数的双向链表。
	其底层数据结构由三部分构成:head、entries 与 end。这三部分在内存上是连续存放的。
head
head 又由三部分构成:
	'zlbytes':占 4 个字节,用于存放 zipList 列表整体数据结构所占的字节数,包括 zlbytes本身的长度。
	'zltail':占 4 个字节,用于存放 zipList 中最后一个 entry 在整个数据结构中的偏移量(字节)。该数据的存在可以快速定位列表的尾 entry 位置,以方便操作。
	'zllen':占 2 字节,用于存放列表包含的 entry 个数。由于其只有 16 位,所以 zipList 最多可以含有的 entry 个数为 2^16-1 = 65535 个。
entries
entries 是真正的列表,由很多的列表元素 entry 构成。
由于不同的元素类型、数值的不同,从而导致每个 entry 的长度不同。每个 entry 由三部分构成:
	'prevlength':该部分用于记录上一个 entry 的长度,以实现逆序遍历。
				  默认长度为 1 字节,只要上一个 entry 的长度<254 字节,prevlength 就占 1 字节,
				  否则其会自动扩展为 3 字节长度。
	'encoding':该部分用于标志后面的 data 的具体类型。
				如果 data 为整数类型,encoding固定长度为 1 字节。
				如果 data 为字符串类型,则 encoding 长度可能会是 1 字节、2 字节或 5 字节。	
				data 字符串不同的长度,对应着不同的 encoding 长度。
	'data':真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字节长度不同。
end

end 只包含一部分,称为 zlend。占 1 个字节,值固定为 255,即二进制位为全 1,表示 一个 zipList 列表的结束。

listPack
	对于 ziplist,实现复杂,为了逆序遍历,每个 entry 中包含前一个 entry 的长度,这样会 导致在 ziplist 中间修改或者插入 entry 时需要进行级联更新。
	在高并发的写操作场景下会极 度降低 Redis 的性能。为了实现更紧凑、更快的解析,更简单的实现,重写实现了 ziplist, 并命名为 listPack。
	在 Redis 7.0 中,已经将 zipList 全部替换为了 listPack,但为了兼容性,在配置中也保留了 zipList 的相关属性。

image-20230809190843958

什么是 listPack
	listPack 也是一个经过特殊编码的用于存储字符串或整数的双向链表。
	其底层数据结构也由三部分构成:head、entries 与 end,且这三部分在内存上也是连续存放的。
	listPack与zipList的重大区别在head与每个entry的结构上,表示列表结束的end与zipList的 zlend 是相同的,占一个字节,且 8 位全为 1。
head
head 由两部分构成:
	'totalBytes':占 4 个字节,用于存放 listPack 列表整体数据结构所占的字节数,包括totalBytes 本身的长度。
	'elemNum':占 2 字节,用于存放列表包含的 entry 个数。其意义与 zipList 中 zllen 的相同。
与 zipList 的 head 相比,没有了记录最后一个 entry 偏移量的 zltail。
entries
entries 也是 listPack 中真正的列表,由很多的列表元素 entry 构成。
由于不同的元素类型、数值的不同,从而导致每个 entry 的长度不同。
但与 zipList 的 entry 结构相比,listPack的 entry 结构发生了较大变化。
其中最大的变化就是没有了记录前一个 entry 长度的 prevlength,而增加了记录当前entry 长度的 element-total-len。
而这个改变仍然可以实现逆序遍历,但却避免了由于在列表中间修改或插入 entry 时引发的级联更新。
每个 entry 仍由三部分构成:
	'encoding':该部分用于标志后面的 data 的具体类型。
				如果 data 为整数类型,encoding长度可能会是 123459 字节。
				如果 data为字符串类型,则 encoding 长度可能会是 125 字节。
				data 字符串不同的长度,对应着不同的 encoding 长度。
	'data':真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字节长度不同。
	'element'-total-len:该部分用于记录当前 entry 的长度,用于实现逆序遍历。
					     由于其特殊的记录方式,使其本身占有的字节数据可能会是 12345 字节。
skipList
什么是 skipList
	skipList,跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单,查找效率较高。
	简单来说跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能。
	也正是这个跳跃功能,使得在查找元素时,能够提供较高的效率。
skipList 原理

假设有一个带头尾结点的有序链表。image-20230809191204764

image-20230809191216845

存在的问题
这种对链表分层级的方式从原理上看确实提升了查找效率,
但在实际操作时就出现了问题:由于固定序号的元素拥有固定层级,所以列表元素出现增加或删除的情况下,会导致列表整体元素层级大调整,但这样势必会大大降低系统性能。
例如,对于划分两级的链表,可以规定奇数结点为高层级链表,偶数结点为低层级链表。
对于划分三级的链表,可以按照节点序号与 3 取模结果进行划分。
但如果插入了新的节点,或删除的原来的某些节点,那么定会按照原来的层级划分规则进行重新层级划分,那么势必会大大降低系统性能。
算法优化
	为了避免前面的问题,skipList 采用了随机分配层级方式。
	即在确定了总层级后,每添加一个新的元素时会自动为其随机分配一个层级。
	这种随机性就解决了节点序号与层级间的固定关系问题。

在这里插入图片描述

	上图演示了列表在生成过程中为每个元素随机分配层级的过程。
	从这个 skiplist 的创建和插入过程可以看出,每一个节点的层级数都是随机分配的,而且新插入一个节点不会影响到其它节点的层级数。
	只需要修改插入节点前后的指针,而不需对很多节点都进行调整。这就降低了插入操作的复杂度。
	skipList 指的就是除了最下面第 1 层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针跳过了一些节点,并且越高层级的链表跳过的节点越多。
	在查找数据的时先在高层级链表中进行查找,然后逐层降低,最终可能会降到第 1 层链表来精确地确定数据位置。
	在这个过程中由于跳过了一些节点,从而加快了查找速度。
quickList

image-20230809194905875

什么是 quickList
	quickList,快速列表,quickList 本身是一个双向无循环链表,它的每一个节点都是一个zipList。
			从Redis3.2版本开始,对于List的底层实现,使用quickList替代了zipList 和 linkedList。zipList 与 linkedList 都存在有明显不足,
			而 quickList 则对它们进行了改进:吸取了 zipList 和 linkedList 的优点,避开了它们的不足。
	quickList 本质上是 zipList 和 linkedList 的混合体。
	其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。
	当然,对于每个 zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size属性可以指定。
检索操作
	为了更深入的理解 quickList 的工作原理,通过对检索、插入、删除等操作的实现分析来加深理解。
	对于 List 元素的检索,都是以其索引 index 为依据的。
	quickList 由一个个的 zipList 构成,每个 zipList 的 zllen 中记录的就是当前 zipList 中包含的 entry 的个数,即包含的真正数据元素的个数。
	根据要检索元素的 index,从 quickList 的头节点开始,逐个对 zipList 的 zllen 做 sum求和,直到找到第一个求和后 sum 大于 index 的 zipList,那么要检索的这个元素就在这个zipList 中。
插入操作
	由于 zipList 是有大小限制的,所以在 quickList 中插入一个元素在逻辑上相对就比较复杂一些。
	假设要插入的元素的大小为 insertBytes,而查找到的插入位置所在的 zipList 当前的大小为 zlBytes,那么具体可分为下面几种情况:
	'情况一':当 insertBytes + zlBytes <= list-max-ziplist-size 时,直接插入到 zipList 中相应位置即可
	'情况二':当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的首部位置,此时需要查看该 zipList 的前一个 zipList 的大小 prev_zlBytes。
		若 insertBytes + prev_zlBytes<= list-max-ziplist-size 时,直接将元素插入到前一个zipList 的尾部位置即可
		若 insertBytes + prev_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新的 zipList,并连入 quickList 中
	'情况三':当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的尾部位置,此时需要查看该 zipList 的后一个 zipList 的大小 next_zlBytes。
		若 insertBytes + next_zlBytes<= list-max-ziplist-size 时,直接将元素插入到后一个zipList 的头部位置即可
		若 insertBytes + next_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新的 zipList,并连入 quickList 中
	'情况四':当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的中间位置,
			 则将当前 zipList 分割为两个 zipList 连接入 quickList 中,然后将元素插入到分割后的前面 zipList 的尾部位置
删除操作
	对于删除操作,只需要注意一点,在相应的 zipList 中删除元素后,该 zipList 中是否还有元素。
	如果没有其它元素了,则将该 zipList 删除,将其前后两个 zipList 相连接。
key 与 value 中元素的数量
	前面讲述的 Redis 的各种特殊数据结构的设计,
	不仅极大提升了 Redis 的性能,并且还使得 Redis 可以支持的 key 的数量、集合 value 中可以支持的元素数量可以非常庞大。
	Redis 最多可以处理 232个 key(约 42 亿),并且在实践中经过测试,每个 Redis 实例至少可以处理 2.5 亿个 key。
	每个 HashListSetZSet 集合都可以包含 232 个元素。

BitMap 操作命令

BitMap基础

image-20231108134654193

'说明:用String类型作为底层数据结构实现的一种统计二值状态的数据类型'

位图本质是数组,它是基于String数据类型的按位的操作。
该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们可以称之为一个索引或者位格)。
Bitmap支持的最大位数是2^32位,它可以极大的节约存储空间,使用512M内存就可以存储多大42.9亿的字节信息(2^32 = 4294967296)
BitMapRedis 2.2.0 版本中引入的一种新的数据类型。该数据类型本质上就是一个仅包含 01 的二进制字符串。
而其所有相关命令都是对这个字符串二进制位的操作。用于描述该字符串的属性有三个:key、offset、bitValue。
	'key'BitMapRedis 的 key-value 中的一种 Value 的数据类型,所以该 Value 一定有其对
应的 key。
	'offset':每个 BitMap 数据都是一个字符串,字符串中的每个字符都有其对应的索引,该索引从 0 开始计数。
			 该索引就称为每个字符在该 BitMap 中的偏移量 offset。
			 这个 offset的值的范围是[0232-1],即该 offset 的最大值为 4G-1,即 429496729542 亿多。
	'bitValue':每个 BitMap 数据中都是一个仅包含 01 的二进制字符串,
			    每个 offset 位上的字符就称为该位的值 bitValue。bitValue 的值非 01
setbit
'格式'SETBIT key offset value
'功能':为给定 key 的BitMap 数据的 offset 位置设置值为 value。
	   其返回值为修改前该 offset位置的 bitValue
'说明':对于原 BitMap 字符串中不存在的 offset 进行赋值,字符串会自动伸展以确保它可以将 value 保存在指定的 offset 上。
	   当字符串值进行伸展时,空白位置以 0 填充。当然,设置的 value 只能是 01。
	   不过需要注意的是,对使用较大 offset 的 SETBIT 操作来说,内存分配过程可能造成 Redis 服务器被阻塞。
'bitmap的偏移量是从零开始算的'
getbit
'格式'GETBIT key offset
'功能':对 key 所储存的 BitMap 字符串值,获取指定 offset 偏移量上的位值 bitValue。
'说明':当 offset 比字符串值的长度大,或者 key 不存在时,返回 0
bitcount
'格式'BITCOUNT key [start] [end]
'功能':统计给定字符串中被设置为 1 的 bit 位的数量。一般情况下,统计的范围是给定的整个 BitMap 字符串。
	   但也可以通过指定额外的 start 或 end 参数,实现仅对指定字节范围内字符串进行统计,包括 start 和 end 在内。
	   注意,这里的 start 与 end 的单位是字节,不是 bit,并且从 0 开始计数。
'说明':start 和 end 参数都可以使用负数值: -1 表示最后一个字节, -2 表示倒数第二个字节,以此类推。
	   另外,对于不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0
bitpos
'格式'BITPOS key bit [start] [end]
'功能':返回 key 指定的 BitMap 中第一个值为指定值 bit(01) 的二进制位的位置。pos,即 position,位置。
	   在默认情况下, 命令将检测整个 BitMap,但用户也可以通过可选的 start 参数和 end 参数指定要检测的范围。
'说明':start 与 end 的意义与 bitcount 命令中的相同。
bitop
'格式'BITOP operation destkey key *key …+
'功能':对一个或多个 BitMap 字符串 key 进行二进制位操作,并将结果保存到 destkey 上。
	operation 可以是 ANDORNOTXOR 这四种操作中的任意一种:
		BITOP AND destkey key [key ...] :对一个或多个 BitMap 执行按位与操作,并将结果保存到 destkey 。
		BITOP OR destkey key [key ...] :对一个或多个 BitMap 执行按位或操作,并将结果保存到 destkey 。
		BITOP XOR destkey key [key ...] :对一个或多个 BitMap 执行按位异或操作,并将结果保存到 destkey 。
		BITOP NOT destkey key :对给定 BitMap 执行按位非操作,并将结果保存到 destkey 。
'说明':
		除了 NOT 操作之外,其他操作都可以接受一个或多个 BitMap 作为输入。
		除了 NOT 操作外,其他对一个 BitMap 的操作其实就是一个复制。
		如果参与运算的多个 BitMap 长度不同,较短的 BitMap 会以 0 作为补充位与较长BitMap 运算,且运算结果长度与较长 BitMap 的相同。
应用场景
	由于 offset 的取值范围很大,所以其一般应用于大数据量的二值性统计。
	例如平台活跃用户统计(二值:访问或未访问)、支持率统计(二值:支持或不支持)、员工考勤统计(二值:上班或未上班)、图像二值化(二值:黑或白)等。
	不过,对于数据量较小的二值性统计并不适合 BitMap,可能使用 Set 更为合适。当然,具体多少数据量适合使用 Set,超过多少数据量适合使用 BitMap,这需要根据具体场景进行具体分析。
	例如,一个平台要统计日活跃用户数量。
	如果使用 Set 来统计,只需上线一个用户,就将其用户 ID 写入 Set 集合即可,最后只需统计出 Set 集合中的元素个数即可完成统计。
	即 Set 集合占用内存的大小与上线用户数量成正比。假设用户 ID 为 m 位 bit 位,当前活跃用户数量为 n,则该 Set 集合的大小最少应该是m*n 字节。
	如果使用 BitMap 来统计,则需要先定义出一个 BitMap,其占有的 bit 位至少为注册用户数量。
	只需上线一个用户,就立即使其中一个 bit 位置 1,最后只需统计出 BitMap 中 1 的个数即可完成统计。
	即 BitMap 占用内存的大小与注册用户数量成正比,与上线用户数量无关。假设平台具有注册用户数量为 N,则 BitMap 的长度至少为 N 个 bit 位,即 N/8 字节。
	何时使用 BitMap 更合适?令 m*n 字节 = N/8 字节,即 n = N/8/m = N/(8*m) 时,使用Set 集合与使用 BitMap 所占内存大小相同。
	以淘宝为例,其用户 ID 长度为 11 位(m),其注册用户数量为 8 亿(N),当活跃用户数量为 8 亿/(8*11) = 0.09 亿 = 9*106= 900 万,使用 Set与 BitMap 占用的内存是相等的。
	但淘宝的日均活跃用户数量为 8 千万,所以淘宝使用 BitMap更合适。

京东领京东

需求说明
签到日历仅展示当月签到数据
签到日历需展示最近连续签到天数
假设当前日期是20210618,且20210616未签到
若20210617已签到且0618未签到,则连续签到天数为1
若20210617已签到且0618已签到,则连续签到天数为2
连续签到天数越多,奖励越大
所有用户均可签到
截至2020年3月31日的12个月,京东年度活跃用户数3.87亿,同比增长24.8%,环比增长超2500万,
此外,2020年3月移动端日均活跃用户数同比增长46%假设10%左右的用户参与签到,签到用户也高达3千万。。。。。。o(╥﹏╥)o
小厂方法(mysql)
建表SQL
CREATE TABLE user_sign
(
  keyid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  user_key VARCHAR(200),#京东用户ID
  sign_date DATETIME,#签到日期(20210618)
  sign_count INT #连续签到天数
)
 
INSERT INTO user_sign(user_key,sign_date,sign_count)
VALUES ('20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx','2020-06-18 15:11:12',1);
 
SELECT
    sign_count
FROM
    user_sign
WHERE
    user_key = '20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    AND sign_date BETWEEN '2020-06-17 00:00:00' AND '2020-06-18 23:59:59'
ORDER BY
    sign_date DESC
    LIMIT 1;
困难和解决思路
方法正确但是难以落地实现,o(╥﹏╥)o。 
签到用户量较小时这么设计能行,但京东这个体量的用户(估算3000W签到用户,一天一条数据,一个月就是9亿数据)
对于京东这样的体量,如果一条签到记录对应着当日用记录,那会很恐怖......

如何解决这个痛点?
1 一条签到记录对应一条记录,会占据越来越大的空间。
2 一个月最多31天,刚好我们的int类型是32位,那这样一个int类型就可以搞定一个月,32位大于31天,当天来了位是1没来就是0。
3 一条数据直接存储一个月的签到记录,不再是存储一天的签到记录。
大厂方法(Bitmaps)
在签到统计时,每个用户一天的签到用1个bit位就能表示,
一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型

HyperLogLog 操作命令

'UV'
Unique Visitor 独立访客,一般理解为客户端IP
'需要慎重去考虑'

'PV'
Page View 页面浏览器
不用去重

'DAU'
Daily Actice User 日活跃用户量  登录或者使用了某个产品的用户数(去重复登录的用户)
常用于反应网站、互联网应用或者网络游戏的运营情况

'MAU'
Monthly Active User 月活跃用户量

HyperLogLog基础

HyperLogLogRedis 2.8.9 版本中引入的一种新的数据类型,其意义是 hyperlog log,超级日志记录。
该数据类型'可以简单理解为一个 set 集合',集合元素为字符串。但实际上HyperLogLog 是一种基数计数概率算法,
通过该算法可以利用极小的内存完成独立总数的统计,去重后的真实个数。其所有相关命令都是对这个'set 集合'的操作。 

image-20231108112912545

pfadd
'格式'PFADD key element *element …+
'功能':将任意数量的元素添加到指定的 HyperLogLog 集合里面。如果内部存储被修改了返回 1,否则返回 0
pfcount
'格式'PFCOUNT key *key …+
'功能':该命令作用于单个 key 时,返回给定 key 的 HyperLogLog 集合的近似基数;
	   该命令作用于多个 key 时,返回所有给定 key 的 HyperLogLog 集合的并集的近似基数;
	   如果key 不存在,则返回 0
pfmerge
'格式'PFMERGE destkey sourcekey *sourcekey …+
'功能':将多个 HyperLogLog 集合合并为一个 HyperLogLog 集合,并存储到 destkey 中,
	   合并后的 HyperLogLog 的基数接近于所有 sourcekey 的 HyperLogLog 集合的并集。

image-20231108113034336

HyperLogLog应用

去重方法
//一些去重的方法

'HashSet去重'
List<String> list;
HashSet<String> set = new HashSet<>(list);

'bitmap'
如果数据显较大亿级统计,使用bitmaps同样会有这个问题。
bitmap是通过用位bit数组来表示各元素是否出现,每个元素对应一位,所需的总内存为N个bit。
基数计数则将每一个元素对应到bit数组中的其中一位,比如bit数组010010101(按照从零开始下标,有的就是1468)。
新进入的元素只需要将已经有的bit数组和新加入的元素进行按位或计算就行。这个方式能大大减少内存占用且位操作迅速。
But,假设一个样本案例就是一亿个基数位值数据,一个样本就是一亿
如果要统计1亿个数据的基数位值,大约需要内存100000000/8/1024/1024约等于12M,内存减少占用的效果显著。
这样得到统计一个对象样本的基数值需要12M。
如果统计10000个对象样本(1w个亿级),就需要117.1875G将近120G,可见使用bitmaps还是不适用大数据量下(亿级)的基数计数场景,但是bitmaps方法是精确计算的。
    
'这两个方法的结论':样本元素越多内存消耗急剧增大,难以管控 + 各种慢,对于亿级统计不太合适,大数据害死人。量变引起质变
   
'使用概率算法解决这个问题'
'通过牺牲准确率来换取空间',对于不要求绝对准确率的场景下可以使用,
因为'概率算法不直接存储数据本身',通过一定的概率统计方法预估基数值,同时保证误差在一定范围内,由于又不储存数据故此可以大大节约内存。
'HyperLogLog就是一种概率算法的实现'。
概率算法原理
'只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容'
HyperLogLog 可对数据量超级庞大的日志数据做不精确的去重计数统计。
当然,'这个不精确的度在 Redis 官方给出的误差是 0.81%'。这个误差对于大多数超大数据量场景是被允许的。
对于平台上每个页面每天的 UV 数据,非常适合使用 HyperLogLog 进行记录。

淘宝亿级UV方案

需求
UV的统计需要去重,一个用户一天内的多次访问只能算一次
淘宝、天猫首页的UV,平均每天是1~1.5个亿
没填存1.5个亿的IP,访问者来了后先去查是否存在,不存在加入
方案讨论
mysql
傻X,o(╥﹏╥)o,不解释
redis的hash
redis——hash = <keyDay,<ip,1>>
按照ipv4的结构来说明,每个ipv4的地址最多是15个字节(ip = "192.168.111.1",最多xxx.xxx.xxx.xxx)
某一天的1.5亿 * 15个字节= 2G,一个月60G,redis死定了。o(╥﹏╥)o
hyperloglog

为什么是只需要花费12Kb?

image-20231108114939857

image-20231108114953646

HyperLogLogService
@Service
@Slf4j
public class HyperLogLogService
{
    @Resource
    private RedisTemplate redisTemplate;
    /**
     * 模拟后台有用户点击首页,每个用户来自不同ip地址
     */
    @PostConstruct
    public void init(){
        log.info("------模拟后台有用户点击首页,每个用户来自不同ip地址");
        new Thread(() -> {
            String ip = null;
            for (int i = 1; i <=200; i++) {
                Random r = new Random();
                ip = r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256);
                Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip);
                log.info("ip={},该ip地址访问首页的次数={}",ip,hll);
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        },"t1").start();
    }
}
HyperLogLogController
@ApiOperation("获得IP去重后的首页访问量")
@RequestMapping(value = "/uv",method = RequestMethod.GET)
public long uv(){
    //pfcount
   return redisTemplate.opsForHyperLogLog().size("hll");
}

Geospatial 操作命令

Geospatial基础

	Geospatial,地理空间。
	Redis3.2 版本中引入了 Geospatial 这种新的数据类型。
	该类型本质上仍是一种集合,只不过集合元素比较特殊,是一种由三部分构成的数据结构,这种数据结构称为空间元素:
		'经度':longitude。有效经度为[-180180]。正的表示东经,负的表示西经。
		'纬度':latitude。有效纬度为[-85.0511287885.05112878]。正的表示北纬,负的表示南纬。
		'位置名称':为该经纬度所标注的位置所命名的名称,也称为该 Geospatial 集合的空间元素名称。
	通过该类型可以设置、查询某地理位置的经纬度,查询某范围内的空间元素,计算两空间元素间的距离等。
geoadd
'格式'GEOADD key longitude latitude member *longitude latitude member …+
'功能':将一到多个空间元素添加到指定的空间集合中。
'说明':当用户尝试输入一个超出范围的经度或者纬度时,该命令会返回一个错误。

GEOADD city 116.403963 39.915119 "天安门" 116.403414 39.924091 "故宫" 116.024067 40.362639 "长城"
解决中文乱码  reids-cli -- raw

image-20231108115503733

geopos
'格式'GEOPOS key member *member …+
'功能':从指定的地理空间中返回指定元素的位置,即经纬度。
'说明':因为 该命令接受可变数量元素作为输入,所以即使用户只给定了一个元素,命令也会返回数组。
    
GEOPOS city 天安门 故宫

image-20231108115734433

geodist
'格式'GEODIST key member1 member2 [unit]
'功能':返回两个给定位置之间的距离。其中 unit 必须是以下单位中的一种:
	m :米,默认
	km :千米
	mi :英里
	ft:英尺
'说明':如果两个位置之间的其中一个不存在, 那么命令返回空值。
	   另外,在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。
    
后面参数是距离单位:	m 米 	km 千米	ft 英尺	mi 英里
GEODIST city 天安门 长城 km

image-20231108133712930

geohash
'格式'GEOHASH key member *member …+
'功能':返回一个或多个位置元素的 Geohash 值。
'说明'GeoHash 是一种地址编码方法。他能够把二维的空间经纬度数据编码成一个字符串。
	   该值主要用于底层应用或者调试, 实际中的作用并不大。
	geohash算法生成的base32编码值
	3维变2维变1维,主要分为三步,将三维的地球变为二维的坐标,在将二维的坐标转换为一维的点块,
	最后将一维的点块,最后将一维的点快转换为二维再通过base32拜编码
GEOHASH city 天安门 故宫 长城

image-20231108115814882

georadius
'格式'GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
'功能':以给定的经纬度为中心,返回指定地理空间中包含的所有位置元素中,与中心距离不超过给定半径的元素。返回时还可携带额外的信息:
	WITHDIST :在返回位置元素的同时,将位置元素与中心之间的距离也一并返回。距离的单位和用户给定的范围单位保持一致。
	WITHCOORD :将位置元素的经维度也一并返回。
	WITHHASH:将位置元素的 Geohash 也一并返回,不过这个 hash 以整数形式表示命令默认返回未排序的位置元素。 
	通过以下两个参数,用户可以指定被返回位置元素的排序方式:
		ASC :根据中心的位置,按照从近到远的方式返回位置元素。
		DESC :根据中心的位置,按照从远到近的方式返回位置元素。
'说明':在默认情况下, 该命令会返回所有匹配的位置元素。
	   虽然用户可以使用 COUNT <count> 选项去获取前 N 个匹配元素,但因为命令在内部可能会需要对所有被匹配的元素进行处理,
	   所以在对一个非常大的区域进行搜索时,即使使用 COUNT 选项去获取少量元素,该命令的执行速度也可能会非常慢。
georadiusbymember
'格式'GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
'功能':这个命令和 GEORADIUS 命令一样,都可以找出位于指定范围内的元素,
	   但该命令的中心点是由位置元素形式给定的,而不是像 GEORADIUS 那样,使用输入的经纬度来指定中心点。
'说明':返回结果中也是包含中心点位置元素的

image-20231108133758429

美团位置附近酒店

需求分析
美团app附近的酒店
摇个妹子,附近的妹子
高德地图附近的人或者一公里以内的各种营业厅、加油站、理发店、超市。。。
找个单车
编码实现
georadius:以给定的纬度为中心,找出某一半径内的元素
GeoController
@Api(tags = "美团地图位置附近的酒店推送GEO")
@RestController
@Slf4j
public class GeoController
{
    @Resource
    private GeoService geoService;

    @ApiOperation("添加坐标geoadd")
    @RequestMapping(value = "/geoadd",method = RequestMethod.GET)
    public String geoAdd()
    {
        return geoService.geoAdd();
    }

    @ApiOperation("获取经纬度坐标geopos")
    @RequestMapping(value = "/geopos",method = RequestMethod.GET)
    public Point position(String member)
    {
        return geoService.position(member);
    }

    @ApiOperation("获取经纬度生成的base32编码值geohash")
    @RequestMapping(value = "/geohash",method = RequestMethod.GET)
    public String hash(String member)
    {
        return geoService.hash(member);
    }

    @ApiOperation("获取两个给定位置之间的距离")
    @RequestMapping(value = "/geodist",method = RequestMethod.GET)
    public Distance distance(String member1, String member2)
    {
        return geoService.distance(member1,member2);
    }

    @ApiOperation("通过经度纬度查找北京王府井附近的")
    @RequestMapping(value = "/georadius",method = RequestMethod.GET)
    public GeoResults radiusByxy()
    {
        return geoService.radiusByxy();
    }

    @ApiOperation("通过地方查找附近,本例写死天安门作为地址")
    @RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
    public GeoResults radiusByMember()
    {
        return geoService.radiusByMember();
    }
}
GeoService
@Service
@Slf4j
public class GeoService
{
    public static final String CITY ="city";

    @Autowired
    private RedisTemplate redisTemplate;

    public String geoAdd()
    {
        Map<String, Point> map= new HashMap<>();
        map.put("天安门",new Point(116.403963,39.915119));
        map.put("故宫",new Point(116.403414 ,39.924091));
        map.put("长城" ,new Point(116.024067,40.362639));

        redisTemplate.opsForGeo().add(CITY,map);

        return map.toString();
    }

    public Point position(String member) {
        //获取经纬度坐标
        List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
        return list.get(0);
    }


    public String hash(String member) {
        //geohash算法生成的base32编码值
        List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);
        return list.get(0);
    }


    public Distance distance(String member1, String member2) {
        //获取两个给定位置之间的距离
        Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
        return distance;
    }

    public GeoResults radiusByxy() {
        //通过经度,纬度查找附近的,北京王府井位置116.418017,39.914402
        Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier());
        //返回50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args);
        return geoResults;
    }

    public GeoResults radiusByMember() {
        //通过地方查找附近
        String member="天安门";
        //返回50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
        //半径10公里内
        Distance distance=new Distance(10, Metrics.KILOMETERS);
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);
        return geoResults;
    }
}

发布/订阅命令

消息系统

在这里插入图片描述

subscribe
'格式'SUBSCRIBE channel *channel …+
'功能'Redis 客户端通过一个 subscribe 命令可以同时订阅任意数量的频道。在输出了订阅了主题后,命令处于阻塞状态,等待相关频道的消息。
psubscribe
'格式'PSUBSCRIBE pattern *pattern …+
'功能':订阅一个或多个符合给定模式的频道。
'说明':这里的模式只能使用通配符 *。例如,it* 可以匹配所有以 it 开头的频道,
		像 it.news、it.blog、it.tweets 等;news.*可以匹配所有以 news.开头的频道,像 news.global.today、news.it 等。
publish
'格式'PUBLISH channel message
'功能'Redis 客户端通过一条 publish 命令可以发布一个频道的消息。返回值为接收到该消息的订阅者数量。
unsubscribe
'格式'UNSUBSCRIBE *channel *channel …++
'功能'Redis 客户端退订指定的频道。
'说明':如果没有频道被指定,也就是一个无参数的 UNSUBSCRIBE 命令被执行,那么客户端使用 SUBSCRIBE 命令订阅的所有频道都会被退订。在这种情况下,命令会返回一个信息,告知客户端所有被退订的频道。
punsubscribe
'格式'PUNSUBSCRIBE *pattern *pattern …++
'功能':退订一个或多个符合给定模式的频道。
'说明':这里的模式只能使用通配符 *。如果没有频道被指定,其效果与 SUBSCRIBE 命令相同,客户端将退订所有订阅的频道。
pubsub
'格式'PUBSUB <subcommand> [argument *argument …++
'功能'PUBSUB 是一个查看订阅与发布系统状态的内省命令集,它由数个不同格式的子命令组成,下面分别介绍这些子命令的用法。
pubsub channels
'格式'PUBSUB CHANNELS [pattern]
'功能':列出当前所有的活跃频道。活跃频道指的是那些至少有一个订阅者的频道。
'说明':pattern 参数是可选的。如果不给出 pattern 参数,将会列出订阅/发布系统中的所有活跃频道。如果给出 pattern 参数,那么只列出和给定模式 pattern 相匹配的那些活跃频道。pattern 中只能使用通配符*
pubsub numsub
'格式'PUBSUB NUMSUB [channel-1 … channel-N]
'功能':返回给定频道的订阅者数量。不给定任何频道则返回一个空列表。
pubsub numpat
'格式'PUBSUB NUMPAT
'功能':查询当前 Redis 所有客户端订阅的所有频道模式的数量总和
基于List实现消息队列

image-20230529114031939

Redis的list数据结构是一个双向链表,很容易模拟出队列效果。队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实 现。当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞 并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。

基于List的消息队列有哪些优缺点?

优点: 
​	利用Redis存储,不受限于JVM内存上限 
​	基于Redis的持久化机制,数据安全性有保证 
​	可以满足消息有序性 
缺点: 
​	无法避免消息丢失 
​	只支持单消费者
基于PubSub的消息队列

消费者可以订阅一个或多个 channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。

image-20230529114048136

基于PubSub的消息队列有哪些优缺点?

优点: 
​	采用发布订阅模型,支持多生产、多消费 
缺点: 
​	不支持数据持久化 
​	无法避免消息丢失 
​	消息堆积有上限,超出时数据丢失
基于Stream的消息队列
发送消息

image-20230529114220997

image-20230529114714760

读取消息

image-20230529133249996

image-20230529133346641

注意:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条 以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题

STREAM类型消息队列的XREAD命令特点:
​		STREAM类型消息队列的XREAD命令特点:
​		一个消息可以被多个消费者读取
​		可以阻塞读取
​		有消息漏读的风险
基于Stream的消息队列-消费者组

在这里插入图片描述

创建消费者组
XGROUP CREATE key groupName ID [MKSTREAM]

key:队列名称
groupName:消费者组名称
ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
MKSTREAM:队列不存在时自动创建队列

删除指定的消费者组

XGROUP DESTORY key groupName

给指定的消费者组添加消费者

XGROUP CREATECONSUMER key groupname consumername

删除消费者组中的指定消费者

XGROUP DELCONSUMER key groupname consumername
从消费者组读取消息
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]

group:消费组名称
consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
count:本次查询的最大数量
BLOCK milliseconds:当没有消息时最长等待时间
NOACK:无需手动ACK,获取到消息后自动确认
STREAMS key:指定队列名称
ID:获取消息的起始ID
STREAM类型消息队列的XREADGROUP命令特点:
​		消息可回溯
​		可以多消费者争抢消息,加快消费速度
​		可以阻塞读取
​		没有消息漏读的风险
​		有消息确认机制,保证消息至少被消费一次
对比

image-20230529134019849

Feed流

image-20230529134454781

Feed流产品有两种常见模式:

'Timeline':不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈 

​	优点:信息全面,不会有缺失。并且实现也相对简单 

​	缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

'智能排序':利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

​	优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷

​	缺点:如果算法不精准,可能起到反作用

Timeline模式

该模式的实现方案有三种:
​	拉模式
​	推模式
​	推拉结合
拉模式:也叫做读扩散
该模式的核心含义就是:当张三和李四和王五发了消息后,都会保存在自己的邮箱中,假设赵六要读取 信息,那么他会从读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行 拉取,然后在进行排序

​	优点:比较节约空间,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行 清楚。

​	缺点:比较延迟,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么 此时就会拉取海量的内容,对服务器压力巨大。

image-20230529134828256

推模式:也叫做写扩散。
推模式是没有写邮箱的,当张三写了一个内容,此时会主动的把张三写的内容发送到他的粉丝收件箱中 去,假设此时李四再来读取,就不用再去临时拉取了

​	优点:时效快,不用临时拉取

​	缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去

image-20230529134940285

推拉结合模式

也叫做读写混合,兼具推和拉两种模式的优点

推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直 接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大 V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现 在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来, 而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。

image-20230529135136651

feed分页

Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。

image-20230529135542289

举个例子:我们从t1时刻开始,拿第一页数据,拿到了10~6,然后记录下当前最后一次拿取的记录,就 是6,t2时刻发布了新的记录,此时这个11放到最顶上,但是不会影响我们之前记录的6,此时t3时刻来 拿第二页,第二页这个时候拿数据,还是从6后一点的5去拿,就拿到了5-1的记录。我们这个地方可以采 用sortedSet来做,可以进行范围查询,并且还可以记录当前获取数据时间戳最小值,就可以实现滚动分 页了

image-20230529135712671

BitMap

Redis中是利用string类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是 2^32个bit位。

image-20230529140816347

一个字节是八位,超过一个字节就会扩容,0也算占的字节

用String类型作为底层数据结构实现的一种统计二值状态的数据类型,位图的本质是数组,它是基于String数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们称之为一个索引)。

BitMap的操作命令有:
​	SETBIT:向指定位置(offset)存入一个0或1
​   GETBIT :获取指定位置(offset)的bit值
​	BITCOUNT :统计BitMap中值为1的bit位的数量
​	BITFIELD :操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
​	BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回
​	BITOP :将多个BitMap的结果做位运算(与 、或、异或)
​	BITPOS :查找bit数组中指定范围内第一个0或1出现的位置
setbit key offset val  			给指定key的值的第offset复制val
getbit key offset 	   			获取指定key的第offset位
bitcount key start end 			返回指定key中【start , end】中为1的数量
bitop operation destkey key 	对不同的二进制存储数据进行位运算(AND,OR,NOT,XOR)


Bitmap的偏移量是从零开始算的

BigKey

MoreKey案例

大批量插入
# 生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;

通过redis提供的管道 --pipe命令插入100w大批量数据
结合自己机器的地址:
cat /tmp/redisTest.txt | /opt/redis-7.0.0/src/redis-cli -h 127.0.0.1 -p 6379 -a 111111 --pipe
多出来的5条,是之前阳哥自己的其它测试数据,100w数据插入redis花费5.8秒左右
 
 
 key * 这个指令有致命的弊端,阻塞执行,100w 数据 ,需要33.66s 
 flushdb 需要是2.16s
 
限制命令

生产上限制keys/flushdb/flushall等危险命令以防止误删误用*

通过配置设置禁用这些命令,redis.conf在SECURITY这一项种

在这里插入图片描述

*不用keys 避免卡顿,那该用什么—scan

类似于mysql limit,但不完全相同

Scan命令用于迭代数据库种的数据库键

在这里插入图片描述

image-20230604120814679

在这里插入图片描述

BigKey案例

多大算Big

在这里插入图片描述

String是value,最大512MB但是 ≥ 10KB就是bigkey

list、hash、set和zset,个数超过5000就是bigKey

哪些危害
	内存不均,集群迁移困难
​	超时删除,大key删除作梗
​	网络流量阻塞
如何发现
redis-cli --bigkeys

image-20230604122438430

MEMORY USAGE

在这里插入图片描述

如何删除
String

一般用del,如果过于庞大unlink

hash

使用hscan每次获取少量field-value,在使用hdel删除每个field

image-20230604122722276

image-20230604122749601

list

使用ltrim渐进式逐步删除,知道全部删除完成

image-20230604164552407

image-20230604164611546

set

使用sscan每次获取部分元素,在使用srem命令删除每个元素

image-20230604164711585

image-20230604164723050

zset

使用zscan每次获取部分元素,在使用ZREMRANGEBYRANK命令删除每个元素

image-20230604164820238

image-20230604164831253

BigKey生产调优

redis.conf配置文件LAZY FREEING相关说明

阻塞和非阻塞删除命令

image-20230604165009737

优化配置

image-20230604165024258

UV统计

	UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天 内同一个用户多次访问该网站,只记录1次。

​	PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户 多次打开页面,则记录多次PV。往往用来衡量网站的流量。

Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用低的令人发指!作为 代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。

image-20230529141504004

image-20230529141514909

Redis 事务

可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他的命令插入,不许加塞

Redis事务 VS 数据库事务

image-20230615085236492

Redis 事务特性
Redis 的事务仅保证了数据的一致性,不具有像 DBMS 一样的 ACID 特性。
	这组命令中的某些命令的执行失败不会影响其它命令的执行,不会引发回滚。即不具备原子性。
	这组命令通过乐观锁机制实现了简单的隔离性。没有复杂的隔离级别。
	这组命令的执行结果是被写入到内存的,是否持久取决于 Redis 的持久化策略,与事务无关。
Redis 事务实现
命令
Redis 事务通过命令进行控制。
​	muti:开启事务,redis会将后续的命令逐个放入队列中,标记一个事务块的开始
​	exec:执行事务,执行事务中的所有操作命令。
​	discard:取消事务,放弃执行事务块中的所有命令。
​	WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令​
case1:正常执行
MULTI
EXEC   -----   执行事务

image-20230615090034166

case2:放弃事务
MULTI
DISCARD    ----  放弃事务

image-20230615090130161

Redis 事务异常处理
case3:全体连坐

image-20230615090212980

case4:冤头债主

image-20230615090402845

Redis不提供事务回滚的功能,开发者必须在事务执行出错后,自行恢复数据库状态

Redis 事务隔离机制
case5:watch监控

Redis使用Watch来提供乐观锁定,类似于CAS(Check-and-Set)

watch

初始化 k1 和 balance 两个key,先监控在开启 multi,保证两 key 变动在同一个事务内,

image-20230615091011141

有加塞篡改

image-20230615091112164

unwatch

image-20230615170038006

Redis 持久化

Redis 具有持久化功能,其会按照设置以快照或操作日志的形式将数据持久化到磁盘。

根据持久化使用技术的不同,Redis 的持久化分为两种:RDBAOF

需要注意的是,RDB 是默认持久化方式,但 Redis 允许 RDB 与 AOF 两种持久化技术同时开启,此时系统会使用 AOF 方式做持久化,即 AOF 持久化技术的优先级要更高。同样的道理,两种技术同时开启状态下,系统启动时若两种持久化文件同时存在,则优先加载 AOF 持久化文件。image-20230806110906254

RDB持久化

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是 把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,会自动读取 RDB 快照文件,从磁盘读取快照文件载入到内存,恢复数据。

快照文件称为RDB文件(dump.rdb),默认是保存在当前运行目录。

持久化的执行
RDB持久化在四种情况下会执行:
​	执行save命令  (线上禁止使用)  ---  手动保存
​	执行bgsave命令(推荐)   	   ---  手动保存
​	Redis停机时                  ---  自动保存  ---  执行 save命令
​	触发RDB条件时				 ---  自动保存  ---  本质仍然是bgsave命令
手动save命令

image-20230529142444910

手动bgsave命令

image-20230529142521267

自动条件触发
	自动条件触发的本质仍是 bgsave 命令的执行。只不过是用户通过在配置文件中做相应的设置后,Redis 会根据设置信息自动调用 bgsave 命令执行。
停机时
Redis停机时会执行一次save命令,实现RDB持久化。
查看持久化时间

通过 lastsave 命令可以查看最近一次执行持久化的时间,其返回的是一个 Unix 时间戳。image-20230810092830373

RDB 优化配置

RDB 相关的配置在 redis.conf 文件的 SNAPSHOTTING 部分

image-20230810092958737

(1)save

该配置用于设置快照的自动保存触发条件,即 save point,保存点

Redis6.0.16以下:
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000
   
    
这个规则是倒叙看的,60s内如果没有执行一万次,则不会执行bgsave,继续往上,300s内如果没有执行10次,也不会执行bgsave,继续wan900s内有一次就执行bgsave,

Redis6.0.16以下:

在这里插入图片描述

Redis6.2以及Redis7.0.0

在这里插入图片描述

(2)stop-write-on-bgsave-error

image-20230810093537832

	默认情况下,如果 RDB 快照已启用(至少一个保存点),且最近的 bgsave 命令失败,Redis 将停止接受写入。这样设置是为了让用户意识到数据没有正确地保存到磁盘上,否则很可能 没有人会注意到,并会发生一些灾难。当然,如果 bgsave 命令后来可以正常工作了,Redis 将自动允许再次写入。
(3) rdbcompression

image-20230810093635980

	当进行持久化时启用 LZF 压缩字符串对象。虽然压缩 RDB 文件会消耗系统资源,降低性能,但可大幅降低文件的大小,方便保存到磁盘,加速主从集群中从节点的数据同步。
(4) rdbchecksum

image-20230810093704159

	从 RDB5 开始,RDB 文件的 CRC64 校验和就被放置在了文件末尾。这使格式更能抵抗 RDB文件的损坏,但在保存和加载 RDB 文件时,性能会受到影响(约 10%),因此可以设置为 no禁用校验和以获得最大性能。在禁用校验和的情况下创建的 RDB 文件的校验和为零,这将告诉加载代码跳过校验检查。默认为 yes,开启了校验功能。
	文件如果损坏了,可以无法启动
(5) sanitize-dump-payload

image-20230810093811567

	该配置用于设置在加载 RDB 文件或进行持久化时是否开启对 zipList、listPack 等数据的全面安全检测。该检测可以降低命令处理时发生系统崩溃的可能。其可设置的值有三种选择:
		no:不检测
		yes:总是检测
		clients:只有当客户端连接时检测。排除了加载 RDB 文件与进行持久化时的检测。
	默认值本应该是 clients,但其会影响 Redis 集群的工作,所以默认值为 no,不检测
(6)dbfilename

image-20230810094104214

(7) rdb-del-sync-files

image-20230810094131507

	主从复制时,是否删除用于同步的从机上的 RDB 文件。默认是 no,不删除。不过需要注意,只有当从机的 RDB 和 AOF 持久化功能都未开启时才生效。
(8)dir

image-20230810094839849

RDB 文件结构

image-20230810095103405

(1) SOF
	SOF 是一个常量,一个字符串 REDIS,仅包含这五个字符,其长度为 5。用于标识 RDB文件的开始,以便在加载 RDB 文件时可以迅速判断出文件是否是 RDB 文件。
(2) rdb_version
这是一个整数,长度为 4 字节,表示 RDB 文件的版本号。
(3) EOF
EOF 是一个常量,占 1 个字节,用于标识 RDB 数据的结束,校验和的开始。
(4) check_sum
	校验和 check_sum 用于判断 RDB 文件中的内容是否出现数据异常。其采用的是 CRC 校验算法。
	CRC 校验算法:
	在持久化时,先将 SOF、rdb_version 及内存数据库中的数据快照这三者的二进制数据拼接起来,形成一个二进制数(假设称为数 a),然后再使用这个 a 除以校验和 check_sum,此时可获取到一个余数 b,然后再将这个 b 拼接到 a 的后面,形成 databases。
	在加载时,需要先使用 check_sum 对 RDB 文件进行数据损坏验证。验证过程:只需将RDB 文件中除 EOF 与 check_sum 外的数据除以 check_sum。只要除得的余数不是 0,就说明文件发生损坏。当然,如果余数是 0,也不能肯定文件没有损坏。这种验证算法,是数据损坏校验,而不是数据没有损坏的校验。
(5) databases

image-20230810100208290

	databases 部分是 RDB 文件中最重要的数据部分,其可以包含任意多个非空数据库。而每个 database 又是由三部分构成:
		SODB:是一个常量,占 1 个字节,用于标识一个数据库的开始。
		db_number:数据库编号。
		key_value_pairs:当前数据库中的键值对数据。

image-20230810100632228

	每个 key_value_pairs 又由很多个用于描述键值对的数据构成。
		VALUE_TYPE:是一个常量,占 1 个字节,用于标识该键值对中 value 的类型。
		EXPIRETIME_UNIT:是一个常量,占 1 个字节,用于标识过期时间的单位是秒还是毫秒。
		time:当前 key-value 的过期时间。
RDB 持久化过程
持久化过程

image-20230810100805897

	对于 Redis 默认的 RDB 持久化,在进行 bgsave 持久化时,redis-server 进程会 fork 出一个 bgsave 子进程,由该子进程以异步方式负责完成持久化。而在持久化过程中,redis-server进程不会阻塞,其会继续接收并处理用户的读写请求。
	bgsave 子进程的详细工作原理如下:
	由于子进程可以继承父进程的所有资源,且父进程不能拒绝子进程的继承权。所以,bgsave 子进程有权读取到 redis-server 进程写入到内存中的用户数据,使得将内存数据持久化到 dump.rdb 成为可能。
	bgsave 子进程在持久化时首先会将内存中的全量数据 copy 到磁盘中的一个 RDB 临时文件,copy 结束后,再将该文件 rename 为 dump.rdb,替换掉原来的同名文件。
写时复制

image-20230810101036796

在这里插入图片描述

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写 入 RDB 文件。

fork采用的是 写时复制技术(copy-on-write)技术:
​	当主进程执行读操作时,访问共享内存;
​	当主进程执行写操作时,则会拷贝一份数据,执行写操作。

在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,尽量避免膨胀。

页表:虚拟内存与物理内存之间的映射关系表

Linux中所有的进程都无法直接操作物理内存,而是由操作系统给每个进程分配一个虚拟内存,主进程只能操作虚拟内存。

执行fork时,会去创建子进程,fork的过程,不是把内存数据进行拷贝,仅仅是把页表进行拷贝。

在fork过程中是阻塞的,如果非要写入数据,会拷贝一份数据,执行写操作。

写时复制技术是 Linux 系统的一种进程管理技术。
原本在 Unix 系统中,当一个主进程通过 fork()系统调用创建子进程后,内核进程会复制主进程的整个内存空间中的数据,然后分配给子进程。这种方式存在的问题有以下几点:
	这个过程非常耗时
	这个过程降低了系统性能
	如果主进程修改了其内存数据,子进程副本中的数据是没有修改的。即出现了数据冗余,而冗余数据最大的问题是数据一致性无法保证。
现代的 Linux 则采用了更为有效的方式:写时复制。子进程会继承父进程的所有资源,其中就包括主进程的内存空间。即子进程与父进程共享内存。只要内存被共享,那么该内存就是只读的(写保护的)。而写时复制则是在任何一方需要写入数据到共享内存时都会出现异常,此时内核进程就会将需要写入的数据 copy 出一个副本写入到另外一块非共享内存区域。
注意事项
将备份文件(dump.rdb)移动到redis安装目录并启动服务即可
执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义
物理恢复,一定要服务和备份分机隔离  

--- 备注:不可以把备份文件dump.rdb和生产redis服务器放在同一台机器,必须分开各自存储,以防生产机物理损坏后备份文件也挂了。

哪些情况下会出发RDB快照

1) 配置文件中默认的快照配置
2) 手动save/bgsave命令
3) 执行flushall/flushdb命令也会产生dump.rdb文件,但是里面是空的,没有意义
4) 执行shutdown且没有设置开启AOF持久化
5) 主从复制时,主节点自动触发
小结
RDB方式bgsave的基本流程?
​		fork主进程得到一个子进程,共享内存空间
​		子进程读取内存数据并写入新的RDB文件
​		用新RDB文件替换旧的RDB文件
RDB的优点?
​		适合大规模的数据备份
​		按照业务定时备份
​		对数据完整性和一致性要求不高
​		RDB文件在内存中的加载速度要比AOF快得多
RDB的缺点?
​		RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
​		fork子进程、压缩、写出RDB文件都比较耗时,
内存数据的全量同步,如果数据太大会导致I/O严重影像服务器性能


如果rdb文件损坏,可以使用redis-check-rdb rdb文件名, 进行文件修复

AOF持久化

AOF原理

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件(appendonly.aof),可以看做是命令日志文件。当需要恢复内存数据时,将这些写操作重新执行一次,便会恢复到之 前的内存数据状态。

在这里插入图片描述

AOF配置
(1) AOF 的开启

AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:

# 是否开启AOF功能,默认是no
appendonly yes
(2) 文件名配置

image-20230810140531145

Redis 7 在这里发生了重大变化。原来只有一个 appendonly.aof 文件,现在具有了'三类多个文件'(并且前缀都是appendonly.aof):
	'基本文件':可以是 RDF 格式也可以是 AOF 格式。其存放的内容是由 RDB 转为 AOF 当时内存的快照数据。该文件可以有多个。
	'增量文件':以操作日志形式记录转为 AOF 后的写入操作。该文件可以有多个。
	'清单文件':用于维护 AOF 文件的创建顺序,保障激活时的应用顺序。该文件只有一个。
(3) 混合式持久化开启

image-20230810140724767

	对于基本文件可以是 RDF 格式也可以是 AOF 格式。通过 aof-use-rdb-preamble 属性可以 选择。其默认值为 yes,即默认 AOF 持久化的基本文件为 'rdb' 格式文件,也就是默认采用混合式持久化。
(4) AOF 文件目录配置

image-20230810140844301

	为了方便管理,可以专门为 AOF 持久化文件指定存放目录。目录名由 appenddirname属性指定,存放在 redis.conf 配置文件的 dir 属性指定的目录,默认为 Redis 安装目录。
AOF 文件格式
	AOF 文件包含三类文件:'基本文件''增量文件''清单文件''基本文件' 一般为' rdb格式''增量文件' 一般为 'aof格式' 
    '清单文件' 一般为 ''
(1) Redis 协议
	'增量文件扩展名为.aof',采用 AOF 格式。AOF 格式其实就是 Redis 通讯协议格式,AOF持久化文件的本质就是基于 Redis 通讯协议的文本,将命令以纯文本的方式写入到文件中。Redis 协议规定,Redis 文本是以行来划分,每行以\r\n 行结束。每一行都有一个消息头,以表示消息类型。消息头由六种不同的符号表示,其意义如下:
		(+) 表示一个正确的状态信息
		(-) 表示一个错误信息
		(*) 表示消息体总共有多少行,不包括当前行
		($) 表示下一行消息数据的长度,不包括换行符长度\r\n
		() 表示一个消息数据
		(:) 表示返回一个数值
(2) 查看 AOF 文件

打开增量文件(appendonly.aof.1.incr.aof),可以看到如下格式内容。

image-20230810141505904

以上内容中框起来的是三条命令。一条数据库切换命令 SELECT 0,两条 set 命令。它们的意义如下:

*2 -- 表示当前命令包含 2 个参数
$6 -- 表示第 1 个参数包含 6 个字符
SELECT --1 个参数
$1 -- 表示第 2 个参数包含 1 个字符
0 --2 个参数
    
*3 --表示当前命令包含 3 个参数
$3 -- 表示第 1 个参数包含 3 个字符
set --1 个参数
$3 -- 表示第 2 个参数包含 3 个字符
k11 --2 个参数
$3 -- 表示第 3 个参数包含 2 个字符
v11 --3 个参数

......
(3) 清单文件

打开清单文件 appendonly.aof.manifest,查看其内容如下:

image-20230810141657463

	该文件首先会按照 seq 序号列举出所有基本文件,基本文件 type 类型为 b,然后再按照 seq 序号再列举出所有增量文件,增量文件 type 类型为 i。 对于 Redis 启动时的数据恢复,也会按照该文件由上到下依次加载它们中的数据。
Rewrite 机制

image-20230529143529873

	也就是 'AOF文件重写'
	随着使用时间的推移,AOF 文件会越来越大。为了防止 AOF 文件由于太大而占用大量 的磁盘空间,降低性能,Redis 引入了 Rewrite 机制来对 AOF 文件进行压缩。
	很重要!!!!!!!
(1) 何为 rewrite
	所谓 Rewrite 其实就是对 AOF 文件进行重写整理。当 Rewrite 开启后,主进程 redis-server创建出一个子进程 bgrewriteaof,由该子进程完成 rewrite 过程。其首先对现有 aof 文件进行rewrite 计算,将计算结果写入到一个临时文件,写入完毕后,再 rename 该临时文件为原 aof文件名,覆盖原有文件。
(2) rewrite 计算
rewrite 计算也称为 rewrite 策略。rewrite 计算遵循以下策略:
	'读操作命令不写入文件'
	'无效命令不写入文件'
	'过期数据不写入文件'
	'多条命令合并写入文件'
比如lpush操作、会将多条合并成一条但是如果过大,会出现缓存溢出的情况,所以内部会进行切割,每64个元素切割成一条
(3) 手动开启 rewrite
Rewrite 过程的执行有两种方式。
​	通过 bgrewriteaof 命令手动开启, 
​	通过 设置条件自动开启。   ----  一般都用这个

以下是手动开启方式:

image-20230810142153158

该命令会使主进程 redis-server 创建出一个子进程 bgrewriteaof,由该子进程完成 rewrite 过程。而在 rewrite 期间,redis-server 仍是可以对外提供读写服务的。

(4) 自动开启 rewrite
	手动方式需要人办干预,所以'一般采用自动方式'。由于 Rewrite 过程是一个计算过程,需要消耗大量系统资源,会降低系统性能。所以,Rewrite 过程并不是随时随地任意开启的,而是通过设置一些条件,当满足条件后才会启动,以降低对性能的影响

image-20230810142247757

	'auto-aof-rewrite-percentage':开启 rewrite 的增大比例,默认 100%。指定为 0,表示禁用自动 rewrite。
	'auto-aof-rewrite-min-size':开启 rewrite 的 AOF 文件最小值,默认 64M。该值的设置主要是为了防止小 AOF 文件被 rewrite,从而导致性能下降。
	'自动重写 AOF 文件'。当 AOF 日志文件大小增长到指定的百分比时,Redis 主进程redis-server 会 fork 出一个子进程 bgrewriteaof 来完成 rewrite 过程。
	其'工作原理'如下:Redis 会记住最新 rewrite 后的 AOF 文件大小作为基本大小,如果从主机启动后就没有发生过重写,则基本大小就使用启动时 AOF 的大小。
	如果当前 AOF 文件大于基本大小的配置文件中指定的百分比阈值,且当前 AOF 文件大于配置文件中指定的最小阈值,则会触发 rewrite。
#自动触发重写机制
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
默认配置是:当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发

#手动触发重写机制
客户端向服务器发送bgrewriteaof命令
重写原理
1:在重写开始前,redis会创建一个“重写子进程”,这个子进程会读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。

2:与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。

3:当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中

4:当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中

5:重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似
AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去替代之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。
AOF 优化配置
(1) appendfsync

image-20230810142507742

	当客户端提交写操作命令后,该命令就会写入到 aof_buf 中,而 aof_buf 中的数据持久化到磁盘 AOF 文件的过程称为数据同步。(将缓存的数据同步到文件里面)
	将 aof_buf 中的数据同步到 AOF 文件,采用不同的数据同步策略,同时的时机是不同的,有'三种策略''always':写操作命令'写入 aof_buf 后会立即调用' fsync()系统函数,将其追加到 AOF 文件。该策略效率较低,但相对比较安全,不会丢失太多数据。最多就是刚刚执行过的写操作在尚未同步时出现宕机或重启,将这一操作丢失。
		'no':写操作命令'写入 aof_buf 后什么也不做',不会调用 fsync()函数。而将 aof_buf 中的数据同步磁盘的操作由操作系统负责。Linux 系统默认同步周期为 30 秒。'效率较高'。
		'everysec':默认策略。写操作命令写入 aof_buf 后并不直接调用 fsync(),而是'每秒调用一次' fsync()系统函数来完成同步。该策略兼顾到了性能与安全,是一种折中方案。
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

image-20230529143406474

(2) no-appendfsync-on-rewrite

image-20230810142914094

当主进程'创建了子进程' 正在执行' bgsave' 或 'bgrewriteaof' 并且 AOF fsync 策略设置为 'always' 或 'everysec'
主进程是否不调用 fsync()来做数据同步  设置为 no,双重否定即肯定,主进程会调用 fsync()做同步。而 yes 则不会调用 fsync()做数据同步
	如果调用 fsync(),在需要同步的数据量非常大时,会阻塞主进程对外提供服务,即会存在延迟问题。如果不调用 fsync(),则 AOF fsync 策略相当于设置为了 no,可能会存在 30 秒数据丢失的风险。
(3) aof-rewrite-incremental-fsync

image-20230810143458755

	当 bgrewriteaof 在执行过程也是先将 rewrite 计算的结果写入到了 aof_rewrite_buf 缓存中,然后当缓存中数据达到一定量后就会调用 fsync()进行刷盘操作,即数据同步,将数据写入到临时文件。该属性用于控制 fsync()每次刷盘的数据量最大不超过 4MB。这样可以避免由于单次刷盘量过大而引发长时间阻塞。
(4) aof-load-truncated

image-20230810143712375

	在进行 AOF 持久化过程中可能会出现系统突然宕机的情况,此时写入到 AOF 文件中的'最后一条数据'可能会不完整。当主机启动后,RedisAOF 文件不完整的情况下是否可以启动,取决于属性 aof-load-truncated 的设置。其值为:
		'yes'AOF 文件最后不完整的数据直接从 AOF 文件中截断删除,不影响 Redis 的启动。
		'no'AOF 文件最后不完整的数据不可以被截断删除,Redis 无法启动。
        

除了最后一条数据错了还行、其他任意位置都不行、redis都不能成功启动

可以使用redis检测工具:
redis-check-aof 文件名(appendonly.aof.3.inf.aof)
会出现错误详情

可以使用
redis-chedk-aof --f 文件名 
来修复这个文件
(5) aof-timestamp-enabeld

image-20230810143853004

	该属性设置为 yes 则会开启在 AOF 文件中增加时间戳的显示功能,可方便按照时间对数据进行恢复。但该方式可能会与 AOF 解析器不兼容,所以默认值为 no,不开启。
AOF 持久化过程

image-20230810144430881

AOF 详细的持久化过程如下:

(1) Redis 接收到的写操作命令并不是直接追加到磁盘的 AOF 文件的,而是将每一条写命令按照 redis 通讯协议格式暂时添加到 AOF 缓冲区 aof_buf。

(2) 根据设置的数据同步策略,当同步条件满足时,再将缓冲区中的数据一次性写入磁盘的AOF 文件,以减少磁盘 IO 次数,提高性能。

(3) 当磁盘的 AOF 文件大小达到了 rewrite 条件时,redis-server 主进程会 fork 出一个子进程bgrewriteaof,由该子进程完成 rewrite 过程。

(4) 子进程 bgrewriteaof 首先对该磁盘 AOF 文件进行 rewrite 计算,将计算结果写入到一个临时文件,全部写入完毕后,再 rename 该临时文件为磁盘文件的原名称,覆盖原文件。

(5) 如果在 rewrite 过程中又有写操作命令追加,那么这些数据会暂时写入 aof_rewrite_buf缓冲区。等将全部 rewrite 计算结果写入临时文件后,会先将 aof_rewrite_buf 缓冲区中的数据写入临时文件,然后再 rename 为磁盘文件的原名称,覆盖原文件。

RDB 与 AOF 对比

image-20230529143659649

在这里插入图片描述

RDB-AOF混合持久化
当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB保存的数据集要完整。
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。
最好不要只使用AOF,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),留着rdb作为一个万一的手段。
RDB-AOF混合持久化方式
1 开启混合方式设置

设置aof-use-rdb-preamble的值为 yes   yes表示开启,设置为no表示禁用

2 RDB+AOF的混合方式---------> 结论:RDB镜像做全量持久化,AOF做增量持久化

先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录。这样的话,重启服务的时候会从RDB和AOF两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。----》AOF包括了RDB头部+AOF混写

在这里插入图片描述

纯缓存模式

同时关闭RDB + AOF

save ""         --- 禁用RDB
禁用rdb持久化模式下,我们仍可以使用save、bgsave生成rdb文件

appendonly no   ---  禁用AOF
禁用aof持久化模式下,我们仍可以使用命令bgrewiteaof生成aof文件

主从集群(复制)

主从集群搭建

基础命令

info replication
可以查看复制节点的主从关系和配置信息
replicaof
主库IP,主库端口
一般写入进redis.conf配置文件内
slaveof
'slaveof 主库IP 主库端口'
 slaveof 127.0.0.1 'master的启动端口'
'每次与master断开之后,都需要重新连接,除非你配置进redis.conf文件'
在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系'转而和新的主数据库同步,重新拜码头'  -- 手动换义父
'slaveof no one' 是当前数据库停止与其他数据库的同步,转成主数据库,自立为王
    
可以查看复制节点的主从关系和配置信息

搭建主从架构

	Redis 的主从集群是一个“一主多从”的读写分离集群。集群中的 'Master' 节点'负责处理客户端的读写请求',而 'Slave' 节点仅能'处理客户端的读请求'。只所以要将集群搭建为读写分离模式,主要原因是,对于数据库集群,写操作压力一般都较小,压力大多数来自于读操作请求。所以,只有一个节点负责处理写操作请求即可。
伪集群搭建与配置
	在采用单线程 IO 模型时,为了提高处理器的利用率,一般会在一个主机中安装多台 Redis,构建一个 Redis 主从伪集群。当然,搭建伪集群的另一个场景是,在学习 Redis,而学习用主机内存不足以创建多个虚拟机。
	下面要搭建的读写分离伪集群包含一个 Master 与两个 Slave。它们的端口号分别是:6380、6381、6382。
(1) 复制 redis.conf
	在 redis 安装目录中 mkdir 一个目录,名称随意。这里命名为 cluster。
	然后将 redis.conf文件复制到 cluster 目录中。该文件后面会被其它配置文件包含,所以该文件中需要设置每个 Redis 节点相同的公共的属性。
	
在reids.conf 名录下  
	mkdir cluster 				创建目录
	cp redis.conf cluster/	    将redis.conf拷贝到这个目录下

image-20231031144107402

(2) 修改 redis.conf

在 redis.conf 中做如下几项修改:

A、daemonize
yes 开启redis后台运行
B、bind
注释掉bind 127.0.0.1  -- 这个会绑定本机ip
C、protected-mode
关闭掉保护模式
protected- mode bo
D、masterauth

image-20231031144139942

	因为我们要搭建主从集群,且每个主机都有可能会是 Master,所以最好不要设置密码验证属性 requirepass。如果真需要设置,一定要每个主机的密码都设置为相同的。此时每个配置文件中都要设置两个完全相同的属性:requirepass 与 masterauth。其中 requirepass 用于指定当前主机的访问密码,而 masterauth 用于指定当前 slave 访问 master 时向 master 提交的访问密码,用于让 master 验证自己身份是否合法。
    '尚硅谷,主机不需要 masterauth 但是需要 requirepass, 从机一定要配置 masterauth和requirepass'
	'要求主和从的密码(requirepass)是一摸一样的,而且 masterauth 和 requirepass 也得是一摸一样的'
    '生产环境一定要配置'
    '使用哨兵的情况下,所有机器都要配 masterauth 和 requirepass'
E、 repl-disable-tcp-nodelay

image-20231031144212968

	该属性用于设置是否禁用 TCP 特性 tcp-nodelay。设置为 yes 则禁用 tcp-nodelay,此时master 与 slave 间的通信会产生延迟,但使用的 TCP 包数量会较少,占用的网络带宽会较小。相反,如果设置为 no,则网络延迟会变小,但使用的 TCP 包数量会较多,相应占用的网络带宽会大。
    nodelay:ture 就是延迟,  no 就是不延迟 ,来了直接发送
	
	tcp-nodelay:为了充分复用网络带宽,TCP 总是希望发送尽可能大的数据块。为了达到该目的,TCP 中使用了一个名为 'Nagle' 的算法。
	'Nagle' 算法的工作原理是,网络在接收到要发送的数据后,并不直接发送,而是等待着数据量足够大(由 TCP 网络特性决定)时再一次性发送出去。这样,网络上传输的有效数据比例就得到了大大提升,无效数据传递量极大减少,于是就节省了网络带宽,缓解了网络压力。tcp-nodelay 则是 TCP 协议中 Nagle 算法的开头。 
(3) 新建 redis6380.conf

新建一个 redis 配置文件 redis6380.conf,该配置文件中的 Redis 端口号为 6380。

include redis.conf  #引入当前目录下的conf文件 ,下面那些重复的配置会替换掉conf里面的配置
dir /工作目录(绝对路径)    #docker下需要将工作目录放到容器卷上
pidfile "/var/run/reids_6380.pid"
port 6380          
dbfilename dump6380.rdb      #rdb的名字
appendfilename "appendonly6380.aof"   #是否开启aof的命令配置(非必须) appendonly yes
replica-priority 90          #选举的优先级 越小越好,但是 0 不能够当master 
logfile /日期也需要配置绝对路径
    
 ps aux | grep redis  查看启动状态

image-20231031144247846

(4) 再复制出两个 conf 文件

再使用 redis6380.conf 复制出两个 conf 文件:redis6381.conf 与 redis6382.conf。然后修 改其中的内容。

文件内

: %s/6381/6382 可以将多个6381 替换为6382

image-20231031144307000

image-20231031144315916

修改 redis6382.conf 的内容如下

在这里插入图片描述

(5) 启动三台 Redis

在这里插入图片描述

(6) 设置主从关系

再打开三个会话框,分别使用客户端连接三台 Redis。然后通过 slaveof 命令,指定 6380 的 Redis 为 Master。

在这里插入图片描述

(7) 查看状态信息

通过 info replication 命令可查看当前连接的 Redis 的状态信息。

image-20231031144410666

分级管理

若 Redis 主从集群中的 Slave 较多时,它们的数据同步过程会对 Master 形成较大的性能 压力。此时可以对这些 Slave 进行分级管理。

在这里插入图片描述

	设置方式很简单,只需要让低级别 Slave 指定其 slaveof 的主机为其上一级 Slave 即可。不过,上一级 Slave 的状态仍为 Slave,只不过,其是更上一级的 Slave。
	例如,指定 6382 主机为 6381 主机的 Slave,而 6381 主机仍为真正的 Master 的 Slave。
	只需要将6382换码头即可,将他的主节点指向6381

在这里插入图片描述

此时会发现,Master 的 Slave 只有 6381 一个主机。

容灾冷处理
如果master挂掉了,这是从节点会显示连接down,但是还是从节点。这时候就需要容灾处理了	

在 Master/SlaveRedis 集群中,若 Master 出现宕机'有两种处理方式':
		一种是'通过手工角色调整',使 Slave 晋升为 Master'冷处理';
		一种是使用'哨兵模式',实现 Redis集群的高可用 HA,即'热处理'。
    这里是冷处理,哨兵问题看下面单独讲解
	无论 Master 是否宕机,Slave 都可通过 slaveof no one 将自己由 Slave 晋升为 Master。如果其原本就有下一级的 Slave,那么,其就直接变为了这些 Slave 的真正的 Master 了。而原来的 Master 也会失去这个原来的 Slave。
    也就是抛弃之前的master了,自己从Slave变为master,自己的小弟还是自己的小弟,不过之前由二级小弟变一级小弟了

在这里插入图片描述

主从基本问题

配置文件固定写死

1、配置文件执行  replicaof 主库Ip 主库端口
2、配从库不配主库(6381和6382)--  replicaof 192.168.200.130 6380       masterauth 111
3、先master后两台slave启动
4、主从关系查看
	日志:主机日志、备机日志
	命令:info replication
主从问题细节

1、从机可以执行写命令吗

在这里插入图片描述

2、从机切入点问题

'slave是从头开始复制还是从切入点开始复制?'
master启动,写到k3
slave1跟着master同时启动,跟着写到k3
'slave2写到k3后才启动,那之前的是否也可以复制?'
slave2启动后,也查得到k3
    
Y,首次一锅端,后续跟随,master写,slave跟

3、主机shutdown后情况如何?从机是上位还是原地待命

从机不动,原地待命,从机数据可以正常使用;等待主机重启动归来

4、主机shutdown后,重启后主从关系还在吗?从机还能否顺利复制

归来后,主机仍是主机,从机仍是从机

5、某台从机down后,master继续,从机重启后他能跟上大部队吗?

从机启动后依旧继续同步数据

命令操作手动指定

就像上面主从集群搭建一样,分别启动3台,目前三台都是主机状态,各不从属
手动在从机上执行slaveof 主库Ip 主库端口

用命令使用的话,2台从机重启后,关系就不存在了,2台从机会直接变为主机,

配置与命令区别

配置,持久稳定
命令,当次生效

主从复制原理

主从复制过程
	当一个 Redis 节点(slave 节点)接收到类似 'slaveof 127.0.0.1 6380' 的指令后直至其可以 从 master 持续复制数据,大体经历了如下几个过程:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(1) 保存 master 地址
当 slave 接收到 slaveof 指令后,slave 会立即将新的 master 的地址保存下来。
(2) 建立连接
	slave 中维护着一个定时任务,该定时任务会尝试着与该 master 建立 socket 连接。如果 连接无法建立,则其会不断定时重试,直到连接成功或接收到 slaveof no one 指令。
(3) slave 发送 ping 命令
	连接建立成功后,slave 会发送 ping 命令进行首次通信。如果 slave 没有收到 master 的回复,则 slave 会主动断开连接,下次的定时任务会重新尝试连接。
(4) 对 slave 身份验证
	如果 master 收到了 slave 的 ping 命令,并不会立即对其进行回复,而是会先进行身份验证。如果验证失败,则会发送消息拒绝连接;如果验证成功,则向 slave 发送连接成功响应。
(5) master 持久化
	首次通信成功后,slave 会向 master 发送数据同步请求(sync 命令)。当 master 接收到请求后,会 fork出一个子进程,让子进程以异步方式立即进行持久化。此时,slave自身原有数据会被会被master数据覆盖清除
	master节点收到sync命令后会开始在后台保存快照(即RDB持久化,主从复制时会触发RDB),同时收集所有接收到的用于修改数据集命令缓存起来,master节点执行RDB持久化完后master将rdb快照文件和所有缓存的命令发送到所有slave,以完成一次完全同步
	而slave服务在接收到数据库文件数据后,将其存盘并加载到内容中,从而完成复制初始化
(6) 数据发送
	repel-ping-replica-period 10   ---  master发出PING包的周期,默认是10秒,保存心跳,保持通信
	持久化完毕后 master 会再 fork 出一个子进程,让该子进程以异步方式将数据发送给slave。slave 会将接收到的数据不断写入到本地的持久化文件中。
	在 slave 数据同步过程中,master 的主进程仍在不断地接受着客户端的写操作,且不仅将新的数据写入到了 master 内存,同时也写入到了同步缓存。当 master 的持久化文件中的数据发送完毕后,master 会再将同步缓存中新的数据发送给 slave,由 slave 将其写入到本地
持久化文件中。数据同步完成。
(7) slave 恢复内存数据
当 slave 与 master 的数据同步完成后,slave 就会读取本地的持久化文件,将其恢复到本地内存,然后就可以对外提供读服务了。
(8) 持续增量复制
在 slave 对外提供服务过程中,master 会持续不断的将新的数据以增量方式发送给 slave,以保证主从数据的一致性。
master会检查backlog里面的offset,master和slave都会保存一个复制的offset还有一个masterId
offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给Slave,类似断点续传
数据同步演变过程
(1) sync 同步
	Redis 2.8 版本之前,首次通信成功后,slave 会向 master 发送 sync 数据同步请求。然后master 就会将其所有数据全部发送给 slave,由 slave 保存到其本地的持久化文件中。这个过程称为全量复制。
	但这里存在一个问题:在全量复制过程中可能会出现由于网络抖动而导致复制过程中断。当网络恢复后,slave 与 master 重新连接成功,此时 slave 会重新发送 sync 请求,然后会从头开始全量复制。
	由于全量复制过程非常耗时,所以期间出现网络抖动的概率很高。而中断后的从头开始不仅需要消耗大量的系统资源、网络带宽,而且可能会出现长时间无法完成全量复制的情况。
(2) psync 同步
	Redis 2.8 版本之后,全量复制采用了 psync(Partial Sync,不完全同步)同步策略。当全量复制过程出现由于网络抖动而导致复制过程中断时,当重新连接成功后,复制过程可以“断点续传”。即从断开位置开始继续复制,而不用从头再来。这就大大提升了性能。

为了实现 psync,整个系统做了三个大的变化

A、复制偏移量

​ 系统为每个要传送数据进行了编号,该编号从 0 开始,每个字节一个编号。该编号称为 复制偏移量。参与复制的主从节点都会维护该复制偏移量。

在这里插入图片描述

​ master 每发送过一个字节数据后就会进行累计。统计信息通过 info replication 的 master_repl_offset 可查看到。同时,slave 会定时向 master 上报其自身已完成的复制偏移量 给 master,所以 master 也会保存 slave 的复制偏移量 offset。

在这里插入图片描述

slave在接收到master的数据后,也会累计接收到的偏移量。统计信息通过info replication 的 slave_repl_offset 可查看到。

B、 主节点复制 ID
	当 master 启动后就会动态生成一个长度为 40 位的 16 进制字符串作为当前 master 的复制 ID,该 ID 是在进行数据同步时 slave 识别 master 使用的。通过 info replication 的master_replid 属性可查看到该 ID。
	
	这里不适用ip + 端口是因为: 如果是ip+端口,如果master宕机了,给master重启后,会进行断点续传操作,万一之前master宕机之前,之前的数据并没有同步到master文件里面,就会导致数据丢失
	这里使用动态 ID 就能保证每次启动会,都会进行全量同步数据。
C、 复制积压缓冲区
	当 master 有连接的 slave 时,在 master 中就会创建并维护一个队列 backlog,默认大小为 1MB,该队列称为复制积压缓冲区。这个缓冲区和 maxmemory 共用内存    
    master 接收到了写操作数据不仅会写入到 master 主存,写入到 master 中'为每个 slave 配置的发送缓存',而且'还会写入到复制积压缓冲区'。其作用就是用于保存最近操作的数据,以备“断点续传”时做数据补偿,防止数据丢失。
  	如果连接因为网络抖动断开了,这时候master有了写操作,这个时候没有办法在slave配置的缓存中写,这时候写到'复制积压缓冲区'中,等到连接后同步到 slave配置的缓存中
        
    一个master,两个slave, master会写三个地方,一个复制积压缓冲区,两个 slave 配置的发送缓存
     reids7.0 之后就只留下了一个复制积压缓冲区 ,共用这个一个缓冲区,充分利用了复制积压缓冲区。
        
D、psync 同步过程

在这里插入图片描述
image-20231031182237098

	psync 是一个由 slave 提交的命令,其格式为 psync ,表示当前 slave 要从指定的 master 中的 repl_offset+1 处开始复制。repl_offset 表示当前 slave 已经完成复制的数据的 offset。该命令保证了“断点续传”的实现。
	在第一次开始复制时,slave 并不知道 master 的动态 ID,并且一定是从头开始复制,所以其提交的 psync 命令为 'PSYNC ? -1'。即 master_replid 为问号(?),repl_offset 为-1。
	如果复制过程中断后 slave 与 master 成功连接,则 slave 再次提交 psyn 命令。此时的 psyn命令的 repl_offset 参数为其前面已经完成复制的数据的偏移量。
	
	其实,并不是slave提交了psyn命令后就可以立即从master处开始复制,而是需要master给出响应结果后,根据响应结果来执行。master 根据 slave 提交的请求及 master 自身情况会给出不同的响应结果。响应结果'有三种可能':
		'FULLRESYNC <master_replid> <repl_offset>':告知 slave 当前 master 的动态 ID 及可以开始全量复制了,这里的 repl_offset 一般为 0
		'CONTINUE':告知 slave 可以按照你提交的 repl_offset 后面位置开始“续传”了
		'ERR':告知 slave,当前 master 的版本低于 Redis 2.8,不支持 psyn,你可以开始全量复制了
E、 psync 存在的问题
	在 psync 数据同步过程中,若 slave 重启,在 slave 内存中保存的 master 的动态 ID 与续传 offset 都会消失,'断点续传'将无法进行,从而只能进行全量复制,导致资源浪费。
	在 psync 数据同步过程中,master 宕机后 slave 会发生'易主',从而导致 slave 需要从新 master 进行全量复制,形成资源浪费。
(3) psync 同步的改进

Redis 4.0 对 psync 进行了改进,提出了“同源增量同步”策略。

A、解决 slave 重启问题
	针对“slave 重启时 master 动态 ID 丢失问题”,改进后的 psync 将 master 的动态 ID 直接写入到了 slave 的持久化文件中。
	slave 重启后直接从本地持久化文件中读取 master 的动态 ID,然后向 master 提交获取复制偏移量的请求。master 会根据提交请求的 slave 地址,查找到保存在 master 中的复制偏移量,然后向 slave 回复 FULLRESYNC <master_replid> <repl_offset>,以告知 slave 其马上要开始发送的位置。然后 master 开始“断点续传”。
	repl_offset可能会小一点,不过不影响,因为
B、 解决 slave 易主问题
	slave 易主后需要和新 master 进行全量复制,本质原因是新 master 不认识 slave 提交的psync 请求中“原 master 的动态 ID”。如果 slave 发送 PSYNC <原master_replid> <repl_offset>命令,新master能够识别出该slave要从原master复制数据,而自己的数据也都是从该master复制来的。那么新 master 就会明白,其与该 slave“师出同门”,应该接收其“断点续传”同步请求。
	而新 master 中恰好保存的有“原 master 的动态 ID”。由于改进后的 psync 中每个 slave都在本地保存了当前 master 的动态 ID,所以当 slave 晋升为新的 master 后,其本地仍保存有之前 master 的动态 ID。而这一点也恰恰为解决“slave 易主”问题提供了条件。通过 master的 info replicaton 中的 master_replid2 可查看到。如果尚未发生过易主,则该值为 40 个 0。
(4) 无盘操作
Redis 6.0 对同步过程又进行了改进,提出了“无盘全量同步”与“无盘加载”策略,避免了耗时的 IO 操作。
	'无盘全量同步':master 的主进程 fork 出的子进程直接将内存中的数据发送给 slave,无需经过磁盘。
	'无盘加载':slave 在接收到 master 发送来的数据后不需要将其写入到磁盘文件,而是直接写入到内存,这样 slave 就可快速完成数据恢复。
(5) 共享复制积压缓冲区
	Redis 7.0 版本对复制积压缓冲区进行了改进,让各个 slave 的发送缓冲区共享复制积压缓冲区。这使得复制积压缓冲区的作用,除了可以保障数据的安全性外,还作为所有 slave的发送缓冲区,充分利用了复制积压缓冲区。

哨兵机制实现

	对于 Master 宕机后的冷处理方式是无法实现高可用的。Redis 从 2.6 版本开始提供了高可用的解决方案—— Sentinel 哨兵机制。在集群中再引入一个节点,该节点充当 Sentinel 哨兵,用于监视 Master 的运行状态,并在 Master 宕机后自动指定一个 Slave 作为新的 Master。整个过程无需人工参与,完全由哨兵自动完成。
	不过,此时的 Sentinel 哨兵又成为了一个单点故障点:若哨兵发生宕机,整个集群将瘫痪。所以为了解决 Sentinel 的单点问题,又要为 Sentinel 创建一个集群,即 Sentinel 哨兵集群。一个哨兵的宕机,将不会影响到 Redis 集群的运行。
	那么这些 Sentinel 哨兵是如何工作的呢?Sentinel 是如何知道其监视的 Master 状态的呢?每个 Sentinel 都会定时会向 Master 发送心跳,如果 Master 在有效时间内向它们都进行了响应,则说明 Master 是“活着的”。如果 Sentinel 中有 quorum 个哨兵没有收到响应,那么就认为 Master 已经宕机,然后会有一个 Sentinel 做 Failover 故障转移。即将原来的某一个 Slave晋升为 Master。

Redis 高可用集群搭建

	在“不差钱”的情况下,可以让 Sentinel 占用独立的主机,即在 Redis 主机上只启动 Redis进程,在 Sentinel 主机上只启动 Sentinel 进程。下面要搭建一个“一主二从三哨兵”的高可用伪集群,即这些角色全部安装运行在一台主机上。“一主二从”使用前面的主从集群,下面仅搭建一个 Sentinel 伪集群。

image-20231102165629612

(1) 复制 sentinel.conf
将 Redis 安装目录中的 sentinel.conf 文件复制到 cluster 目录中。该配置文件中用于存放一些 sentinel 集群中的一些公共配置。
(2) 修改 sentinel.conf

修改 cluster/sentinel.conf 配置文件。

sentinel monitor

在这里插入图片描述

	该配置用于指定 Sentinel 要监控的 master 是谁<ip><redis-port>,并为 master 起了一个名字<master-name>。该名字在后面很多配置中都会使用。同时指定 Sentinel 集群中决定该master“客观下线状态”判断的法定 sentinel 数量<quorum>。<quorum>的另一个用途与
sentinel 的 Leader 选举有关。要求中至少要有 max(quorum, sentinelNum/2+1)个 sentinel 参与,选举才能进行。
	这里将该配置注释掉,因为要在后面的其它配置文件中设置,如果不注释就会出现配置冲突。
sentinel auth-pass

在这里插入图片描述

如果 Redis 主从集群中的主机设置了访问密码,那么该属性就需要指定 master 的主机名与访问密码。以方便 sentinel 监控 master。
如果有的redis没有配置密码,也是对的,也可以晋升为master,但是如果配置密码,必须密码要相同,主从和这个都要一样
新建 sentinel26380.conf
在 Redis 安装目录下的 cluster 目录中新建 sentinel26380.conf 文件作为 Sentinel 的配置文件,并在其中键入如下内容
生产上一定要配密码

sentinel monitor mymaster 192.168.192.102 6380 2     -- 这是一个主从复制得
sentinel monitor mymaster 192.168.192.112 6280 2     -- 这个可以配置好几个,相当于好几个主从复制,一般一个就够了

在这里插入图片描述

	sentinel monitor 属性用于指定当前监控的 master 的 IPPort,同时为集群中 master 指定一个名称 mymaster,以方便其它属性使用。
	最后的 2 是参数 'quorum' 的值,quorum 有两个用途。
		一个是只有当 quorum 个 sentinel都认为当前 master 宕机了才能开启故障转移。
		另一个用途与 sentinel 的 Leader 选举有关。要求中至少要有 'max(quorum, sentinelNum/2+1)'个 sentinel 参与,选举才能进行。
再复制两个 conf 文件
再使用sentinel26380.conf 复制出两个conf文件:sentinel26381.conf与sentinel26382.conf。然后修改其中的内容。
:%s/26380/26381  -- 全局替换命令

在这里插入图片描述

Redis 高可用集群的启动

(1) 启动并关联 Redis 集群
首先要启动三台 Redis,然后再通过 slaveof 关联它们。以  6380 为master节点
redis-cli -p 6381 salveof 192.168.200.130 6380
redis-cli -p 6382 salveof 192.168.200.130 6380
(2)启动 Sentinel 集群
启动命令
	在/usr/local/bin 目录下有一个命令 redis-sentinel 用于启动 Sentinel。不过,我们发现一个奇怪的现象:/usr/local/bin 目录中的 redis-sentinel 命令是 redis-server 命令的软链接,这是为什么呢?

在这里插入图片描述

	查看 Redis 安装目录中的 src 目录中的 redis-server 与 redis-sentinel 命令,我们发现这两个命令的大小一模一样。其实,这两个命令本质上是同一个命令。
	只所以可以启动不同的进程,主要是因为在启动时所加载的配置文件的不同。所以在启动 Sentinel 时,需要指定 sentinel.conf 配置文件。
两种启动方式
由于 redis-server 与 redis-sentinel 命令本质上是同一个命令,所以使用这两个命令均可启动 Sentinel。
	方式一,使用 redis-sentinel 命令:'redis-sentinel sentinel26380.conf'
	方式二,使用 redis-server 命令:'redis-server sentinel26380.conf --sentinel'
启动三台Sentinel

image-20231102170816915

(3) 查看 Sentinel 信息
运行中的 Sentinel 就是一个特殊 Redis,其也可以通过客户端连接,然后通过 info sentinel来查看当前连接的 Sentinel 的信息。

image-20231102170934835

(4) 查看 sentinel 配置文件

打开任意 sentinel 的配置文件,发现其配置内容中新增加了很多配置。

image-20231102170958936

Sentinel 优化配置

在公共的 sentinel.conf 文件中,还可以通过修改一些其它属性的值来达到对 Sentinel 的 配置优化。

(1) sentinel down-after-milliseconds

image-20230810152140034

	每个 Sentinel 会通过定期发送 ping 命令来判断 'master、slave 及其它 Sentinel 是否存活'。如果 Sentinel 在该属性指定的时间内没有收到它们的响应,那么该 Sentinel 就会主观认为该主机宕机。默认为 30 秒。
(2) sentinel parallel-syncs

在这里插入图片描述

	该属性用于指定,在故障转移期间,即老的 master 出现问题,新的 master 刚晋升后,允许多少个 slave 同时从新 master 进行数据同步。默认值为 1 表示所有 slave 逐个的从新 master进行数据同步。一个一个来
(3) sentinel failover-timeout

在这里插入图片描述

指定故障转移的超时时间,默认时间为 3 分钟。该超时时间的用途很多:
	由于第一次故障转移失败,在同一个 master 上进行第二次故障转移尝试的时间为该failover-timeout 的两倍(老的master宕机了,选举出了一个新的master,在这个上面执行slave no one ,但是失败了,尝试三分钟后就不要这个了直接断开,重新从剩余几个从机下面再选一个master,这次如果失败的话重试时间为6分钟。)
	新 master 晋升完毕,slave 从老 master 强制转到新 master 进行数据同步的时间阈值。
	取消正在进行的故障转换所需的时间阈值。(选举了一个新的master,还没有进行配置更新,sentinel后悔了,也是这个时间)
	新 master 晋升完毕,所有 replicas 的配置文件更新为新 master 的时间阈值。(过了这个时间,就不会按照上面一个一个同步了,就会一起同步配置文件了)
(4) sentinel deny-scripts-reconfig

image-20230810152315430

	指定是否可以通过命令 sentinel set 动态修改 notification-script 与 client-reconfig-script 两个脚本。默认是不能的。这两个脚本如果允许动态修改,可能会引发安全问题。
(5) 动态修改配置

image-20230810152726999

​ 通过 redis-cli 连接上 Sentinel 后,通过 sentinel set 命令可动态修改配置信息。例如,下 面的命令动态修改了 sentinel monitor 中的 quorum 的值。下表是 sentinel set 命令支持的参数:

image-20230810152748610

哨兵机制原理

三个定时任务

Sentinel 维护着三个定时任务以监测 Redis 节点及其它 Sentinel 节点的状态。

(1) info 任务
每个 Sentinel 节点每 10 秒就会'向 Redis 集群'中的'每个节点'发送 info 命令,以获得最新的 Redis 拓扑结构。
(2) 心跳任务
	每个Sentinel节点每1秒就会向所有Redis节点及其它Sentinel节点发送一条ping命令,以检测这些节点的存活状态。该任务是判断节点在线状态的重要依据。
(3) 发布/订阅任务
	每个 Sentinel 节点在启动时都会向所有 Redis 节点订阅_ _sentinel_ _:hello 主题的信息,当 Redis 节点中该主题的信息发生了变化,就会立即通知到所有订阅者。每个sentinel既是主题信息的订阅者,也是发布者
    sentinel向redis节点发送信息订阅这个,另一个sentinel也订阅,订阅的时候发现有其他订阅的,就会拿到这个向节点订阅的sentinel信息。也就是sentinel之间通过redis节点进行互换信息。
	启动后,每个 Sentinel 节点每 2 秒就会向每个 Redis 节点发布一条_ _sentinel_ _:hello 主题的信息,该信息是当前 Sentinel 对每个 Redis 节点在线状态的判断结果及当前 Sentinel 节点信息。
	当 Sentinel 节点接收到_ _sentinel_ _:hello 主题信息后,就会读取并解析这些信息,然后'主要完成以下三项工作':
		如果发现有新的 Sentinel 节点加入,则记录下新加入 Sentinel 节点信息,并与其建立连接。
		如果发现有 Sentinel Leader 选举的选票信息,则执行 Leader 选举过程。
		汇总其它 Sentinel 节点对当前 Redis 节点在线状态的判 断结果,作为 Redis 节点客观下线的判断依据。
Redis 节点下线判断

对于每个 Redis 节点在线状态的监控是由 Sentinel 完成的。

(1) 主观下线
	每个 Sentinel 节点每秒就会向每个 Redis 节点发送 ping 心跳检测,如果 Sentinel 在down-after-milliseconds 时间内没有收到某 Redis 节点的回复,则 Sentinel 节点就会对该 Redis节点做出“下线状态”的判断。这个判断仅仅是当前 Sentinel 节点的“一家之言”,所以称为主观下线。'单个 sentinel 自己主观的'
(2) 客观下线
	当 Sentinel 主观下线的节点是 master 时,该 Sentinel 节点会向每个其它 Sentinel 节点发送 sentinel is-master-down-by-addr 命令,以询问其对 master 在线状态的判断结果。这些Sentinel 节点在收到命令后会向这个发问 S entinel 节点响应 0(在线)或 1(下线)。当 Sentinel收到超过 quorum 个下线判断后,就会对 master 做出客观下线判断。
	所以客观下线的判断方式有两种
		第一个是,通过发布订阅方式,这个是被动的,因为被动会有一个2s的同步信息时间,redis的master宕机2s是灾难性的,所有会有主动发送命令模式
		第二个是:主动发送命令,当有sentinel发现一个master主观下线时,会立即向其他sentinel发送命令,询问master的在线状态,并判断

masterName是对某个master+slave组合的一个区分标识(一套sentinel可以监听多组master+slave这样的组合)

image-20231106153231784

'quorum这个参数是进行客观下线的一个依据,法定票数'
意思是至少有quorum个sentinel认为这个master有故障才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因导致无法连接master,而此时master并没有出现故障,所以这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用。
Sentinel Leader 选举
Sentinel 节点对 master 做出客观下线判断后会由 'Sentinel Leader' 来完成后续的故障转移,即 Sentinel 集群中的节点也并非是对等节点,是存在 'Leader' 与 'Follower' 的。
	Sentinel 集群的 Leader 选举是通过 Raft 算法实现的。Raft 算法比较复杂,后面会详细学习。这里仅简单介绍一下大致思路。
	每个选举参与者都具有当选 Leader 的资格,当其完成了“客观下线”判断后,就会立即“毛遂自荐”推选自己做 Leader,然后将自己的提案发送给所有参与者。其它参与者在收到提案后,只要自己手中的选票没有投出去,其就会立即通过该提案并将同意结果反馈给提案者,后续再过来的提案会由于该参与者没有了选票而被拒绝。当提案者收到了同意反馈数量大于等于 max(quorum,sentinelNum/2+1)时,该提案者当选 Leader'说明':
		在网络没有问题的前提下,基本就是谁先做出了“客观下线”判断,谁就会首先发起Sentinel Leader 的选举,谁就会得到大多数参与者的支持,谁就会当选 LeaderSentinel Leader 选举会在次故障转移发生之前进行。选择是非常快的,基本在 200ns 时间就选举完了,1s = 10^9 ns
		故障转移结束后 Sentinel 不再维护这种 Leader-Follower 关系,即 Leader 不再存在。因为reids故障不常见,所有不需要一直维护这个关系
master 选择算法

在这里插入图片描述

在进行故障转移时,Sentinel Leader 需要从所有 RedisSlave 节点中选择出新的 Master。其选择算法为:
1) '过滤'掉所有'主观下线的',或'心跳没有响应 Sentinel',或' replica-priority 值为 0 的' Redis节点
2) 在剩余 Redis 节点中选择出 'replica-priority 最小'的的节点列表。'如果只有一个节点,则直接返回',否则,继续
3) 从优先级'相同的节点列表'中选择'复制偏移量最大的节点'。'如果只有一个节点,则直接返回',否则,继续
4) 从复制偏移值量相同的节点列表中选择'动态 ID 最小的节点'返回
故障转移过程

在这里插入图片描述

Sentinel Leader 负责整个故障转移过程,经历了如上步骤:
1) Sentinel Leader 根据 master 选择算法选择出一个 slave 节点作为新的 master
2) Sentinel Leader 向新 master 节点发送 slaveof no one 指令,使其晋升为 master
3) Sentinel Leader 向新 master 发送 info replication 指令,获取到 master 的动态 ID
4) Sentinel Leader 向其余 Redis 节点发送消息,以告知它们新 master 的动态 ID
5) Sentinel Leader 向其余 Redis 节点发送 slaveof <mastIp> <masterPort>指令,使它们成为新master 的 slave
6) Sentinel Leader 从所有 slave 节点中每次选择出 parallel-syncs 个 slave 从新 master 同步数据,直至所有 slave 全部同步完毕,这里如果超过了设置的超时时间,他就不会按照设定的 parallel-syncs 个执行了,就会自己判断取多少个
7) 故障转移完毕
节点上线

不同的节点类型,其上线的方式也是不同的。

(1) 原 Redis 节点上线
	无论是原下线的 master 节点还是原下线的 slave 节点,只要是原 Redis 集群中的节点上线,只需启动 Redis 即可。因为每个 Sentinel 中都保存有原来其监控的所有 Redis 节点列表,Sentinel 会定时查看这些 Redis 节点是否恢复。如果查看到其已经恢复,则会命其从当前 master 进行数据同步。
	不过,如果是原 master 上线,在新 master 晋升后 Sentinel Leader 会立即先将原 master节点更新为 slave,然后才会定时查看其是否恢复。
(2) 新 Redis 节点上线
	如果需要在 Redis 集群中添加一个新的节点,其未曾出现在 Redis 集群中,则'上线操作只能手工完成'。即添加者在添加之前必须知道当前 master 是谁,然后在新节点启动后运行slaveof 命令加入集群。
(3) Sentinel 节点上线
	如果要添加的是 Sentinel 节点,无论其是否曾经出现在 Sentinel 集群中,都需要手工完成。即添加者在添加之前必须知道当前 master 是谁,然后在配置文件中修改 sentinel monitor属性,指定要监控的 master。然后启动 Sentinel 即可。

哨兵使用建议

哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
哨兵节点的数量应该是奇数
各个哨兵节点的配置应一致
如果哨兵节点部署在Docker等容器里面,尤其要注意端口的正确映射
哨兵集群+主从复制,并不能保证数据零丢失。新master上位的时间中,数据可能会丢失  --  引出下面集群

CAP 定理

概念
CAP 定理指的是在一个分布式系统中,一致性 Consistency、可用性 Availability、分区容错性 Partition tolerance,三者不可兼得。
	'一致性(C)':分布式系统中多个主机之间是否能够保持数据一致的特性。即,当系统数据发生更新操作后,各个主机中的数据仍然处于一致的状态。
	'可用性(A)':系统提供的服务必须一直处于可用的状态,即对于用户的每一个请求,系统总是可以在有限的时间内对用户做出响应。
	'分区容错性(P)':分布式系统在遇到任何网络分区故障时,仍能够保证对外提供满足一致性和可用性的服务。
定理
	CAP 定理的内容是:对于分布式系统,网络环境相对是不可控的,出现网络分区是不可避免的,因此系统必须具备分区容错性。但系统不能同时保证一致性与可用性。即要么 CP,要么 AP。
	集群中同步数据一定是需要时间的,所以一致性和可用性一定是二选一的
	
BASE 理论
	BASEBasically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的简写,BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 CAP 定理逐步演化而来的。
	BASE 理论的核心思想是:'即使无法做到强一致性,但每个系统都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性'。
(1) 基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。
(2) 软状态
	软状态,是指允许系统数据存在的中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统主机间进行数据同步的过程存在一定延时。软状态,其实就是一种灰度状态,过渡状态。
(3) 最终一致性
	最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要保证系统数据的实时一致性。
从客户端访问到一致性内容的'时间角度'来说
'实时一致性':要求数据内容一旦发生更新,客户端立刻可以访问到最新的数据。所以,在集群环境下,该特性是无法存在的,只存在于单机环境中。
'最终一致性':数据内容发生更新后,经过一小段时间后,客户端可以访问到最新的数据。

    
单从客户端访问到的内容'角度(不说时间问题)'来说   
'强一致性':也称为严格一致性。要求客户端访问到的一定是更新过的新数据。
'弱一致性':允许客户端从集群不同节点访问到的数据是不一致的。
CAP 的应用

下面将生产中常见到的一些中间件与服务器集群的 CAP 特性进行分析。

(1) Zookeeper 与 CAP
	Zookeeper 遵循的是 CP 模式,即保证了一致性,但牺牲了可用性。当 Leader 节点中的数据发生了变化后,在 Follower 还没有同步完成之前,整个 Zookeeper集群是不对外提供服务的。如果此时有客户端来访问数据,则客户端会因访问超时而发生重试。不过,由于 Leader 的选举非常快,所以这种重试对于用户来说几乎是感知不到的。所以说,Zookeeper 保证了一致性,但牺牲了可用性。
(2) Consul 与 CAP
Consul 遵循的是 CP 模式,即保证了一致性,但牺牲了可用性。
(3) Redis 与 CAP
Redis 遵循的是 AP 模式,即保证了可用性,但牺牲了一致性。
(4) Eureka 与 CAP
Eureka 遵循的是 AP 模式,即保证了可用性,但牺牲了一致性。
(5) Nacos 与 CAP
Nacos 在做注册中心时,默认是 AP 的。但其也支持 CP 模式,但需要用户提交请求进行转换。

Raft 算法

基础
	用在sentinel哨兵选举过程中
	Raft 算法是一种通过对'日志复制管理'来达到集群节点'一致性'的算法。这个日志复制管理发生在集群节点中的 LeaderFollowers 之间。'Raft 通过选举出的 Leader 节点'负责'管理日志复制'过程,以实现各个节点间数据的一致性。 
	两个过程:
		Leader选举
		数据同步
角色、任期及角色转变

image-20230810153933528

Raft 中,节点有三种角色:
	'Leader''唯一'负责处理客户端'写请求'的节点;也可以处理客户端读请求;同时负责日志复制工作
	'Candidate':Leader 选举的候选人,其可能会成为 Leader。是一个选举中的过程角色 
	'Follower':可以处理客户端读请求;负责同步来自于 Leader 的日志;当接收到其它Cadidate 的投票请求后可以进行投票;当发现 Leader 挂了,其会转变为 Candidate 发起Leader 选举
leader 选举

通过 Raft 算法首先要实现集群中 Leader 的选举。

(1) 我要选举
若 follower 在心跳超时范围内没有接收到来自于 leader 的心跳,则认为 leader 挂了。此时其首先会使其本地 term 增一。然后 follower 会完成以下步骤:
	此时若接收到了其它 candidate 的投票请求,则会将选票投给这个 candidate
	由 follower 转变为 candidate
	若之前尚未投票,则向自己投一票
	向其它节点发出投票请求,然后等待响应
(2) 我要投票
follower 在接收到投票请求后,其会根据以下情况来判断是否投票:
	发来投票请求的 candidate 的 term 不能小于我的 term
	在我当前 term 内,我的选票还没有投出去
	若接收到多个 candidate 的请求,我将采取 first-come-first-served 方式投票
(3) 等待响应
当一个 Candidate 发出投票请求后会等待其它节点的响应结果。这个响应结果可'能有三种情况':
	收到过半选票,成为新的 leader。然后会将消息广播给所有其它节点,以告诉大家我是新的 Leader 了
	接收到别的 candidate 发来的新 leader 通知,比较了新 leader 的 term 并不比自己的 term小,则自己转变为 follower
	经过一段时间后,没有收到过半选票,也没有收到新 leader 通知,则重新发出选举
(4) 选举时机
	在很多时候,当 Leader 真的挂了,Follower 几乎同时会感知到,所以它们几乎同时会变为 candidate 发起新的选举。此时就可能会出现较多 candidate 票数相同的情况,即无法选举出 Leader。
	为了防止这种情况的发生,Raft 算法其采用了 randomized election timeouts 策略来解决这个问题。其会为这些 Follower 随机分配一个选举发起时间 election timeout,这个 timeout在 150-300ms 范围内。只有到达了 election timeout 时间的 Follower 才能转变为 candidate,否则等待。那么 election timeout 较小的 Follower 则会转变为 candidate 然后先发起选举,一般情况下其会优先获取到过半选票成为新的 leader。
数据同步

在 Leader 选举出来的情况下,通过日志复制管理实现集群中各节点数据的同步。

(1) 状态机
	Raft 算法一致性的实现,是基于日志复制状态机的。
	状态机的最大特征是,不同 Server中的状态机若当前状态相同,然后接受了相同的输入,则一定会得到相同的输出。

在这里插入图片描述

(2) 处理流程

在这里插入图片描述

当 leader 接收到 client 的写操作请求后,大体会经历以下流程:

	leader 在接收到 client 的写操作请求后,leader 会将数据与 term 封装为一个 box,并随着下一次心跳发送给所有 followers,以征求大家对该 box 的意见。同时在本地将数据封装为日志
	follower 在接收到来自 leader 的 box 后首先会比较该 box 的 term 与本地记录的曾接受过的 box 的最大 term,只要不比自己的小就接受该 box,并向 leader 回复同意。同时会将该 box 中的数据封装为日志。
	当 leader 接收到过半同意响应后,会将日志 commit 到自己的状态机,状态机会输出一个结果,同时日志状态变为了 committed
	同时 leader 还会通知所有 follower 将日志 commit 到它们本地的状态机,日志状态变为了 committed
	在 commit 通知发出的同时,leader 也会向 client 发出成功处理的响应
(3) AP 支持

在这里插入图片描述

	Log 由 term index、log index 及 command 构成。为了保证可用性,各个节点中的日志可以不完全相同,但 leader 会不断给 follower 发送 box,以使各个节点的 log 最终达到相同。即 raft 算法不是强一致性的,而是最终一致的。
脑裂
	Raft 集群存在脑裂问题。在多机房部署中,由于网络连接问题,很容易形成多个分区。而多分区的形成,很容易产生脑裂,从而导致数据不一致。
	由于三机房部署的容灾能力最强,所以生产环境下,三机房部署是最为常见的。下面以三机房部署为例进行分析,根据机房断网情况,可以分为五种情况:
(1) 情况一–不确定

在这里插入图片描述

	这种情况下,B 机房中的主机是感知不到 Leader 的存在的,所以 B 机房中的主机会发起新一轮的 Leader 选举。由于 B 机房与 C 机房是相连的,虽然 C 机房中的 Follower 能够感知到 A 机房中的 Leader,但由于其接收到了更大 term 的投票请求,所以 C 机房的 Follower也就放弃了 A 机房中的 Leader,参与了新 Leader 的选举。
	若新 Leader 出现在 B 机房,A 机房是感知不到新 Leader 的诞生的,其不会自动下课,所以会形成脑裂。但由于 A 机房 Leader 处理的写操作请求无法获取到过半响应,所以无法完成写操作。但 B 机房 Leader 的写操作处理是可以获取到过半响应的,所以可以完成写操作。故,A 机房与 B、C 机房中出现脑裂,且形成了数据的不一致。
	若新 Leader 出现在 C 机房,A 机房中的 Leader 则会自动下课,所以不会形成脑裂。
(2) 情况二–形成脑裂

image-20230810155924586

这种情况与情况一基本是一样的。不同的是,一定会形成脑裂,无论新 Leader 在 B 还是 C 机房。
(3) 情况三–无脑裂

image-20230810155947320

A、C 可以正常对外提供服务,但 B 无法选举出新的 Leader。由于 B 中的主机全部变为了选举状态,所以无法提供任何服务,没有形成脑裂。
(4) 情况四–无脑裂

在这里插入图片描述

A、B、C 均可以对外提供服务,不受影响。
(5) 情况五–无脑裂

在这里插入图片描述

A 机房无法处理写操作请求,但可以对外提供读服务。
B、C 机房由于失去了 Leader,均会发起选举,但由于均无法获取过半支持,所以均无法选举出新的 Leader。
Leader 宕机处理
(1) 请求到达前 Leader 挂了
	client 发送写操作请求到达 Leader 之前 Leader 就挂了,因为请求还没有到达集群,所以这个请求对于集群来说就没有存在过,对集群数据的一致性没有任何影响。Leader 挂了之后,会选举产生新的 Leader。
	由于 Stale Leader 并未向 client 发送成功处理响应,所以 client 会重新发送该写操作请求。
(2) 未开始同步数据前 Leader 挂了
	client 发送写操作请求给 Leader,请求到达 Leader 后,Leader 还没有开始向 Followers发出数据 Leader 就挂了。这时集群会选举产生新的 Leader。Stale Leader 重启后会作为Follower 重新加入集群,并同步新 Leader 中的数据以保证数据一致性。之前接收到 client 的数据被丢弃。
	由于 Stale Leader 并未向 client 发送成功处理响应,所以 client 会重新发送该写操作请求。
(3) 同步完部分后 Leader 挂了
	client 发送写操作请求给 Leader,Leader 接收完数据后向所有 Follower 发送数据。在部分 Follower 接收到数据后 Leader 挂了。由于 Leader 挂了,就会发起新的 Leader 选举。
		若 Leader 产生于已完成数据接收的 Follower,其会继续将前面接收到的写操作请求转换为日志,并写入到本地状态机,并向所有 Flollower 发出询问。在获取过半同意响应后会向所有 Followers 发送 commit 指令,同时向 client 进行响应。
		若 Leader 产生于尚未完成数据接收的 Follower,那么原来已完成接收的 Follower 则会放弃曾接收到的数据。由于 client 没有接收到响应,所以 client 会重新发送该写操作请求。
(4) commit 通知发出后 Leader 挂了
	client 发送写操作请求给 Leader,Leader 也成功向所有 Followers 发出的 commit 指令,并向 client 发出响应后,Leader 挂了。
	由于 Stale Leader 已经向 client 发送成功接收响应,且 commit 通知已经发出,说明这个写操作请求已经被 server 成功处理。
Raft 算法动画演示

在网络上有一个关于 Raft 算法的动画,其非常清晰全面地演示了 Raft 算法的工作原理。 该动画的地址为:http://thesecretlivesofdata.com/raft/

分布式系统(集群)

	Redis 分布式系统,官方称为 Redis ClusterRedis 集群,其是 Redis 3.0 开始推出的分布 式解决方案。其可以很好地**解决不同 Redis 节点存放不同数据**,并将用户请求方便地路由到不同 Redis 的问题。
	Redis集群支持多个 master,每个master又可以挂在多个slave
	'由于cluster自带sentinel的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能'  --  妈的哨兵白学
    '客户端与Redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可'
     redis集群'不保证强一致性',这意味着在特定的条件下,redis集群可能会丢掉一些被系统收到的写入命令

在这里插入图片描述

在这里插入图片描述

数据分区算法

​ 分布式数据库系统会根据不同的数据分区算法,将数据分散存储到不同的数据库服务器节点上,每个节点管理着整个数据集合中的一个子集。

在这里插入图片描述

常见的数据分区规则有两大类:'顺序分区''哈希分区'

顺序分区

	顺序分区规则可以将数据按照'某种顺序平均分配到不同的节点'。不同的顺序方式,产生了不同的分区算法。例如,'轮询分区算法'、'时间片轮转分区算法'、'数据块分区算法'、'业务主题分区算法'等。由于这些算法都比较简单,所以这里就不展开描述了。
(1) 轮询分区算法
	每产生一个数据,就依次分配到不同的节点。该算法'适合于数据问题不确定的场景'。其分配的结果是,在数据总量非常庞大的情况下,每个节点中数据是很平均的。但生产者与数据节点间的连接要长时间保持。
(2) 时间片轮转分区算法
	在某人固定长度的时间片内的数据都会分配到一个节点。时间片结束,再产生的数据就会被分配到下一个节点。这些节点会被依次轮转分配数据。该算法可能会出现节点数据不平均的情况(因为每个时间片内产生的数据量可能是不同的)。但生产者与节点间的连接只需占用当前正在使用的这个就可以,其它连接使用完毕后就立即释放。
(3) 数据块分区算法
在整体数据总量确定的情况下,根据各个节点的存储能力,可以将连接的某一整块数据分配到某一节点。
(4) 业务主题分区算法
数据可根据不同的业务主题,分配到不同的节点。

哈希分区

​ 哈希分区规则是充分利用数据的哈希值来完成分配,对数据哈希值的不同使用方式产生了不同的哈希分区算法。使用确定性哈希函数,这意味着给定的key将多次始终映射到同一分片上。哈希分区算法相对较复杂,这里详细介绍几种常见的哈希分区算法。

(1) 节点取模分区算法
	该算法的前提是,'每个节点都已分配好了一个唯一序号',对于 N 个节点的分布式系统,其序号范围为[0, N-1]。然后选取数据本身或可以代表数据特征的数据的一部分作为 key,计算 hash(key)与节点数量 N 的模,该计算结果即为该数据的存储节点的序号。
	该算法最大的优点是简单,但其也存在较严重的不足。如果分布式系统扩容或缩容,已经存储过的数据需要根据新的节点数量 N 进行数据迁移,否则用户根据 key 是无法再找到原来的数据的。
    生产中扩容一般采用翻倍扩容方式,以减少扩容时数据迁移的比例。数据迁移时只迁移一半数据
(2) 一致性哈希分区算法
一致性 hash 算法通过一个叫作一致性 hash 环的数据结构实现。这个环的起点是 0,终点是 2^32- 1,并且起点与终点重合。环中间的整数按逆/顺时针分布,故这个环的整数分布范围是[0, 2^32-1]。

在这里插入图片描述

在这里插入图片描述

	上图中存在四个对象 o1、o2、o3、o4,分别代表四个待分配的数据,红色方块是这四个数据的 hash(o)Hash 环中的落点。同时,图上还存在三个节点 m0、m1、m2,绿色圆圈是这三节点的 hash(m)Hash 环中的落点。
	现在要为数据分配其要存储的节点。该数据对象的 hash(o) 按照逆/顺时针方向距离哪个节点的 hash(m)最近,就将该数据存储在哪个节点。这样就会形成上图所示的分配结果。
	该算法的最大优点是,节点的扩容与缩容,仅对按照逆/顺时针方向距离该节点最近的节点有影响,对其它节点无影响。
	当节点数量较少时,非常容易形成'数据倾斜问题',且节点变化影响的节点数量占比较大,即影响的数据量较大。所以,该方式'不适合数据节点较少的场景'。
(3) 虚拟槽分区算法
	该算法首先虚拟出一个固定数量的整数集合,该集合中的每个整数称为一个 slot 槽。这个槽的数量一般是远远大于节点数量的。然后再将所有 slot 槽平均映射到各个节点之上。例如,Redis 分布式系统中共虚拟了 16384 个 slot 槽,其范围为[0, 16383]。假设共有 3 个节点,那么 slot 槽与节点间的映射关系如下图所示:

在这里插入图片描述

	而数据只与 slot 槽有关系,与节点没有直接关系。数据只通过其 key 的 hash(key)映射到slot 槽:slot = hash(key) % slotNums。这也是该算法的一个优点,解耦了数据与节点,客户端无需维护节点,只需维护与 slot 槽的关系即可。
	Redis 数据分区采用的就是该算法。其计算槽点的公式为:slot = CRC16(key) % 16384CRC16()是一种带有校验功能的、具有良好分散功能的、特殊的 hash 算法函数。'其实 Redis 中计算槽点的公式不是上面的那个,而是:slot = CRC16(key) & 16383'。
	若要计算 a % b,如果 b 是 2 的整数次幂,那么 a % b = a & (b-1)。位运算很快,数学运算慢
        
'为什么redis集群的最大槽是16364?''
正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。
这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。
同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。
因此16k处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳1000个矩阵,但数量足够少,可以轻松地将插槽配置作为原始位图传播。请注意,在小型群集中,位图将难以压缩,因为当N较小时,位图将设置的slot / N位占设置位的很大百分比。

(1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb 
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb 
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。


(2)redis的集群主节点数量基本不可能超过1000个。
集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。

(3)槽位越小,节点少的情况下,压缩比高,容易传输
Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。 
        

系统搭建与运行

系统搭建

(1) 系统架构
下面要搭建的 Redis 分布式系统由 6 个节点构成,这 6 个节点的地址及角色分别如下表所示。
一个 master 配备一个 slave,不过 master 与 slave 的配对关系,在系统搭建成功后会自动分配

在这里插入图片描述

(2) 删除持久化文件
	先将之前“Redis 主从集群”中在 Redis 安装目录下生成的 RDB 持久化文件 dump638*.conf与 AOF 持久化文件删除。因为 Redis 分布式系统要求创建在一个空的数据库之上。注意,AOF持久化文件全部在 appendonlydir 目录中。

image-20231102173538100

(3) 创建目录
在 Redis 安装目录中 mkdir 一个新的目录 cluster-dis,用作分布式系统的工作目录。
(4) 复制 2 个配置文件
 将cluster 目录中的 redis.conf 与 redis6380.conf 文件复制到 cluster-dis 目录。

在这里插入图片描述

(5) 修改 redis.conf

对于 redis.conf 配置文件,主要涉及到以下三个四个属性:

A、dir

在这里插入图片描述

指定工作目录为前面创建的 cluster-dis 目录。持久化文件、节点配置文件将来都会在工作目录中自动生成。
B、 cluster-enabled

在这里插入图片描述

该属性用于开启 Redis 的集群模式。
C、 cluster-config-file

image-20231102175143413

	该属性用于指定“集群节点”的配置文件。该文件会在第一次节点启动时自动生成,其生成的路径是在 dir 属性指定的工作目录中。在集群节点信息发生变化后(如节点下线、故障转移等),节点会自动将集群状态信息保存到该配置文件中。
	不过,该属性在这里仍保持注释状态。在后面的每个节点单独的配置文件中配置它。
D、cluster-node-timeout

在这里插入图片描述

用于指定“集群节点”间通信的超时时间阈值,单位毫秒。
(6) 修改 redis6380.conf

在这里插入图片描述

仅添加一个 cluster-config-file 属性即可。
(7) 复制 5 个配置文件
使用 redis6380.conf 复制出 5 个配置文件 redis6381.conf、redis6382.conf、redis6383.conf、redis6384.conf、redis6385.conf。
(8) 修改 5 个配置文件
	修改 5 个配置文件 redis6381.conf、redis6382.conf、redis6383.conf、redis6384.conf、redis6385.conf 的内容,将其中所有涉及的端口号全部替换为当前文件名称中的端口号。例如,下面的是 redis6381.conf 的配置文件内容。

系统启动与关闭

(1) 启动节点

启动所有 Redis 节点。

image-20231102183123764

此时查看 cluster-dis 目录,可以看到生成了 6 个 nodes 的配置文件。

image-20231102183149402

(2) 创建系统
6 个节点启动后,它们仍是 6 个独立的 Redis,通过 redis-cli --cluster create 命令可将 6个节点创建了一个分布式系统。

image-20231102183216965

该命令用于将指定的 6 个节点连接为一个分布式系统。--cluster replicas 1 指定每个 master 会带有一个 slave 作为副本。
回车后会立即看到如下日志

image-20231102183235031

输入 yes 后回车,系统就会将以上显示的动态配置信息真正的应用到节点上,然后就可以看到如下日志:

image-20231102183310999

(3) 测试系统

在这里插入图片描述

	通过 cluster nodes 命令可以查看到系统中各节点的关系及连接情况。只要能看到每个节点给出 connected,就说明分布式系统已经成功搭建。不过,对于客户端连接命令 redis-cli,需要注意两点:
		'redis-cli 带有-c 参数,表示这是要连接一个“集群”,而非是一个节点,否则会导致路由失效'
        路由失效:进的6380,写操作分配到了6381,不加-c就无法路由过去就会报错
		端口号可以使用 6 个中的任意一个。
(4) 关闭系统
对于分布式系统的关闭,只需将各个节点 shutdown 即可。

在这里插入图片描述

集群操作

连接集群

无论要怎样操作分布式系统,都需要首先连接上。
与之前单机连接相比的唯一区别就是增加了参数-c。

在这里插入图片描述

写入数据

key 单个写入
无论 value 类型为 String 还是 List、Set 等集合类型,只要写入时操作的是一个 key,那么在分布式系统中就没有问题。

image-20231102183636618

key 批量操作
	对一次写入多个 key 的操作,由于多个 key 会计算出多个 slot,多个 slot 可能会对应多个节点。而由于一次只能写入一个节点,所以该操作会报错。

在这里插入图片描述

	不过,系统也提供了一种对批量 key 的操作方案,为这些 key 指定一个统一的 group,让这个 group 作为计算 slot 的唯一值。

在这里插入图片描述

集群查询

查询 key 的 slot
通过 cluster keyslot 可以查询指定 key 的 slot。例如,下面是查询 emp 的 slot。

在这里插入图片描述

查询 slot 中 key 的数量
通过 cluster countkeysinslot 命令可以查看到指定 slot 所包含的 key 的个数。

在这里插入图片描述

查询 slot 中的 key
通过 cluster getkeysinslot 命令可以查看到指定 slot 所包含的 key。

在这里插入图片描述

故障转移

	分布式系统中的某个 master 如果出现宕机,那么其相应的 slave 就会自动晋升为 master。如果原 master 又重新启动了,那么原 master 会自动变为新 master 的 slave。
(1) 模拟故障
通过 cluster nodes 命令可以查看系统的整体架构及连接情况。

image-20231102184102614

当然,也可以通过 info replication 查看当前客户端连接的节点的角色。可以看到,6381 节点是 master,其 slave 为 6383 节点。

在这里插入图片描述

为了模拟 6381 宕机,直接将其 shutdown。

在这里插入图片描述

通过客户端连接上 6383 节点后可以查看到,其已经自动晋升为了 master。
这个晋升的时间可以会丢失一些被系统收到的写入请求命令--但是这个情况不多

在这里插入图片描述

重启 6381 节点后查看其角色,发现其自动成为了 6383 节点的 slave。

通过CLUSTER FAILOVER 可以将当前机器的主从位置互换,6381 变为主, 6383变为从

在这里插入图片描述

(2) 全覆盖需求
	如果某 slot 范围对应节点的 'master 与 slave 全部宕机',那么整个分布式系统是否还可以 对外提供读服务,就取决于属性 cluster-require-full-coverage 的设置

image-20231102184500884

该属性有两种取值:
	yes:默认值。要求所有 slot 节点必须全覆盖的情况下系统才能运行。
 	no:slot 节点不全的情况下系统也可以提供查询服务。相当于丢数据的情况下也允许查询

集群扩容

下面要在正在运行的分布式系统中添加两个新的节点:端口号为 6386 的节点为 master节点,其下会有一个端口号为 6387 的 slave 节点。
(1) 复制并修改 2 个配置文件
	使用 redis6380.conf 复制出 2 个配置文件 redis6386.conf 与 redis6387.conf,并修改其中的各处端口号为相应端口号,为集群扩容做前期准备。
(2) 启动系统与 2 个节点
	由于要演示的是在分布式系统运行期间的动态扩容,所以这里先启动分布式系统。
	要添加的两个节点是两个 Redis,所以需要先将它们启动。只不过,在没有添加到分布式系统之前,它们两个是孤立节点,每个节点与其它任何节点都没有关系。

在这里插入图片描述

(3) 添加 master 节点

image-20231102184702826

	通过命令 redis-cli --cluster add-node {newHost}:{newPort} {existHost}:{existPort}可以将新的节点添加到系统中。其中{newHost}:{newPort}是新添加节点的地址,{existHost}:{existPort}是原系统中的任意节点地址。
	添加成功后可看到如下日志。

image-20231102184726112

	添加成功后,通过 redis-cli -c -p 6386 cluster nodes 命令可以看到其它 master 节点都分配有 slot,只有新添加的 master 还没有相应的 slot。当然,通过该命令也可以看到该新节点的动态 ID

image-20231102184803367

(4) 分配 slot
	为新的 master 分配的 slot 来自于其它节点,总 slot 数量并不会改变。所以 slot 分配过
程本质是一个 slot 的移动过程。
	通过 redis-cli –c --cluster 'reshard' {existIP}:{existPort}命令可开启 slot 分配流程。其中地址{existIP}:{existPort}为分布式系统中的任意节点地址。

image-20231102184831890

该流程中会首先查询出当前节点的 slot 分配情况。

image-20231102184858670

然后开始 Q&A 交互。一共询问了四个问题,这里有三个:
	准备移动多少 slot?
	准备由谁来接收移动的 slot?
	选择要移动 slot 的源节点。有两种方案。如果选择键入 all,则所有已存在 slot 的节点
都将作为 slot 源节点,即该方案将进行一次 slot 全局大分配。也可以选择其它部分节点作为 slot 源节点。此时将源节点的动态 ID 复制到这里,每个 ID 键入完毕后回车,然后再复制下一个 slot 源节点动态 ID,直至最后一个键入完毕回车后再键入 done。这里键入的是 all,进行全局大分配。

image-20231102184932432

其首先会检测指定的 slot 源节点的数据,然后制定出 reshard 的方案。

在这里插入图片描述

这里会再进行一次 Q&A 交互,询问是否想继续处理推荐的方案。键入 yes,然后开始真 正的全局分配,直至完成。

image-20231102185000055

	此时再通过 redis-cli -c -p 6386 cluster nodes 命令查看节点信息,可以看到 6386 节点中已经分配了 slot,只不过分配的 slot 编号并不连续。master 节点新增完成。

在这里插入图片描述

(5) 添加 slave 节点
	现要将 6387 节点添加为 6386 节点的 slave。当然,首先要确保 6387 节点的 Redis 是启动状态。
	通过 redis-cli --cluster add-node {newHost}:{newPort} {existHost}:{existPort} --cluster-slave 
--cluster-master-id masterID 命令可将新添加的节点直接添加为指定 master 的 slave。

在这里插入图片描述

回车后可看到如下的日志,说明添加成功。

image-20231102185116145

此时再通过 redis-cli -c -p 6386 cluster nodes 命令可以看到其已经添加成功,且为指定master 的 slave。

在这里插入图片描述

在这里插入图片描述

集群缩容

下面要将 slave 节点 6387 与 master 节点 6386 从分布式系统中删除。
(1) 删除 slave 节点
对于 slave 节点,可以直接通过 redis-cli --cluster del-node <delHost>:<delPort> delNodeID 命令删除。

在这里插入图片描述

此时再查看集群,发现已经没有了 6387 节点。

image-20231102185237098

(2) 移出 master 的 slot
在删除一个 master 之前,必须要保证该 master 上没有分配有 slot。否则无法删除。所以,在删除一个 master 之前,需要先将其上分配的 slot 移出.

image-20231102185315551

以上交互指定的是将 6386 节点中的 1999 个 slot 移动到 6380 节点。
注意:
	要删除的节点所包含的 slot 数量在前面检测结果中都是可以看到的,例如,6386 中的 并不是 2000 个,而是 1999 个
	What is the receiving node ID?仅能指定一个接收节点
回车后继续。

image-20231102185349444

在这里插入图片描述

此时再查看发现,6386 节点中已经没有 slot 了。

在这里插入图片描述

(3) 删除 master 节点

此时就可以删除 6386 节点了。

在这里插入图片描述

此时再查看集群,发现已经没有 6386 节点了

image-20231102185508207

分布式系统的限制

Redis 的分布式系统存在一些使用限制:
	仅支持 0 号数据库
	批量 key 操作支持有限
	分区仅限于 key
	事务支持有限
	不支持分级管理 -- 只有master 和 slave 不支持,不如分布管理中 slave 也可以有自己的小弟

Redis 缓存

Jedis 客户端

<!--jedis 依赖-->
 <dependency>
	 <groupId>redis.clients</groupId>
	 <artifactId>jedis</artifactId>
	 <version>4.2.0</version>
 </dependency>
 
 <dependency>
 <groupId>junit</groupId>
 	<artifactId>junit</artifactId>
 	<version>4.11</version>
 	<scope>test</scope>
 </dependency>
使用 Jedis 实例
(1) value 为 String 的测试

image-20230810161227868

(2) value 为 Hash 的测试

image-20230810161251849

(3) value 为 List 的测试

image-20230810161308153

(4) value 为 Set 的测试

在这里插入图片描述

(5) value 为 ZSet 的测试

在这里插入图片描述

使用 JedisPool
	如果应用非常频繁地创建和销毁 Jedis 实例,虽然节省了系统资源与网络带宽,但会大大降低系统性能。因为创建和销毁 Socket 连接是比较耗时的。此时可以使用 Jedis 连接池来解决该问题。
	使用 JedisPool 与使用 Jedis 实例的区别是,JedisPool 是全局性的,整个类只需创建一次即可,然后每次需要操作 Redis 时,只需从 JedisPool 中拿出一个 Jedis 实例直接使用即可。使用完毕后,无需释放 Jedis 实例,只需返回 JedisPool 即可。

在这里插入图片描述

使用 JedisPooled

对于每次对 Redis 的操作都需要使用 try-with-resource 块(这个块本身就可以默认jin’xin)是比较麻烦的,而使用 JedisPooled 则无需再使用该结构来自动释放资源了。

在这里插入图片描述

连接 Sentinel 高可用集群
	对于 Sentinel 高可用集群的连接,直接使用 JedisSentinelPool 即可。在该客户端只需注册所有 Sentinel 节点及其监控的 Master 的名称即可,无需出现 master-slave 的任何地址信息。其采用的也是 JedisPool,使用完毕的 Jedis 也需要通过 close()方法将其返回给连接池。

在这里插入图片描述

连接分布式系统
	对于 Redis 的分布式系统的连接,直接使用 JedisCluster 即可。其底层采用的也是 Jedis连接池技术。每次使用完毕后,无需显式关闭,其会自动关闭。
	对于 JedisCluster 常用的构造器有两个。一个是只需一个集群节点的构造器,这个节点可以是集群中的任意节点,只要连接上了该节点,就连接上了整个集群。但该构造器存在一个风险:其指定的这个节点在连接之前恰好宕机,那么该客户端将无法连接上集群。所以,推荐使用第二个构造器,即将集群中所有节点全部罗列出来。这样就会避免这种风险了。

在这里插入图片描述

操作事务
	对于 Redis 事务的操作,Jedis 提供了 multi()、watch()、unwatch()方法来对应 Redis 中的multi、watch、unwatch 命令。Jedis的 multi()方法返回一个 Transaction 对象,其 exec()与 discard()方法用于执行和取消事务的执行。
(1) 抛出 Java 异常

在这里插入图片描述

其输出结果为全部回滚的结果。

在这里插入图片描述

(2) 让 Redis 异常

image-20230810162047611

其输出结果为修改过的值。说明 Redis 运行时抛出的异常不会被 Java 代码捕获到,其不 会影响 Java 代码的执行。

image-20230810162057790

(3) watch()

image-20230810162114942

RedisTemplate

连接单机

依赖配置
<!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
# ========================redis单机=====================
spring.redis.database=0
# 修改为自己真实IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

image-20231107095329908

image-20231107095358449

JDK 序列化方式 (默认)
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer ,
默认情况下,RedisTemplate 使用该数据列化方式,我们来看下源码 RedisTemplate#afterPropertiesSet()

在这里插入图片描述

自定义序列化
配置类方式
redis机器上不会直接显示中文,需要在连接的时候加上  -- raw参数,才能显示

@Configuration
public class RedisConfig
{
    /**
     * redis序列化的工具配置类,下面这个请一定开启配置
     * 127.0.0.1:6379> keys *
     * 1) "ord:102"  序列化过
     * 2) "\xac\xed\x00\x05t\x00\aord:102"   野生,没有序列化过
     * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
     * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
     * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
     * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
     * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

@Configuration
public class RedisConfig {
	@Bean
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
		// 创建RedisTemplate对象
		RedisTemplate<String, Object> template = new RedisTemplate<>();
		// 设置连接工厂
		template.setConnecti onFactory(connectionFactory);
		// 创建JSON序列化工具
		GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
		// 设置Key的序列化
		template.setKeySerializer(RedisSerializer.string());
		template.setHashKeySerializer(RedisSerializer.string());
		// 设置Value的序列化
		template.setValueSerializer(jsonRedisSerializer);
		template.setHashValueSerializer(jsonRedisSerializer);
		// 返回
		return template;
	}
}

在这里插入图片描述

StringRedisTemplate

在这里插入图片描述

在这里插入图片描述

省去了我们自定义RedisTemplate的序列化方式的步骤,而是直接使用,手动给序列化为json即可

RedisTemplate的两种序列化
 	方案一: 自定义RedisTemplate 
			修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer 

​	方案二: 使用StringRedisTemplate 
			写入Redis时,手动把对象序列化为JSON 
			读取Redis时,手动把读取到的JSON反序列化为对象

连接集群

依赖配置
# ========================redis集群=====================
spring.redis.password=111111
# 获取失败 最大重定向次数
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386
其他配置不需要修改,直接使用即可
故障演示
Redis Cluster集群部署采用了33从拓扑结构,数据读写访问master节点, slave节点负责备份。'当master宕机主从切换成功,redis手动OK,but 2个经典故障,SpringBoot客户端没有动态感知到RedisCluster的最新集群信息。'

在这里插入图片描述

故障原因
SpringBoot2.X版本,Redis默认的连接池采用Lettuce,当Redis集群节点发生变化后,Letture是不会刷新节点拓扑的
解决方案
1、jedis(不推荐)

image-20231107102045765

2、重写连接工厂
//仅做参考,不写,不写,不写。'极度不推荐'

@Bean
public DefaultClientResources lettuceClientResources() {
    return DefaultClientResources.create();
}

@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, ClientResources clientResources) {

    ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enablePeriodicRefresh(Duration.ofSeconds(30)) //按照周期刷新拓扑
            .enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
            .build();

    ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
            //redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
            .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
            .topologyRefreshOptions(topologyRefreshOptions)
            .build();

    LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
            .clientResources(clientResources)
            .clientOptions(clusterClientOptions)
            .build();

    RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
    clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
    clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
    LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);
    return lettuceConnectionFactory;
}
3、动态感知拓扑刷新

image-20231107102549315

改写yml
# ========================redis集群=====================
spring.redis.password=111111
# 获取失败 最大重定向次数
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true        --  对比上一次新增的
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000          --  对比上一次新增的
spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386

缓存使用

本地缓存

map中存的所有东西都是再内存中的,直接定义一个map就可以当缓存使用  
本地缓存性能提升比较大。

private Map<String, Object> cache = new HashMap<>();

在分布式情况下会出现的问题在这里插入图片描述

分布式缓存Redis

1)、springboot2.0以后默认使用lettuce操作redis的客户端,它使用netty进行网络通讯
2)、lettuce的bug导致netty堆外内存溢出  -Xmx300m: netty如果没有指定堆外内存,默认使用300m大小  可设置:-Dio.netty.maxDirectMemory
    //解决方案:不能直接使用-Dio.netty.maxDirectMemory只去去调大堆外内存(调大这个解决不了问题)
    1)、升级lettuce客户端。      2)、切换使用jedis (优点:比较简单,缺点:老版客户端,好久没更新)

	  <!--  redis排除 lettuce 客户端,使用  jedis客户端 , 性能会降低一些  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>


redisTemplate:
	lettuce、jedis都是操作redis的底层客户端(随便哪个),spring将前两个客户端再次封装为 redisTemplate,我们不管选择哪个客户端,都可以使用 redisTemplate。

Spring Cache

@EnableCaching      //开启缓存功能 ,在主配置类上
//TODO Cache负责缓存的读写
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)  //category 区名
 								如果 key 是普通字符串需要加上单引号    sync = true加本地锁
1@Cacheable 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
2、value = {"category"}: 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)3、默认行为
     1)、如果缓存中有,方法不再调用,而是直接去调用缓存中的
     2)、key是默认生成的:缓存的名字::SimpleKey::[](自动生成key值)
     3)、缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中,不推荐jdk得序列化方式,下面自定义改为json
     4)、默认时间是 -1:也就是永不过期

     自定义操作:key的生成
          1)、指定生成缓存的key:key属性指定,接收一个SpEl
                  eg: key = "'level1Categorys'"  ,redis的key就为:category::level1Categorys
                  eg: key = "#root.method.name"    直接取本方法的方法名
           2)、指定缓存的数据的存活时间:配置文档中修改存活时间  ---  spring.cache.redis.time-to-live=3600000
           3)、将数据保存为json格式
4、原理:CacheAutoConfiguration 导入了 -> RedisCacheConfiguration -> 自动配置了  RedisCacheManager -> 初始化所有缓存 ->每个缓存决定使用什么配置  -> 如果 redisCacheConfiguration 有就用已有的,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个 RedisCacheConfiguration即可  -> 就会应用到当前 RedisCacheManager 管理的所有缓存分区中

5Spring-Cache的不足之处:
     1)、读模式
           缓存穿透:查询一个null数据。解决方案:缓存空数据
           缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
           缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
     2)、写模式:(缓存与数据库一致)
           1)、读写加锁。
           2)、引入Canal,感知到MySQL的更新去更新Redis
           3)、读多写多,直接去数据库查询就行

总结:
   常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
   特殊数据:特殊设计 
     
原理:
    CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
               
               
清除一个缓存 : @CacheEvict(value = "category", key = "'getLevel1Categorys'")
清除多个缓存 : 
               1)、按照缓存名字
               @Caching(evict = {
            			@CacheEvict(value = "category", key = "'getLevel1Categorys'"),
           	 		@CacheEvict(value = "category", key = "'getCatalogJson'")
    		 	})
               2)、按照分区,删除这个分区的所有缓存
                @CacheEvict(value = "category", allEntries = true)

高并发问题

Redis 做缓存虽减轻了 DBMS 的压力,减小了 RT,但在高并发情况下也是可能会出现各种问题的。

缓存穿透
	'存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库'
	当用户访问的数据既不在缓存也不在数据库中时,就会导致每个用户查询都会“穿透”缓存“直抵”数据库。这种情况就称为缓存穿透。当高度发的访问请求到达时,缓存穿透不仅增加了响应时间,而且还会引发对 DBMS 的高并发查询,这种高并发查询很可能会导致DBMS 的崩溃。缓存穿透产生的主要原因有两个:一是在数据库中没有相应的查询结果,二是查询结果为空时,不对查询结果进行缓存。

常见的解决方案有三种:

'缓存空对象'
​		优点:实现简单,维护方便 
​		缺点: 额外的内存消耗 可能造成短期的不一致 

'布隆过滤' 
​		优点:内存占用较少,没有多余key
​		缺点: 实现复杂 存在误判可能
    
'bloomfilter过滤器'
​		优点:独立于redis实现,实现简单。
​		缺点:考虑分布式的情况 
    
	'缓存空对象思路分析':简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis 中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了

	'布隆过滤':布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思 想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问 redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,假设布隆过滤器判断这个数据不存在,则直接返回 这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突

image-20230525172153961

在这里插入图片描述

image-20230604193517098

方案1:空对象缓存或缺省值

一般来说没问题

image-20230604193622623

but:黑客或者恶意攻击

黑客会对你的系统进行攻击,哪一个不存在的id去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉。

'key相同打你系统':第一次打到mysql,空对象缓存后第二次就返回defaultNull缺省值,避免mysql被攻击,不用再到数据库中去走一圈了。

'key不同打你系统':由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)
方案2:Google布隆过滤器

Guava中布隆过滤器的实现算是比较权威的,所以实际项目中我们可以直接使用Guava布隆过滤器。

案例:白名单过滤器

1)白名单架构说明

image-20230604194501753

误判问题,但是概率小可以接收,不能从布隆过滤器删除

全部合法的key都需要放入Guava版布隆过滤器+redis里面,不然数据就是返回null

2)Coding实战

 改POM
 <!--guava Google 开源的 Guava 中自带的布隆过滤器-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>
'测试过滤器'
@Test
public void testGuavaWithBloomFilter()
 {
	// 创建布隆过滤器对象
     BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
	// 判断指定元素是否存在
     System.out.println(filter.mightContain(1));
     System.out.println(filter.mightContain(2));
	// 将元素添加进布隆过滤器
     filter.put(1);
     filter.put(2);
     System.out.println(filter.mightContain(1));
     System.out.println(filter.mightContain(2));
}



'正式代码'
'GuavaBloomFilterController'
@Api(tags = "google工具Guava处理布隆过滤器")
@RestController
@Slf4j
public class GuavaBloomFilterController
{
    @Resource
    private GuavaBloomFilterService guavaBloomFilterService;

    @ApiOperation("guava布隆过滤器插入100万样本数据并额外10W测试是否存在")
    @RequestMapping(value = "/guavafilter",method = RequestMethod.GET)
    public void guavaBloomFilter()
    {
        guavaBloomFilterService.guavaBloomFilter();
    }
}


'GuavaBloomFilterService'
@Service
@Slf4j
public class GuavaBloomFilterService{
    public static final int _1W = 10000;
    //布隆过滤器里预计要插入多少数据
    public static int size = 100 * _1W;
    //误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
    //fpp the desired false positive probability
    public static double fpp = 0.03;
    // 构建布隆过滤器
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,fpp);
    public void guavaBloomFilter(){
        //1 先往布隆过滤器里面插入100万的样本数据
        for (int i = 1; i <=size; i++) {
            bloomFilter.put(i);
        }
        //故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里
        List<Integer> list = new ArrayList<>(10 * _1W);
        for (int i = size+1; i <= size + (10 *_1W); i++) {
            if (bloomFilter.mightContain(i)) {
                log.info("被误判了:{}",i);
                list.add(i);
            }
        }
        log.info("误判的总数量::{}",list.size());
    }
}
现在总共有10万数据是不存在的,误判了3033次,
原始样本:100W
不存在数据:1000001W---1100000W   
    
误判率设置越小,需要的开销越大,槽和hash函数就会越多。
误判率默认为0.03,不设置也是0.03

image-20231110085319983

image-20230604195136378

缓存击穿
	'缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击'
	对于某一个缓存,在高并发情况下若其访问量特别巨大,当该缓存的有效时限到达时,可能会出现大量的访问都要重建该缓存,即这些访问请求发现缓存中没有该数据,则立即到DBMS 中进行查询,那么这就有可能会引发对 DBMS 的高并发查询,从而接导致 DBMS 的崩溃。这种情况称为缓存击穿,而该缓存数据称为热点数据。
	对于缓存击穿的解决方案,较典型的是使用“双重检测锁”机制。
    '可能会引起的原因:'
        时间到了自然清除但还被访问到
        delete掉的key,刚巧又被访问

在这里插入图片描述

常见的解决方案有两种:

方案一:互斥更新,采用双检加锁策略
方案二:逻辑过期方案
逻辑锁方案

我们可以采用tryLock 方法 + double check来解决这样的问题。

image-20230525173024990

image-20230604200001102

实现思路
'核心思路':相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如 果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得 到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询,如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就 能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿。

'具体实现':利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在 stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程
逻辑过期方案
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁,而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数 据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

在这里插入图片描述

实现思路
'思路分析':当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库, 而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。

在这里插入图片描述

淘宝案例

image-20231110091906192

'AB双缓存架构,差异失效时间 '
'控制层'
/**
* 偷个懒不加mybatis了,模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
* @return
*/
privateList<Product> getProductsFromMysql() {
	List<Product> list=new ArrayList<>();
	for (int i = 1; i <=20; i++) {
		Random rand = new Random();
		int id= rand.nextInt(10000);
		Product obj=new Product((long) id,"product"+i,i,"detail");
		list.add(obj);
	}
	return list;
}

//@PostConstruct -- 最基础的代码。会出现缓存穿透
public void initJHS(){
	log.info("启动定时器淘宝聚划算功能模拟.........."+ DateUtil.now());
	new Thread(() -> {
			//模拟定时器,定时把数据库的特价商品,刷新到redis中
			while (true){
			//模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
			List<Product> list=this.getProductsFromMysql();
			//采用redis list数据结构的lpush来实现存储
			this.redisTemplate.delete(JHS_KEY);
			//lpush命令
			this.redisTemplate.opsForList().leftPushAll(JHS_KEY,list);
			//间隔一分钟 执行一遍,模拟一天一更新
			try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
			log.info("runJhs定时刷新..............");
		}
	},"t1").start();
}

@PostConstruct
public void initJHSAB(){
	log.info("启动AB定时器计划任务淘宝聚划算功能模拟.........."+DateUtil.now());
	new Thread(() -> {
		//模拟定时器,定时把数据库的特价商品,刷新到redis中
		while (true){
		//模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
		List<Product> list=this.getProductsFromMysql();
		//先更新B缓存
		this.redisTemplate.delete(JHS_KEY_B);
		this.redisTemplate.opsForList().leftPushAll(JHS_KEY_B,list);
		this.redisTemplate.expire(JHS_KEY_B,20L,TimeUnit.DAYS);
		//再更新A缓存
		this.redisTemplate.delete(JHS_KEY_A);
		this.redisTemplate.opsForList().leftPushAll(JHS_KEY_A,list);
		this.redisTemplate.expire(JHS_KEY_A,15L,TimeUnit.DAYS);
		//间隔一分钟 执行一遍
		try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

		log.info("runJhs定时刷新双缓存AB两层..............");
		}
	},"t1").start();
}
    
    

'业务层' 
    /**
* 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
* @param page
* @param size
* @return
*/
@RequestMapping(value = "/pruduct/find",method = RequestMethod.GET)
@ApiOperation(" 按照分页和每页显示容量,点击查看")
public List<Product> find(int page, int size) {
	List<Product> list=null;
	long start = (page - 1) * size;
	long end = start + size - 1;
	try {
		//采用redis list数据结构的lrange命令实现分页查询
		list = this.redisTemplate.opsForList().range(JHS_KEY, start, end);
		if (CollectionUtils.isEmpty(list)) {
			//TODO 走DB查询
		}
		log.info("查询结果:{}", list);
	} catch (Exception ex) {
		//这里的异常,一般是redis瘫痪 ,或 redis网络timeout
		log.error("exception:", ex);
		//	TODO 走DB查询
	}
	return list;
}

@RequestMapping(value = "/pruduct/findab",method = RequestMethod.GET)
@ApiOperation("防止热点key突然失效,AB双缓存架构")
public List<Product> findAB(int page, int size) {
	List<Product> list=null;
	long start = (page - 1) * size;
	long end = start + size - 1;
	try {
		//采用redis list数据结构的lrange命令实现分页查询
		list = this.redisTemplate.opsForList().range(JHS_KEY_A, start, end);
		if (CollectionUtils.isEmpty(list)) {
			log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
			//用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
			this.redisTemplate.opsForList().range(JHS_KEY_B, start, end);
			//TODO 走DB查询
		}
		log.info("查询结果:{}", list);
	} catch (Exception ex) {
		//这里的异常,一般是redis瘫痪 ,或 redis网络timeout
			log.error("exception:", ex);
			//TODO 走DB查询
		}
	return list;
	}
}
进行对比
		'互斥锁方案':由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响 

​		'逻辑过期方案': 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦

image-20230525173558049

缓存雪崩
	'缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力'
	对于缓存中的数据,很多都是有过期时间的。若大量缓存的过期时间在同一很短的时间段内几乎同时到达,那么在高并发访问场景下就可能会引发对 DBMS 的高并发查询,而这将可能直接导致 DBMS 的崩溃。这种情况称为缓存雪崩。
	对于缓存雪崩没有很直接的解决方案,最好的解决方案就是预防,即提前规划好缓存的过期时间。要么就是让缓存永久有效,当 DB 中数据发生变化时清除相应的缓存。如果 DBMS采用的是分布式部署,则将热点数据均匀分布在不同数据库节点中,将可能到来的访问负载均衡开来。
    '发生:'
​	redis主机挂了,Redis全盘崩溃,偏硬件运维
​	redis中有大量key同时国企大面试失效,偏软件开发

image-20230525172522617

预防+解决
	redis中key设置为永不过期 or 过期时间错开
​	redis缓存集群实现高可用:
		主从 + 哨兵
		Redis Cluster
		开启Redis持久化机制aof/rdb,尽快回复缓存集群
​	多缓存结合预防雪崩 —— ehcache本地缓存 + redis缓存
​	服务降级 —— Hystrix或者阿里sentinel限流&降级
​	人民币玩家 —— 阿里云-云数据库Redis版
缓存预热

@PostConstruct初始化白名单数据

数据库缓存双写不一致

双检加锁机制

缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入 太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。
	'内存淘汰':redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制, 淘汰掉一些不重要的数据(可以自己设置策略方式)'超时剔除':当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
​	'主动更新':我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

image-20230525171206159

目的 — 要达到最终一致性

image-20230604171014028

以上三种情况都是针对'高并发读'场景中可能会出现的问题,而数据库缓存双写不一致问题,则是在'高并发写'场景下可能会出现的问题。
(1)更新缓存修改DB
不推荐、业务上一般把mysql作为底单数据库,保证最后解释
先更新缓存,再更新数据库】,AB两个线程发起调用

【正常逻辑】
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update mysql 80
====================================
【异常逻辑】多线程环境下,AB两个线程有快有慢有并行
A update redis  100
B update redis  80
B update mysql 80
A update mysql 100

----mysql100,redis80
(2)修改DB更新缓存
	对于具有缓存 warmup 功能的系统,DBMS 中常用数据的变更,都会引发缓存中相关数据的更新。在高并发写请求场景下,若多个请求要对 DBMS 中同一个数据进行修改,修改后还需要更新缓存中相关   数据,那么就有可能会出现缓存与数据库中数据不一致的情况。

image-20231025135344754

还有一种问题
1 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
2 先更新mysql修改为99成功,然后更新redis。
3 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。
4  上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据
(3)删缓存,修改DB
异常问题
1、A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时)B突然出现要来读取缓存数据。

image-20231107180815096

2 此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:
  2.1  B从mysql获得了旧值
       B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
  2.2  B会把获得的旧值写回redis 
       获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)。

image-20231107181019117

3、A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了,o(╥﹏╥)o两个并发操作,一个是更新操作,另一个是查询操作,A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

4 总结流程:
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql......A还么有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
(4)请求B将旧值写回redis缓存
(5)请求A将新值写入mysql数据库
延时双删策略
延迟双删方案是专门针对于“修改 DB 删除缓存”场景的解决方案。但该方案并不能彻底解决数据不一致的状况,其只可能降低发生数据不一致的概率。
延迟双删方案是指,在写操作完毕后会立即执行一次缓存的删除操作,然后再停上一段时间(一般为几秒)后再进行一次删除。而两次删除中间的间隔时长,要大于一次缓存写操作的时长。

在这里插入图片描述

image-20231107181612624

双删方案问题
'休眠时间该多久?'
线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。
这个时间怎么确定呢?

第一种方法:
在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

第二种方法:
新启动一个后台监控程序,比如后面要讲解的WatchDog监控程序,会加时
'这种同步淘汰策略,吞吐量降低该怎么办?'

在这里插入图片描述

(4)修改DB,删缓存
	这个比较推荐
	在很多系统中是没有缓存 warmup 功能的,为了保持缓存与数据库数据的一致性,一般都是在对数据库执行了写操作后,就会删除相应缓存。
	在高并发读写'请求'场景下,若这些请求对 DBMS 中同一个数据的操作既包含写也包含读,且修改后还要删除缓存中相关数据,那么就有可能会出现缓存与数据库中数据不一致的情况。

image-20230810162528582

'解决方案'
1 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
2 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
3 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。

image-20231107182519983

(5) 解决方案:队列
	以上两种场景中,只所以会出现数据库与缓存中数据不一致,主要是因为对请求的处理出现了并行。只要将请求写入到一个统一的队列,只有处理完一个请求后才可处理下一个请求,即使系统对用户请求的处理串行化,就可以完全解决数据不一致的问题。
	性能急剧下降,不好用。
(6) 解决方案:分布式锁
	使用队列的串行化虽然可以解决数据库与缓存中数据不一致,但系统失去了并发性,降低了性能。使用分布式锁可以在不影响并发性的前提下,协调各处理线程间的关系,使数据库与缓存中的数据达成一致性。
	只需要对数据库中的这个共享数据的访问通过分布式锁来协调对其的操作访问即可。
总结
在大多数业务场景下, 
阳哥个人建议是(仅代表我个人,不权威),优先使用'先更新数据库,再删除缓存的方案(先更库→后删存)'。理由如下:

1 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

多补充一句:如果'使用先更新数据库,再删除缓存的方案'
如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性,请大家参考。

image-20230604200127250

双写一致性案例
canal
canal,主要用途是基于mysql数据库增量日志解析,提供增量数据订阅和消费

'作用'
	数据库镜像
	数据库实时备份
	索引后见和实时维护(拆分异构索引、倒排索引等)
	业务cache刷新
	带业务逻辑的增量数据处理
mysql主从原理

在这里插入图片描述

MySQL的主从复制将经过如下步骤:

1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中;
2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变,如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;
3、同时 master 主服务器为每个 I/O Thread 启动一个dump  Thread,用于向其发送二进制事件日志;
4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中;
5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;
6、最后 I/O ThreadSQL Thread 将进入睡眠状态,等待下一次被唤醒;
canal工作原理
canal 模拟mysql salve的交互协议,伪装自己为mysql slave,向MySQLmaster发送dump协议
mysql master收到dump 请求,开始推送binary log给slave(即canal)
canal解析binary log对象(原始为byte流)
mysql-canal-redis双写一致性
mysql
1、查看mysql版本
select version();   mysql5.7.28

2、当前的主机二进制日志
show master status;

3、查看show varibales like 'log_bin'
variable  value
lon_bin   off

4、开启mysql的binlog写入功能   linux-my.cnf  widows  mu.ini
log-bin=mysql-bin #开启 binlog
binlog-format=ROW #选择 ROW 模式
server_id=1    #配置MySQL replaction需要定义,不要和canal的 slaveId重复

ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。
STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况;
MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或者删除情况下就会变为row模式;

5、重启mysql

6、再次查看show variables like 'log_bin'
variable  value
lon_bin   on

7、授权canal连接mysql账号
mysql默认的用户在mysql库的user表里   select * from mysql.user;
默认没有canal账户,下面新建+授权
DROP USER IF EXISTS 'canal'@'%';
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';       mysql8 不需要IDENTIFIED
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal';  
FLUSH PRIVILEGES;
SELECT * FROM mysql.user;
canal服务端
1、下载
https://github.com/alibaba/canal/releases/tag/canal-1.1.6
下载Linux版本: canal.deployer-1.1.6.tar.gz
注意发布时间+版本,2022.8.11后发布的才用

2、解压
解压后整体放入/mycanal路径下

3、配置
修改/mycanal/conf/example路径下instance.properties文件
	canal.instance.master.address=自己mysql主机的master的ip地址   windows下的也可以
	canal.instance.dbusername=canal  换成自己的账号和密码
	canal.instance.dbPassword=canal
	
4、启动
/opt/mycanal/bin路径下启动   ./startup.sh

5、查看
判断canal是否启动成功
	查看server日志   
    /mycanal/logs/canal目录下  cat canal.log 提示  the canal server is running now ...
	查看 样例example的日志
	/mycanal/logs/example目录下  cat example.log 提示 c.a.otter.canal.install.core.AbstractCanalInstance-start -successful...
canal客户端

Lua 脚本

变量和循环

Lua的数据类型

image-20230531161117575

另外,Lua提供了type()函数来判断一个变量的数据类型:

image-20230531161135147

声明变量

Lua声明变量的时候无需指定数据类型,而是用local来声明变量为局部变量:

-- 声明字符串,可以用单引号或双引号,
local str = 'hello'

-- 字符串拼接可以使用 ..
local str2 = 'hello' .. 'world'

-- 声明数字
local num = 21

-- 声明布尔类型
local flag = true

Lua中的table类型既可以作为数组,又可以作为Java中的map来使用。数组就是特殊的table,key是数组角标而已:

-- 声明数组 ,key为角标的 table
local arr = {'java', 'python', 'lua'}

-- 声明table,类似java的map
local map = {name='Jack', age=21}

Lua中的数组角标是从1开始,访问的时候与Java中类似:

-- 访问数组,lua数组的角标从1开始
print(arr[1])

Lua中的table可以用key来访问:

-- 访问table
print(map['name'])
print(map.name)

循环

对于table,我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。

遍历数组:

-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍历数组
for index,value in ipairs(arr) do
	print(index, value)
end

遍历普通table

-- 声明map,也就是table
local map = {name='Jack', age=21}
-- 遍历table
for key,value in pairs(map) do
	print(key, value)
end

条件控制、函数

Lua中的条件控制和函数声明与Java类似。

函数

定义函数的语法:

function 函数名( argument1, argument2..., argumentn)
	-- 函数体
	return 返回值
end

例如,定义一个函数,用来打印数组:

function printArr(arr)
	for index, value in ipairs(arr) do
		print(value)
	end
end
条件控制

类似Java的条件控制,例如if、else语法:

if(布尔表达式)
then
	--[ 布尔表达式为 true 时执行该语句块 --]
else
	--[ 布尔表达式为 false 时执行该语句块 --]
end

与java不同,布尔表达式中的逻辑运算是基于英文单词:
在这里插入图片描述

案例

需求:自定义一个函数,可以打印table,当参数为nil时,打印错误信息

function printArr(arr)
	if not arr then
		print('数组不能为空!')
	end
	for index, value in ipairs(arr) do
		print(value)
	end
end

eval命令的使用

eval和evalsha命令是从Redis2.6.0版本开始引入的,使用内置的Lua解释器,可以对Lua脚本进行求值。

eval命令的说明:

> help eval

  EVAL script numkeys key [key ...] arg [arg ...]
  summary: Execute a Lua script server side
  since: 2.6.0
  group: scripting
参数说明:
​		script:一段Lua脚本程序,这段Lua脚本不需要也不应该定义函数,它运行在Redis服务器中。
​		numkeys:键名参数的个数。指定后续参数有几个key,即:key [key …]中key的个数。
​		key[]: 键名参数,表示在脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问(KEYS[1]、KEYS[2],以此类推)。
​		arg[]:不是键名参数的附加参数,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1]、ARGV[2],诸如此类)

eg:

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 a b c d
1) "a"
2) "b"
3) "c"
4) "d"

返回结果是Redis multi bulk replies的Lua数组,这是一个Redis的返回类型,其他客户端库(如JAVA客户端)可能会将他们转换成数组类型。

Lua中执行redis命令

在Lua中,可以通过内置的函数**redis.call()redis.pcall()**来执行redis命令。

redis.call()和redis.pcall()两个函数的参数可以是任意的Redis命令:

当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因

redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误

> eval "return redis.call('set','foo','bar')" 0
OK

在这里插入图片描述

在这里插入图片描述

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static{//写成静态代码块,类加载就可以完成初始定义,就不用每次释放锁都去加载这个,性能提高咯
    UNLOCK_SCRIPT = new DefaultRedisScript<>();
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));//设置脚本位置
    UNLOCK_SCRIPT.setResultType(Long.class);
}
    public void unlock(){
        //调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
        );
    }

Redis单线程 VS 多线程

Redis为什么选择单线程

在这里插入图片描述

在这里插入图片描述

这个是6.0之后就算是多线程的了

在这里插入图片描述

Redis3.x单线程时代但性能依旧很快的主要原因

	'基于内存操作'Reids的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高
​	'数据结构简单'Redis的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分负责都都是O(1),因此性能比较高
​	'多路复用和非阻塞I/O':Redis使用I/O多路复用功能来监听多个socker连接客户端,这样就可以使用一个线程来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作
​	'避免上下文切换':因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生
    
Redis 是基于内存操作的,因此他的瓶颈可能是机器的内存或者网络带宽而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了,况且使用多线程比较麻烦。但是在 Redis 4.0 中开始支持多线程了,例如后台删除、备份等功能。

简单来说,Redis4.0之前一直采用单线程的主要原因有以下三个:

	1 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;
​	2 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO;
​	3 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。

内存的响应时长(RT)100ns
Redis每秒处理的读写请求数量应该可以达到1s/100ns = 10^9ns/100ns = 10^7 = 1000w
实际情况是,Redis每秒可以处理8w-11w的读写请求
Redis 的单线程模型采用了'多路复用技术'。

对于多路复用器的多路选择算法常见的有三种:'select' 模型、'poll' 模型、'epoll' 模型。
	'poll' 模型的选择算法:采用的是轮询算法。该模型对客户端的就绪处理是有延迟的。
	'epoll' 模型的选择算法:采用的是回调方式。根据就绪事件发生后的处理方式的不同,又可分为 LT 模型与 ET 模型。
    
select和poll基本的是一样的,只不过是 select 底层采用的是数组 poll 底层采用多个是链表 数组那个效率太低了,扩容太消耗性能了

混合模型

在这里插入图片描述

从 Redis 4.0 版本开始,Redis 中就开始加入了多线程元素。处理客户端请求的仍是单线程模型,但对于一些比较耗时但又不影响对客户端的响应的操作,就由后台其它线程来处理。例如,持久化、对 AOF 的 rewrite、对失效连接的清理等。

为什么会引入多线程

单线程遇到的问题

正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如时包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿。

'这就是redis3.x单线程时代最经典的故障,大key删除的头疼问题'
    
由于redis是单线程的,del bigkey ...
等待很久这个线程才会释放,类似于加了以恶搞synchronized锁,在高并发的情况下,程序就会非常堵

如何解决

Redis需要删除一个很大的数据时,因为是单线程原子命令操作,这就会导致 Redis 服务卡顿,于是在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的。

把删除工作交给了后台的小弟(子线程)异步来删除数据了。

unlink key 删除大key

在这里插入图片描述

在Redis 4.0就引入了多个线程来实现数据的异步惰性删除等功能,但是其处理读写请求的仍然只有一个线程,所有仍然算是狭义上的单线程

Redis6/7多线程特征和IO多路复用

在这里插入图片描述

	多线程 IO 模型中的'多线程'仅用于'接受、解析'客户端的请求,然后将解析出的请求写入到任务队列。'而对具体任务(命令)的处理,仍是由主线程处理'。这样做使得用户无需考虑线程安全问题,无需考虑事务控制,无需考虑像 LPUSH/LPOP 等命令的执行顺序问题。

对于Reids主要的性能瓶颈是内存或者网络带宽而并非CPU

image-20230604112402012

主线程和IO线程是怎样写作完成请求处理的
image-20230604112534742

image-20230604112600872

image-20230604112607977

Unix网络变成种五种IO模型

Blocking IO - 阻塞IO
NoneBlocking IO - 非阻塞IO
signal driven IO - 信号驱动IO
asynchronous IO - 异步IO
IO multiplexing - IO多路复用
Linux世界一切皆文件

文件描述符、简称FD,句柄

'文件描述符(File descriptor)'是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,文件描述符这一概念往往只适用于UNIXLinux这样的操作系统。
IO多路复用
一种同步的IO模型,实现'一个线程'监视'多个文件句柄','一旦某个文件句柄就绪'就能够通知到对应应用程序进行相应的读写操作,'没有文件句柄就绪时'就会阻塞应用程序,从而释放CPU资源
	'I/O':网络I/O,尤其在操作系统层面指数据在内核态和用户态之间的读写操作
​	'多路':多个客户端连接(连接就是套接字描述符,即 socket 或者 channel )
​	'复用':复用一个或几个线程
​	'IO多路复用':也就是说一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接,无需创建或者维护过多的进程/线程

	'一句话':
​	一个服务端进程可以同时处理多个套接字描述符
​	实现IO多路复用的模型有3种:可以分select—>poll—>epoll(重点)三个阶段来描述
场景体验——epoll

image-20230604114335394

image-20230604114420106

小总结

只是用一个服务端进程可以同时处理多个套接字描述符链接

image-20230604114507113

面试题:Redis为什么这么快
IO多路复用+epoll函数使用,才是redis为什么这么快的直接原因,而不是仅仅单线程命令+redis安装在内存中。

简单说明

Redis工作线程是单线程的,但是,整个Redis来说,是多线程的

主线程和IO线程是怎样协作完成求情处理的

I/O 的读和写本身是堵塞的,比如当 socket 中有数据时,Redis 会通过调用先将数据从内核态空间拷贝到用户态空间,再交给 Redis 调用,而这个拷贝的过程就是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。

在这里插入图片描述

从Redis6开始,就新增了多线程的功能来提高 I/O 的读写性能,他的主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使多个 socket 的读写可以并行化了,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),将最耗时的Socket的读取、请求解析、写入单独外包出去,剩下的命令执行仍然由主线程串行执行并和内存的数据交互。

在这里插入图片描述

结合上图可知,网络IO操作就变成多线程化了,其他核心部分仍然是线程安全的,是个不错的

结论

Redis6→7将网络数据读写、请求协议解析通过多个IO线程的来处理 ,

对于真正的命令执行来说,仍然使用主线程操作,一举两得,便宜占尽!!! o( ̄▽ ̄) d

在这里插入图片描述

Redis7默认是否开启了多线程

如果在实际应用中,发现Redis实例的'CPU开销不大但吞吐量却没有提升',可以考虑使用Redis7的多线程机制,加速网络处理,进而提升实例的吞吐量。

Redis7将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理8W到10W的QPS,

这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了

在Redis6.0及7后,多线程机制默认是关闭的,如果需要使用多线程功能,需要在redis.conf中完成两个设置

在这里插入图片描述

1.设置io-thread-do-reads配置项为yes,表示启动多线程。

2.设置线程个数。关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 23,如果为 8CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。

小总结

Redis自身出道就是优秀,基于内存操作、数据结构简单、多路复用和非阻塞 I/O、避免了不必要的线程上下文切换等特性,在单线程的环境下依然很快;

但对于大数据的 key 删除还是卡顿厉害,因此在 Redis 4.0 引入了多线程unlink key/flushall async 等命令,主要用于 Redis 数据的异步删除;

而在 Redis6/7中引入了 I/O 多线程的读写,这样就可以更加高效的处理更多的任务了,'Redis 只是将 I/O 读写变成了多线程','而命令的执行依旧是由主线程串行执行的',因此在多线程下操作 Redis 不会出现线程安全的问题。

'Redis 无论是当初的单线程设计,还是如今与当初设计相背的多线程,目的只有一个:让 Redis 变得越来越快。'
    
'优缺点总结'1'单线程模型'
	'优点':可维护性高,性能高。不存在并发读写情况,所以也就不存在执行顺序的不确定性,不存在线程切换开销,不存在死锁问题,不存在为了数据安全而进行的加锁/解锁开销。
	'缺点':性能会受到影响,且由于单线程只能使用一个处理器,所以会形成处理器浪费。
	
(2'多线程模型'
	 '优点':其结合了多线程与单线程的优点,避开了它们的所有不足
	 '缺点':该模型没有显示不足。如果非要找其不足的话就是,其并非是一个真正意义上的 “多线程”,因为真正处理“任务”的线程仍是单线程。所以,其对性能也是有些影响的。

epoll和IO多路复用

问题

'要解决的问题'
并发多客户端连接,在多路复用之前最简单和典型的方案:同步阻塞网络IO模型
这种模式的特点就是用一个进程来处理一个网络连接(一个用户请求),比如一段典型的示例代码如下。
直接调用 recv 函数从一个 socket 上读取数据。

int main()
{
 ...
 recv(sock, ...) //从用户角度来看非常简单,一个recv一用,要接收的数据就到我们手里了。
}          

我们来总结一下这种方式:
优点就是这种方式非常容易让人理解,写起代码来非常的自然,符合人的直线型思维。
缺点就是性能差,每个用户请求到来都得占用一个进程来处理,来一个请求就要分配一个进程跟进处理,
类似一个学生配一个老师,一位患者配一个医生,可能吗?进程是一个很笨重的东西。一台服务器上创建不了多少个进程。

'结论'
进程在 Linux 上是一个开销不小的家伙,先不说创建,光是上下文切换一次就得几个微秒。所以为了高效地对海量用户提供服务,必须要让一个进程能同时处理很多个 tcp 连接才行。现在假设一个进程保持了 10000 条连接,那么'如何发现哪条连接上有数据可读了、哪条连接可写了' ?
 
我们当然可以采用循环遍历的方式来发现 IO 事件,但这种方式太低级了。
我们希望有一种更高效的机制,在很多连接中的某条上有 IO 事件发生的时候直接快速把它找出来。
'其实这个事情 Linux 操作系统已经替我们都做好了,它就是我们所熟知的 IO 多路复用机制'。
'这里的复用指的就是对进程的复用'

是什么

I/O:网络I/O
多路:多个客户端连接(连接就是套接字描述符,即 socket 或者 channel ),指的是多条TCP连接
复用:用一个进程来处理多条的连接,使用单进程就能够实现同时处理多个客户端的连接

一句话:
	实现了用一个进程来处理大量的用户连接
	IO多路复用类似一个规范和接口,落地实现,可分为select->poll->epoll三个阶段来描述

为什么快

RedisIO多路复用
Redis利用epoll来实现IO多路复用,'将连接信息和事件放到队列中',一次放到文件事件'分派器',事件分派器将事件分发给事件'处理器'

在这里插入图片描述

Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是'由于读写操作等待用户输入或输出都是阻塞的',所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现
 
所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合'。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理'。
 
Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) 
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:
	多个套接字、
	IO多路复用程序、
	文件事件分派器、
	事件处理器。
'因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型'

redis的设计与实践

在这里插入图片描述

在这里插入图片描述

read

上午开会,错过了公司食堂的饭点, 中午就和公司的首席架构师一起去楼下的米线店去吃米线。我们到了一看,果然很多人在排队。

架构师马上发话了:嚯,'请求排队'啊!你看这位收银点菜的,'像不像nginx的反向代理'?只收请求,不处理,把请求都发给后厨去处理。
我们交了钱,拿着号离开了点餐收银台,找了个座位坐下等餐。
架构师:你看,这就是'异步处理,我们下了单就可以离开等待',米线做好了会通过小喇叭'“回调”'我们去取餐;
如果'同步'处理,我们就得在收银台站着等餐,后面的请求无法处理,客户等不及肯定会离开了。

接下里架构师盯着手中的纸质号牌。

架构师:你看,这个纸质号牌在后厨“服务器”那里也有,这不就是表示'会话的ID'吗?
有了它就可以把大家给区分开,就不会把我的排骨米线送给别人了。过了一会, 排队的人越来越多,已经有人表示不满了,可是收银员已经满头大汗,忙到极致了。

架构师:你看他这个系统缺乏'弹性扩容', 现在这么多人,应该增加收银台,可以没有其他收银设备,老板再着急也没用。
老板看到在收银这里帮不了忙,后厨的订单也累积得越来越多, 赶紧跑到后厨亲自去做米线去了。

架构师又发话了:幸亏这个系统的后台有'并行处理能力',可以随意地增加资源来处理请求(做米线)。
我说:他就这点儿资源了,除了老板没人再会做米线了。
不知不觉,我们等了20分钟, 但是米线还没上来。
架构师:你看,系统的处理能力达到极限,'超时了'吧。
这时候收银台前排队的人已经不多了,但是还有很多人在等米线。

老板跑过来让这个打扫卫生的去收银,让收银小妹也到后厨帮忙。打扫卫生的做收银也磕磕绊绊的,没有原来的小妹灵活。

架构师:这就叫'服务降级',为了保证米线的服务,把别的服务都给关闭了。
又过了20分钟,后厨的厨师叫道:237号, 您点的排骨米线没有排骨了,能换成番茄的吗?
架构师低声对我说:瞧瞧, 人太多, '系统异常'了。然后他站了起来:不行,系统得进行'补偿操作:退费'。

说完,他拉着我,饿着肚子,头也不回地走了。
'同步'
调用者要一直等待调用结果的通知后才能进行后续的执行,现在就要,我可以等,等出结果为止。

'异步'
指被调用方先返回应答让调用者先回去,然后再计算调用结果,计算完最终结果后再通知并返回给调用方。
异步调用想要获得结果一般通过回调

'同步与异步的理解'
同步、异步的讨论对象是被调用者(服务提供者),重点在于获得调用结果的消息通知方式上。

'阻塞'
调用方一直等待而且别的事情什么都不做,当前进/线程会被挂起,啥都不干

'非阻塞'
调用在发出去后,调用方选去忙别的事情,不会阻塞当前进/线程,而会立即返回。

'阻塞与非阻塞的理解'
阻塞、非阻塞的讨论对象是调用者(服务请求者),重点在于等消息时候的行为,调用者是否能干其它事

'总结,4种组合方式'
	'同步阻塞':服务员说快到你了,先别离开我后台看一眼马上通知你。客户在海底捞火锅前台干等着,啥都不干。
	'同步非阻塞':服务员说快到你了,先别离开。客户在海底捞火锅前台边刷抖音边等着叫号。
	'异步阻塞':服务员说还要再等等,你先去逛逛,一会儿通知你。客户怕过号在海底捞火锅前台拿着排号小票啥都不干,一直等着电员通知。
	'异步非则色':服务员说还要再等等,你先去逛逛,一会儿通知你。拿着排号小票+刷着抖音,等着店员通知

Java代码

背景
一个redisService + 2个Client
BIO
当用户进程调用了recvfrom这个系统调用,kernel(内核)就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,'BIO的特点就是在IO执行的两个阶段都被block了'。 

在这里插入图片描述

accept监听
'RedisService'
public class RedisServer                    
{
    public static void main(String[] args) throws IOException
    {
        byte[] bytes = new byte[1024];                                  
        ServerSocket serverSocket = new ServerSocket(6379);
        while(true)
        {
            System.out.println("-----111 等待连接");
            Socket socket = serverSocket.accept();
            System.out.println("-----222 成功连接");
        }
    }
}

'RedisClient01'
public class RedisClient01
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("------RedisClient01 start");
        Socket socket = new Socket("127.0.0.1", 6379);
    }
}

'RedisClietn02'
public class RedisClient02
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("------RedisClient02 start");
        Socket socket = new Socket("127.0.0.1", 6379);
    }
}
read读取
先启动RedisServiceBIO,再启动RedisClient01验证后再启动2号客户端
'RedisServerBIO'
public class RedisServerBIO
{
    public static void main(String[] args) throws IOException
    {

        ServerSocket serverSocket = new ServerSocket(6379);

        while(true)
        {
            System.out.println("-----111 等待连接");
            Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
            System.out.println("-----222 成功连接");

            InputStream inputStream = socket.getInputStream();
            int length = -1;
            byte[] bytes = new byte[1024];
            System.out.println("-----333 等待读取");
            while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
            {
                System.out.println("-----444 成功读取"+new String(bytes,0,length));
                System.out.println("====================");
                System.out.println();
            }
            inputStream.close();
            socket.close();
        }
    }
}

'RedisClient01'
public class RedisClient01
{
    public static void main(String[] args) throws IOException
    {
        Socket socket = new Socket("127.0.0.1",6379);
        OutputStream outputStream = socket.getOutputStream();

        //socket.getOutputStream().write("RedisClient01".getBytes());

        while(true)
        {
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if (string.equalsIgnoreCase("quit")) {
                break;
            }
            socket.getOutputStream().write(string.getBytes());
            System.out.println("------input quit keyword to finish......");
        }
        outputStream.close();
        socket.close();
    }
}

'RedisClient02'
public class RedisClient02
{
    public static void main(String[] args) throws IOException
    {
        Socket socket = new Socket("127.0.0.1",6379);
        OutputStream outputStream = socket.getOutputStream();

        //socket.getOutputStream().write("RedisClient01".getBytes());

        while(true)
        {
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if (string.equalsIgnoreCase("quit")) {
                break;
            }
            socket.getOutputStream().write(string.getBytes());
            System.out.println("------input quit keyword to finish......");
        }
        outputStream.close();
        socket.close();
    }
}

在这里插入图片描述

'多线程模式'

利用多线程
只要连接了一个socket,操作系统分配一个线程来处理,这样read()方法堵塞在每个具体线程上而不堵塞主线程,
就能操作多个socket了,哪个线程中的socket有数据,就读哪个socket,各取所需,灵活统一。

程序服务端只负责监听是否有客户端连接,使用 accept() 阻塞
客户端1连接服务端,就开辟一个线程(thread1)来执行 read() 方法,程序服务端继续监听
客户端2连接服务端,也开辟一个线程(thread2)来执行 read() 方法,程序服务端继续监听
客户端3连接服务端,也开辟一个线程(thread3)来执行 read() 方法,程序服务端继续监听

任何一个线程上的socket有数据发送过来,read()就能立马读到,cpu就能进行处理。

'RedisServerBIOMultiThread'
public class RedisServerBIOMultiThread
{
    public static void main(String[] args) throws IOException
    {
        ServerSocket serverSocket = new ServerSocket(6379);

        while(true)
        {
            //System.out.println("-----111 等待连接");
            Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
            //System.out.println("-----222 成功连接");

            new Thread(() -> {
                try {
                    InputStream inputStream = socket.getInputStream();
                    int length = -1;
                    byte[] bytes = new byte[1024];
                    System.out.println("-----333 等待读取");
                    while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
                    {
                        System.out.println("-----444 成功读取"+new String(bytes,0,length));
                        System.out.println("====================");
                        System.out.println();
                    }
                    inputStream.close();
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            },Thread.currentThread().getName()).start();

            System.out.println(Thread.currentThread().getName());

        }
    }
}

'RedisClient01'
public class RedisClient01
{
    public static void main(String[] args) throws IOException
    {
        Socket socket = new Socket("127.0.0.1",6379);
        OutputStream outputStream = socket.getOutputStream();

        //socket.getOutputStream().write("RedisClient01".getBytes());

        while(true)
        {
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if (string.equalsIgnoreCase("quit")) {
                break;
            }
            socket.getOutputStream().write(string.getBytes());
            System.out.println("------input quit keyword to finish......");
        }
        outputStream.close();
        socket.close();
    }
}

'RedisClient02'
public class RedisClient02
{
    public static void main(String[] args) throws IOException
    {
        Socket socket = new Socket("127.0.0.1",6379);
        OutputStream outputStream = socket.getOutputStream();

        //socket.getOutputStream().write("RedisClient01".getBytes());

        while(true)
        {
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if (string.equalsIgnoreCase("quit")) {
                break;
            }
            socket.getOutputStream().write(string.getBytes());
            System.out.println("------input quit keyword to finish......");
        }
        outputStream.close();
        socket.close();
    }
}

'存在的问题'
多线程模型
每来一个客户端,就要开辟一个线程,如果来1万个客户端,那就要开辟1万个线程。
在操作系统中用户态不能直接开辟线程,需要调用内核来创建的一个线程,
这其中还涉及到用户状态的切换(上下文的切换),十分耗资源。
知道问题所在了,请问如何解决??

'解决'
第一个方法:使用线程池
这个在客户端连接少的情况下可以使用,但是用户量大的情况下,你不知道线程池要多大,太大了内存可能不够,也不可行。
   
第二个方法:NIO(非阻塞式IO)方式
因为read()方法堵塞了,所有要开辟多个线程,如果什么方法能使read()方法不堵塞,这样就不用开辟多个线程了,这就用到了另一个IO模型,NIO(非阻塞式IO'总结'
tomcat7之前就是用BIO多线程来解决多连接
目前两个问题
'两个通点'
accept,read
在阻塞式I/O模型中,应用程序在调用recvfrom开始到它返回有数据报准备好这段时间是阻塞的,recvfrom返回成功后,应用进程才能开始处理数据报


'阻塞式IO小总结'

在这里插入图片描述

'思考'
每个线程分配一个连接,必然会产生多个,既然是多个socket链接必然需要放入进容器,纳入统一管理
NIO
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。'所以,NIO特点是用户进程需要不断的主动询问内核数据准备好了吗?一句话,用轮询替代阻塞!'

在这里插入图片描述

NIO模式中,一切都是非阻塞的:

accept()方法'是非阻塞的',如果没有客户端连接,就返回无连接标识
read()方法'是非阻塞的',如果read()方法读取不到数据就返回空闲中标识,如果读取到数据时只阻塞read()方法读数据的时间

在NIO模式中,只有一个线程:
当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,
看这个socket的read()方法能否读到数据,这样'一个线程就能处理多个客户端的连接和读取了'
code案例
'RedisServiceNIO'
public class RedisServerNIO
{
    static ArrayList<SocketChannel> socketList = new ArrayList<>();
    static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    public static void main(String[] args) throws IOException
    {
        System.out.println("---------RedisServerNIO 启动等待中......");
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("127.0.0.1",6379));
        serverSocket.configureBlocking(false);//设置为非阻塞模式

        while (true)
        {
            for (SocketChannel element : socketList)
            {
                int read = element.read(byteBuffer);
                if(read > 0)
                {
                    System.out.println("-----读取数据: "+read);
                    byteBuffer.flip();
                    byte[] bytes = new byte[read];
                    byteBuffer.get(bytes);
                    System.out.println(new String(bytes));
                    byteBuffer.clear();
                }
            }

            SocketChannel socketChannel = serverSocket.accept();
            if(socketChannel != null)
            {
                System.out.println("-----成功连接: ");
                socketChannel.configureBlocking(false);//设置为非阻塞模式
                socketList.add(socketChannel);
                System.out.println("-----socketList size: "+socketList.size());
            }
        }
    }
}

'RedisClient01'
public class RedisClient01
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("------RedisClient01 start");
        Socket socket = new Socket("127.0.0.1",6379);
        OutputStream outputStream = socket.getOutputStream();
        while(true)
        {
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if (string.equalsIgnoreCase("quit")) {
                break;
            }
            socket.getOutputStream().write(string.getBytes());
            System.out.println("------input quit keyword to finish......");
        }
        outputStream.close();
        socket.close();
    }
}

'RedisClient02'
public class RedisClient02
{
    public static void main(String[] args) throws IOException
    {
        System.out.println("------RedisClient02 start");


        Socket socket = new Socket("127.0.0.1",6379);
        OutputStream outputStream = socket.getOutputStream();

        while(true)
        {
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if (string.equalsIgnoreCase("quit")) {
                break;
            }
            socket.getOutputStream().write(string.getBytes());
            System.out.println("------input quit keyword to finish......");
        }
        outputStream.close();
        socket.close();
    }
}
问题和优缺点
NIO成功的解决了BIO需要开启多线程的问题,NIO中一个线程就能解决多个socket,但是还存在2个问题。

'问题一':
这个模型在客户端少的时候十分好用,但是客户端如果很多,比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket,如果一万个socket中只有10个socket有数据,也会遍历一万个socket,就会'做很多无用功,每次遍历遇到' read 返回 -1 时仍然是一次浪费资源的系统调用。

'问题二':
'而且这个遍历过程是在用户态进行的',用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大因为这些问题的存在。

'优点':不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。
'缺点':轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 I/O 模型。
'结论':让Linux内核搞定上述需求,我们将一批文件描述符通过一次系统调用传给内核由内核层去遍历,才能真正解决这个问题。'IO多路复用应运而生,也即将上述工作直接放进Linux内核,不再两态转换而是直接从内核获得结果,因为内核是非阻塞的'。
    
'问题升级:如何用单线程处理大量的链接?'

在这里插入图片描述

IO多路复用
是什么

I/O多路复用在英文中其实叫 I/O multiplexing

在这里插入图片描述

多个Socket复用一根网线这个功能是在内核+驱动层实现的

I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流. 目的是尽量多的提高服务器的吞吐能力。

在这里插入图片描述

大家都用过nginx,nginx使用epoll接收请求,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。redis类似同理

**文件描述符(File descriptor)**是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

IO multiplexing就是我们说的select,poll,epoll,有些技术书籍也称这种IO方式为event driven IO事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象并同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select,poll,epoll等函数就可以返回

在这里插入图片描述

说人话
模拟一个tcp服务器处理30个客户socket,一个监考老师监考多个学生,谁举手就应答谁。

假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:

'第一种选择':按顺序逐个验收,先验收A,然后是B,之后是CD。。。这中间如果有一个学生卡住,全班都会被耽误,你用循环挨个处理socket,根本不具有并发能力。 

'第二种选择':你创建30个分身线程,每个分身线程检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。

'第三种选择',你站在讲台上等,谁解答完谁举手。这时CD举手,表示他们解答问题完毕,你下去依次检查CD的答案,然后继续回到讲台上等。此时EA又举手,然后去处理EA。。。这种就是IO复用模型。Linux下的select、poll和epoll就是干这个的。

将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
能干嘛

Redis单线程如何处理那么多并发客户端连接,为什么单线程,为什么快

Redis的IO多路复用

Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到事件分派器,事件分派器将事件分发给事件处理器

在这里插入图片描述

Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) 

所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

'所谓 I/O 多路复用机制,就是说通过一种考试监考机制,一个老师可以监视多个考生,一旦某个考生举手想要交卷了,能够通知监考老师进行相应的收卷子或批改检查操作。所以这种机制需要调用班主任(select/poll/epoll)来配合。多个考生被同一个班主任监考,收完一个考试的卷子再处理其它人,无需等待所有考生,谁先举手就先响应谁,当又有考生举手要交卷,监考老师看到后从讲台走到考生位置,开始进行收卷处理'。

Reactor设计模式

基于 I/O 复用模型:'多个连接共用一个阻塞对象',应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。'即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术'。

在这里插入图片描述

Reactor 模式中有 2 个关键组成:
	1ReactorReactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
	2Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际办理人。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
	
每一个网络连接其实都对应一个文件描述符

Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)
 
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:
	多个套接字、
	IO多路复用程序、
	文件事件分派器、
	事件处理器。'因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型'

在这里插入图片描述

select,poll,epoll
5种I/O模型总结
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,
'变成了一次系统调用 + 内核层遍历这些文件描述符'。 
所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理;

在这里插入图片描述

为什么3个都保有

在这里插入图片描述

本地锁

只要是同一把锁,就能锁住这需要这个锁的所有线程
synchronizedthis):SpringBoot 所有的组件在容器中都是单例的
本地锁比如synchronized, JUC包下的(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁
synchronized (this){
	//执行业务即可
}

使用锁的时候必须将确认缓存和最后的结果放入缓存都在锁内执行(无论是本地所还是分布式锁)

在这里插入图片描述

悲观锁、乐观锁

在这里插入图片描述

'悲观锁':
悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以 再细分为公平锁,非公平锁,可重入锁,等等

'乐观锁':
乐观锁:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版 本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas 


乐观锁的典型代表:就是cas,利用cas进行无锁化机制加锁,var5 是操作前读取的内存值,while中的 var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值。
其中do while 是为了在操作失败时,再次进行自旋操作,即把之前的逻辑再操作一次。

int var5;
do {
	var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

课程中的使用方式是没有像cas一样带自旋的操作,也没有对version的版本号+1 ,他的操作逻辑是在操 作时,对版本号进行+1 操作,然后要求version 如果是1 的情况下,才能操作,那么第一个线程在操作 后,数据库中的version变成了2,但是他自己满足version=1 ,所以没有问题,此时线程2执行,线程2 最后也需要加上条件version =1 ,但是现在由于线程1已经操作过了,所以线程2,操作时就不满足 version=1 的条件了,所以线程2无法执行成功

在这里插入图片描述

boolean success = seckillVoucherService.update()
		.setSql("stock= stock -1") //set stock = stock -1
		.eq("voucher_id",
voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

是这样我们分析过,成功的概率太低,所以我们的乐观锁需要变 一下,改成stock大于0 即可

boolean success = seckillVoucherService.update()
		.setSql("stock= stock -1")
		.eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0

拓展

针对cas中的自旋压力过大,我们可以使用Longaddr这个类去解决

Java8 提供的一个对AtomicLong改进后的一个类,LongAdder

大量线程并发更新一个原子性的时候,天然的问题就是自旋,会导致并发性问题,当然这也比我们直接 使用syn来的好

所以利用这么一个类,LongAdder来进行优化

如果获取某个值,则会对cell和base的值进行递增,最后返回一个完整的值

在这里插入图片描述

分布式下是不安全的

在这里插入图片描述

分布式锁

分布式锁是控制分布式系统间同步访问共享资源的一种方式,其可以保证共享资源在并 发场景下的数据一致性。

	当有多个线程要访问某一个共享资源(DBMS 中的数据或 Redis 中的数据,或共享文件等)时,为了达到协调多个线程的同步访问,此时就需要使用分布式锁了。
	为了达到同步访问的目的,规定,让这些线程在访问共享资源之前先要获取到一个令牌token,只有具有令牌的线程才可以访问共享资源。这个令牌就是通过各种技术实现的分布式锁。而这个分布锁是一种“互斥资源”,即只有一个。只要有线程抢到了锁,那么其它线程只能等待,直到锁被释放或等待超时。
	'一个靠谱的分布式锁需要具备的条件和刚需'
		'排他性'OnlyOne,任何时刻只能且仅有一个线程持有
		'高可用':若reids集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失效的情况,高并发请求下,性能依旧ok好用。
		'防死锁':杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
		'不乱抢':防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放
		'重入性':同一个节点的同一个线程如果获得锁之后,他也可能再次获取这个锁。

常见的分布式锁

	'Mysql':mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使 用mysql作为分布式锁比较少见
​	'Redis':redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者 zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入 成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
​	'Zookeeper':zookeeper也是企业级开发中较好的一个实现分布式锁的方案

在这里插入图片描述

Redis锁

核心思路

在这里插入图片描述

我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中 就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退 出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

在这里插入图片描述

实现分布式锁
加锁逻辑

在这里插入图片描述

在这里插入图片描述

释放锁逻辑
public void unlock() {
	//通过del删除锁
	stringRedisTemplate.delete(KEY_PREFIX + name);
}
Redis锁误删

逻辑说明:

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:

解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删锁
逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。

在这里插入图片描述

核心逻辑:

在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
可以使用lua脚本来解决原子性问题

在这里插入图片描述

1、加锁

在这里插入图片描述

2、释放锁

在这里插入图片描述

lua实操

在这里插入图片描述

Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30 ,TimeUnit.SECONDS); 
if (lock) {
     //加锁成功...执行业务
     Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
     //获取值对比+对比成功删除=原子操作 (lua脚本解锁), 你下面获取 lockValue 的时候,刚好到自动删除时间,但是你也获取到这个值了,你就会把别人的锁删掉
      String lockValue = redisTemplate.opsForValue().get("lock");
      if (lockValue.equals(uuid)) {
            //删除自己的锁
            redisTemplate.delete("lock");
       }
 }

分布式锁会出现的细节问题:
    1、如果不设置超时时间,setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了'死锁'
    230s内可以业务还未执行完毕,就到了锁的超时释放时间,就会出现另一个进程开始执行业务后,早来的那个进行刚好执行完后释放锁,就会出现删别人锁的行为,所有value值用u  uid来对标一下是否是删除的自己的锁。
    3、因为获取值和对比成功删除是两个步骤,如果在获取值和对比之前刚好时间结束,锁被自动删除,另一个进程来执行任务,这个时候未进行对比得进行对比成功,会将新的进程得锁给删掉
	
继续优化: 使用lua脚本
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300 ,TimeUnit.SECONDS);
 Map<String, List<Catelog2Vo>> dataFromDb = null;
    try {
        //加锁成功...执行业务
        dataFromDb = getDataFromDb();
    } finally {
          String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
           //删除锁
          redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

 }

Redisson

基于setnx实现的分布式锁存在下面的问题:

	重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止 死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内, 调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死 锁,我们的synchronized和Lock锁都是可重入的。

​	不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该 能再次尝试获得锁。

​	超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然 我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患

​	主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从 机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。

在这里插入图片描述

快速入门

引入依赖

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.13.6</version>
</dependency>

配置Redisson客户端:

@Configuration
public class RedissonConfig {

    @Bean
	public RedissonClient redissonClient(){
		// 配置
		Config config = new Config();
		config.useSingleServer().setAddress("redis://192.168.150.101:6379")
            .setPassword("123321")
            .setDatabase(0);  //0号库
		// 创建RedissonClient对象
		return Redisson.create(config);
	}
}

可以直接使用

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
	//获取锁(可重入),指定锁的名称
	RLock lock = redissonClient.getLock("anyLock");
	//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
	boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
	//判断获取锁成功
	if(isLock){
		try{
			System.out.println("执行业务");
		}finally{
			//释放锁
			lock.unlock();
		}
	}
}
Redission可重入锁原理
在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,如果是对于synchronized而言,他在c语言代码中会有一个count,原理和state类似,也是重入一次就加一,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。 

在分布式锁中,他采用hash结构用来存储锁,其中大key表示表示这把锁是否存在,用小key表示当前这 把锁被哪个线程持有

"if (redis.call('exists', KEYS[1]) == 0) then " +
				"redis.call('hset', KEYS[1], ARGV[2], 1); " +
				"redis.call('pexpire', KEYS[1], ARGV[1]); " +
				"return nil; " +
			"end; " +
			"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
				"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
				"redis.call('pexpire', KEYS[1], ARGV[1]); " +
				"return nil; " +
			"end; " +
			"return redis.call('pttl', KEYS[1]);

KEYS[1] : 锁名称

ARGV[1]: 锁失效时间

ARGV[2]: id + “:” + threadId; 锁的小key

exists: 判断数据是否存在 name:是lock是否存在,如果==0,就表示当前这把锁不存在

redis.call(‘hset’, KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ,写成一个hash结构

Lock{
 	id + ":" + threadId : 1 
}

如果当前这把锁存在,则第一个条件不满足,再判断 redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1

此时需要通过大key+小key判断当前这把锁是否是属于自己的,如果是自己的,则进行 redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)

将当前这个锁的value进行+1 ,redis.call(‘pexpire’, KEYS[1], ARGV[1]); 然后再对其设置过期时间,如果 以上两个条件都不满足,则表示当前这把锁抢锁失败,最后返回pttl,即为当前这把锁的失效时间

他会去判断当前这个方法的返回值是否为null,如果是null, 则对应则前两个if对应的条件,退出抢锁逻辑,如果返回的不是null,即走了第三个分支,在源码处会进 行while(true)的自旋抢锁。

在这里插入图片描述

redission锁的MutiLock原理
	为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例 此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入 到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的 master中实际上并没有锁信息,此时锁信息就已经丢掉了。
	redis集群是AP模式就,会出现这个问题,会出现短暂的数据不一致,他收到请求会先进行响应,之后才会进行集群内同步。
	zookeeper集群是CP模式,就不会出现这个问题,他收到请求会先进行集群内同步,同步成功完成后才会给客户端响应

在这里插入图片描述

为了解决这个问题,redission提出来了MutiLock锁,使用这把锁咱们就不使用主从了,每个节点的地位 都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才 是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加 锁成功,就保证了加锁的可靠性。

在这里插入图片描述

MutiLock 加锁原理是

当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿 锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间 就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms 有线程加锁失败,则会再次去进行重试

在这里插入图片描述

Redisson 红锁
原理
	Redisson 红锁可以防止主从集群锁丢失问题。Redisson 红锁要求,必须要构建出至少三个 Redis 主从集群。若一个请求要申请锁,必须向所有主从集群中提交 key 写入请求,只有 当大多数集群锁写入成功后(过半),该锁才算申请成功。
修改启动类 Application
	我们这里要使用三个高可用的 Redis 主从集群,所以需要在启动类中添加三个 Sentinel 集群构建的 Redisson 的 Bean。由于这三个 Bean 将来要使用 byName 注入方式,所以这里为每个 Bean 指定了一个名称。

在这里插入图片描述

修改 Controller 类

在类中添加 Redisson 的 byName 方式的自动注入。

在这里插入图片描述

复制 seckillHandler6()方法并重命名为 seckillHandler7(),然后仅修改锁创建代码,其它代码不变。

在这里插入图片描述

问题
无论前面使用的是哪种锁,它们解决并发问题的思路都是相同的,那就将所有请求通过锁实现串行化。而串行化在高并发场景下势必会引发性能问题。
Redisson 实操
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");

//2、加锁
myLock.lock();      //阻塞式等待。默认加的锁都是30s

try {
      System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
      try { TimeUnit.SECONDS.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); }
   } catch (Exception ex) {
        ex.printStackTrace();
   } finally {
    //3、解锁  如果解锁代码没有运行(比如断电),Redisson并不会出现死锁问题,因为宕机了,不会有服务给看门狗续期,就会在到期后解锁
       System.out.println("释放锁..." + Thread.currentThread().getId());
       myLock.unlock();
   }


//原理:
    
1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
    将默认30秒加锁改为 'myLock.lock(10,TimeUnit.SECONDS);'   //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间(不会自动续期)
   问题:在锁时间到了以后,不会自动续期
        1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
        2、如果我们未指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
   只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔 internalLockLeaseTime/3=10秒都会自动的再次续期,续成30,只要业务在执行,就会一直重置时间
        internalLockLeaseTime 【看门狗时间30s 】 / 3, 也就是10s

 //最佳实践
1)、lock.lock(30,TimeUnit.SECONDS); 默认为30s解锁,省掉了整个续期操作,直接不使用看门狗机制。手动解锁,如果业务超过30s内执行结束,就相当于业务已经完蛋了,自动解锁就行了。
    
//读写锁:
     * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
     * 写锁没释放读锁必须等待
     *+ 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
     *+ 读 :必须等待写锁释放
     *+ 写 :阻塞方式
     *+ 写 :有读锁。写也需要等待
     * 只要有写的存都必须等待,不管写是在前还是在后
    写锁:RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
         RLock rLock = readWriteLock.writeLock();
    读锁:RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
         RLock rLock = readWriteLock.readLock();
    其余解锁方式是一样的
    
//闭锁
     * 放假、锁门
     * 1班没人了
     * 5个班,全部走完,我们才可以锁大门
     * 分布式闭锁
      RCountDownLatch door = redisson.getCountDownLatch("door");  
	  door.trySetCount(5); //等待五个
	  door.await();       //等待闭锁都完成
      ....都完成后开始执行业务
      //另一个方法:
      RCountDownLatch door = redisson.getCountDownLatch("door");
      door.countDown();       //计数-1
      

    
//信号量:   -- 可以用来做秒杀
     * 车库停车
     * 3车位
     * 信号量也可以做分布式限流
        RSemaphore park = redisson.getSemaphore("park");
		park.trySetPermits(3);  //可以秒杀的数量
        //park.acquire();     //获取一个信号、获取一个值,占一个车位,会等待,下面那个不等待
        boolean flag = park.tryAcquire();   //用来判断是否能够执行业务
        if (flag) {
            //执行业务
        } else {
            return "error";
        } 

        //另一个方法
		RSemaphore park = redisson.getSemaphore("park");
        park.release();     //释放一个车位

在这里插入图片描述

手写分布式锁

'上述内容是工作中用的,下面是纯手写,并优化锁的内容'

1、简单版本

Lock lock = new ReentrantLock();

public String sale(){
	String retMessage = “”;
	lock.lock();
	try{
		//1 查询库存信息
		String result = stringRedisTemplate.opsForValue().get(“inventory001”);
		//2 判断库存是否足够
		Integer inventoryNumber = result == null0 : 整数。parseInt(结果);
		//3 扣减库存
		if(inventoryNumber > 0) {
			stringRedisTemplate.opsForValue().set(" inventory001",String.valueOf(--inventoryNumber));
			retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
			System.out.println(retMessage);
		}else{
			retMessage = "商品卖完了,o(╥﹏╥)o";
		}finally {
			lock. unlock();
		}
		return retMessage+"\t"+"服务端口号:"+port;
    }
}

2、nginx下多台

在这里插入图片描述

启动两个上面同样的服务,就会出现超卖现象

在单机环境下,可以使用synchronizedLock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
分布式锁出现:
    跨进程 + 跨服务
    解决超卖
    防止缓存击穿

3、redis分布式锁

修改3.1版本
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    private Lock lock = new ReentrantLock();
    public String sale()
    {
        String retMessage = "";
        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
        if(!flag){
            //暂停20毫秒后递归调用
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
            sale();
        }else{
            try{
                //1 查询库存信息
                String result = stringRedisTemplate.opsForValue().get("inventory001");
                //2 判断库存是否足够
                Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //3 扣减库存
                if(inventoryNumber > 0) {
                    stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                    retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                    System.out.println(retMessage);
                }else{
                    retMessage = "商品卖完了,o(╥﹏╥)o";
                }
            }finally {
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}

通过递归重试的方式
测试Jmter压测5000ok
递归是一种思想没错,但是容易导致StackOverflowError,不太推荐,进一步完善
修改为3.2版本
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    private Lock lock = new ReentrantLock();
    public String sale()
    {
        String retMessage = "";
        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){  //什么时候为true了,什么时候跳出
            //暂停20毫秒,类似CAS自旋
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}

多线程判断想想JUC里面说过的虚假唤醒,用while替代if
用自旋替代递归重试

4、宕机与过期+防死锁

4、宕机与过期+防死锁

仅接3.2版本代码
'问题':部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定key

'修改4.1版本'
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue))
{
    //暂停20毫秒,进行递归重试.....
    try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);

'4.1结论'
设置key+过期时间分开了,必须要合并成一行具备原子性

'修改4.2版本'

while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)){
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
   }
   
'4.2结论'
jmter压测ok,加锁和过期时间必须同一行,保证原子性

5、防止误删key

在这里插入图片描述

4.2版本后续
'问题'
实际业务处理时间如果超过了默认设置key的过期时间,就会张冠李戴,删了别人的锁

'解决'
只能删除自己的,不许动别人的,代码升级为5.0
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    private Lock lock = new ReentrantLock();

    public String sale()
    {
        String retMessage = "";
        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
        {
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
                System.out.println(retMessage);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            // v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
            if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}

6、Lua保证原子性

5.0代码后续
'问题'
finaly块的判断 + del删除操作不是原子性的

启用lua脚本编写redis分布式锁判断 + 删除判断代码

finally {
        //V6.0 将判断+删除自己的合并为lua脚本保证原子性
         String luaScript =
                 "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
                     "return redis.call('del',KEYS[1]) " +
                  "else " +
                     "return 0 " +
                  "end";
         stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
        }



stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList(key),value);
stringRedisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(key),value); //使用该构造方法,不然报错

在这里插入图片描述

7、可重入锁+设计模式

6.0代码 	while判断并自旋重试获取锁+setnx含自然过期时间+Lua脚本官网删除锁命令

'问题'
如何兼顾锁的可重入问题?
可重入锁
'可重入锁又名递归锁'

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻==========================塞了岂不是天大的笑话,出现了作茧自缚==========================
所以'JavaReentrantLocksynchronized都是可重入锁',可重入锁的===================一个优点是可一定程度避免死锁。
===========================
'一个线程中的多个流程可以获取同一把锁','持有'这把同步锁可以再次进入。
自己可以获取自己的内部锁=================
可重入锁种类
隐式锁===================================================================================================================================================
	隐式锁(即synchronized关键字使用的锁)默认是可重入锁
	指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
	简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
	与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

'同步块'
public class ReEntryLockDemo
{
    public static void main(String[] args)
    {
        final Object objectLockA = new Object();
        new Thread(() -> {
            synchronized (objectLockA)
            {
                System.out.println("-----外层调用");
                synchronized (objectLockA)
                {
                    System.out.println("-----中层调用");
                    synchronized (objectLockA)
                    {
                        System.out.println("-----内层调用");
                    }
                }
            }
        },"a").start();
    }
}

'同步方法'
/**
 * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
 */
public class ReEntryLockDemo
{
    public synchronized void m1()
    {
        System.out.println("-----m1");
        m2();
    }
    public synchronized void m2()
    {
        System.out.println("-----m2");
        m3();
    }
    public synchronized void m3()
    {
        System.out.println("-----m3");
    }

    public static void main(String[] args)
    {
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
        reEntryLockDemo.m1();
    }
}


'Synchronized的重入的实现机理'
	每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
	当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
	在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
	当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。  
显式锁
显式锁(即Lock)也有RenntrantLock这样的可重入锁

/**
 * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
 */
public class ReEntryLockDemo
{
    static Lock lock = new ReentrantLock();

    public static void main(String[] args)
    {
        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("----外层调用lock");
                lock.lock();
                try
                {
                    System.out.println("----内层调用lock");
                }finally {
                    // 这里故意注释,实现加锁次数和释放次数不一样
                    // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                    lock.unlock(); // 正常情况,加锁几次就要解锁几次
                }
            }finally {
                lock.unlock();
            }
        },"a").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("b thread----外层调用lock");
            }finally {
                lock.unlock();
            }
        },"b").start();
    }
}

切记,一般而言,lock了几次就要unlock几次
redis实现
用哪个类型

在这里插入图片描述

思考,上述可重入锁技术问题,reids中哪个数据类型可以代替  
	k,k,v    hset
	Map<Strng, Map<Object, Object>>
	hset key field value    , hset redis锁名字(zzyyRedisLock)  某个请求线程的UUID+ThreadID  加锁的次数
	
'小总结'
	setnx,只能解决有无的问题,够用但是不完美
	hset,不但解决有无,还解决可重入问题
思考+设计重点
目前有2条支线
目的是保证同一个时候只能有一个线程持有锁进去redis扣减库存动作

2个分支
	1:保证加锁/解锁,lock/unlock
	2:扣减库存redis命令的原子性(下图)

在这里插入图片描述

Lua脚本实现
加锁lua脚本lock
'命令过程分析'
先判断reids分布式锁这个key是否存在 exists kye
	返回0,说明不存在,hset新建当前线程属于自己的锁BY UUIDThreadID
		HSET   zzyyRedisLock   0c90d37cb6ec42268861b3d739f8b3a8:1      1
		命令       key             value = UUID:ThreadID               次数
	返回0,说明已经有锁,需进一步判断是不是当前线程自己的(HEXISTS zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1)
		返回0,说明不是自己的
		返回1,说明是自己的锁,自增1次表示重入
			HINCRBY 		key 				field 					increment
			HINCRBY zzyyRedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 	 1
'将上述修改为Lua脚本'
'v1版本'
if redis.call('exists','key') == 0 then
  redis.call('hset','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
elseif redis.call('hexists','key','uuid:threadid') == 1 then
  redis.call('hincrby','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
else
  return 0
end

相同部分是否可以替换处理???
hincrby命令可否替代hset命令

'v2版本'
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then
  redis.call('hincrby','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
else
  return 0
end

'v3版本'		
  key		KEYS[1]				zzyyRedisLock
 value		ARGV[1]		 2f586ae740a94736894ab9d51880ed9d:1
过期时间值	 ARGV[2]			 30if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then 
  redis.call('hincrby',KEYS[1],ARGV[1],1) 
  redis.call('expire',KEYS[1],ARGV[2]) 
  return 1 
else
  return 0
end
解锁lua脚本unlock
'命令过程分析'
有锁且还是自己的锁(hexists key uuid:ThreadID)
	返回0,说明根本没锁,程序块返回nil
	不是0,说明有所且是自己的锁,直接调用 hincrby -1 表示每次减个1,解锁1次。直到它变为零表示可以删除该锁key,del锁key
'将上述修改为Lua脚本'
'v1版本'
if redis.call('HEXISTS',lock,uuid:threadID) == 0 then
 return nil
elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then
 return redis.call('del',lock)
else 
 return 0
end

'v2版本'
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then
 return nil
elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then
 return redis.call('del',KEYS[1])
else
 return 0
end
    
    
eval "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 zzyyRedisLock 2f586ae740a94736894ab9d51880ed9d:1
整合进微服务
结合设计模式(工厂)
'通过实现JUC里面的Lock接口,实现Redis分布式锁RedisDIstrbutedLock'
//@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;

    private String lockName;//KEYS[1]
    private String uuidValue;//ARGV[1]
    private long   expireTime;//ARGV[2]
    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
        this.expireTime = 30L;
    }
    @Override
    public void lock()
    {
        tryLock();
    }
    @Override
    public boolean tryLock()
    {
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    /**
     * 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
        if(time != -1L){
            this.expireTime = unit.toSeconds(time);
        }
        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                        "redis.call('expire',KEYS[1],ARGV[2]) " +
                        "return 1 " +
                "else " +
                        "return 0 " +
                "end";

        System.out.println("script: "+script);
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);

        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
        return true;
    }

    /**
     *干活的,实现解锁功能
     */
    @Override
    public void unlock()
    {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                "   return nil " +
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                "   return redis.call('del',KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";
        // nil = false 1 = true 0 = false
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("This lock doesn't EXIST");
        }

    }

    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }

    @Override
    public Condition newCondition()
    {
        return null;
    }
}


service直接使用上面的设计模式,有什么问题(下图)

在这里插入图片描述

考虑扩展,本次是reids实现分布式锁,以后zookeeper,mysql实现那?
引入工厂模式改造。

'DistributedLockFactory'
@Component
public class DistributedLockFactory
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;

    public Lock getDistributedLock(String lockType)
    {
        if(lockType == null) return null;

        if(lockType.equalsIgnoreCase("REDIS")){
            lockName = "zzyyRedisLock";
            return new RedisDistributedLock(stringRedisTemplate,lockName);
        } else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
            //TODO zookeeper版本的分布式锁实现
            return new ZookeeperDistributedLock();
        } else if(lockType.equalsIgnoreCase("MYSQL")){
            //TODO mysql版本的分布式锁实现
            return null;
        }

        return null;
    }
}


'RedisDistributedLock' 
public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;

    private String lockName;//KEYS[1]
    private String uuidValue;//ARGV[1]
    private long   expireTime;//ARGV[2]

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName){
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
        this.expireTime = 30L;
    }
    @Override
    public void lock(){
        tryLock();
    }
    @Override
    public boolean tryLock(){
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    /**
     * 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
        if(time != -1L){
            this.expireTime = unit.toSeconds(time);
        }
        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                        "redis.call('expire',KEYS[1],ARGV[2]) " +
                        "return 1 " +
                "else " +
                        "return 0 " +
                "end";
        System.out.println("script: "+script);
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
        return true;
    }

    /**
     *干活的,实现解锁功能
     */
    @Override
    public void unlock()
    {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                "   return nil " +
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                "   return redis.call('del',KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";
        // nil = false 1 = true 0 = false
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("This lock doesn't EXIST");
        }

    }

    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }

    @Override
    public Condition newCondition()
    {
        return null;
    }
}

'InventoryService'
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;
    
    public String sale()
    {

        String retMessage = "";

        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0)
            {
                inventoryNumber = inventoryNumber - 1;
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port;
                System.out.println(retMessage);
                return retMessage;
            }
            retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisLock.unlock();
        }
        return retMessage;
    }
}
可重入测试
'InventoryService类新增可重入测试方法'
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;

    public String sale()
    {
        String retMessage = "";
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
                System.out.println(retMessage);
                testReEnter();
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }

    private void testReEnter()
    {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            System.out.println("################测试可重入锁#######");
        }finally {
            redisLock.unlock();
        }
    }
}


'结果错了,ThreadID一致了,但是UUID不ok。每次进锁都会生成一个uuid'
'引入工厂改造'
'DistributedLockFactory'
@Component
public class DistributedLockFactory
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;

    public DistributedLockFactory()
    {
        //生成的是不带-的字符串,类似于:b17f24ff026d40949c85a24f4f375d42
        this.uuidValue = IdUtil.simpleUUID();//UUID   这个是在其他地方注入的时候就生成。保证是全局唯一的,这个可以后面可以再加一个线程编号,保证每个请求都是唯一的,uuid是区别不同的服务,再加线程编号区别不同请求。RedisDistributedLock的有参构造里面加了,看下面
    }

    public Lock getDistributedLock(String lockType)
    {
        if(lockType == null) return null;

        if(lockType.equalsIgnoreCase("REDIS")){
            lockName = "zzyyRedisLock";
            return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);
        } else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
            //TODO zookeeper版本的分布式锁实现
            return new ZookeeperDistributedLock();
        } else if(lockType.equalsIgnoreCase("MYSQL")){
            //TODO mysql版本的分布式锁实现
            return null;
        }
        return null;
    }
}

'RedisDistributedLock'
public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long   expireTime;

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }

    @Override
    public void lock()
    {
        this.tryLock();
    }
    @Override
    public boolean tryLock()
    {
        try
        {
            return this.tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        if(time != -1L)
        {
            expireTime = unit.toSeconds(time);
        }

        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                    "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                    "redis.call('expire',KEYS[1],ARGV[2]) " +
                    "return 1 " +
                "else " +
                    "return 0 " +
                "end";
        System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);

        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
        {
            try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        return true;
    }

    @Override
    public void unlock()
    {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                    "return nil " +
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                    "return redis.call('del',KEYS[1]) " +
                "else " +
                        "return 0 " +
                "end";
        System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("没有这个锁,HEXISTS查询无");
        }
    }

    //=========================================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }
    @Override
    public Condition newCondition()
    {
        return null;
    }
}

'InventoryService类新增可重入测试方法'
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;

    public String sale()
    {
        String retMessage = "";
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
                this.testReEnter();
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }


    private void testReEnter()
    {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            System.out.println("################测试可重入锁####################################");
        }finally {
            redisLock.unlock();
        }
    }
}

8、自动续期

确保 redisLock 过期时间大于业务执行时间的问题。redis分布式锁如何续期?
CAP
redis集群是AP
redis异步复制造成的锁丢失
比如:主节点没来的及刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据
Zookeeper集群是CP

在这里插入图片描述

在这里插入图片描述

Eureka集群是AP

在这里插入图片描述

Nacos集群是AP

在这里插入图片描述

lua脚本(加钟)
//==============自动续期
if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
  return redis.call('expire',KEYS[1],ARGV[2])
else
  return 0
end

代码新增自动续期功能
del掉之前的lockName  zzyyRedisLock
'RedisDistributedLock'

public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;

    private String lockName;//KEYS[1]
    private String uuidValue;//ARGV[1]
    private long   expireTime;//ARGV[2]

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate,String lockName,String uuidValue)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }
    @Override
    public void lock()
    {
        tryLock();
    }

    @Override
    public boolean tryLock()
    {
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    /**
     * 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        if(time != -1L)
        {
            this.expireTime = unit.toSeconds(time);
        }

        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                        "redis.call('expire',KEYS[1],ARGV[2]) " +
                        "return 1 " +
                        "else " +
                        "return 0 " +
                        "end";

        System.out.println("script: "+script);
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);

        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
        this.renewExpire();
        return true;
    }

    /**
     *干活的,实现解锁功能
     */
    @Override
    public void unlock()
    {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                        "   return nil " +
                        "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                        "   return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
        // nil = false 1 = true 0 = false
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("This lock doesn't EXIST");
        }
    }

    private void renewExpire()
    {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 " +
                        "end";

        new Timer().schedule(new TimerTask()
        {
            @Override
            public void run()
            {
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                    renewExpire();
                }
            }
        },(this.expireTime * 1000)/3);
    }

    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }

    @Override
    public Condition newCondition()
    {
        return null;
    }
}

9、总结

synchroized(单机版ok,上分布式死翘翘)
	nginx分布式微服务单机锁不行
		取消单机锁,上reids分布式锁setnx
			只加了锁,没有释放锁,必须要再代码层面finally释放锁
			宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockkey的过期时间设定
			为reids的分布式锁key,增加过期时间此外,还要setnx+过期时间必须同一行
				必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
					unlock变为Lua脚本保证
						锁重入,hset替代setnx+lock变为Lua脚本保证
							自动续期

Redlock算法

自研锁主要考点

	按照JUC里面java.util.concurrent.locks.Lock接口规范编写
	lock()加锁关键逻辑
		加锁	加锁实际上就是再redis钟,给key键设置一个值,为避免死锁,并给定一个过期时间
		自旋
		续期
	unlock解锁关键逻辑
		将key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除,只能自己删除自己的锁
		
'上面自研的reids锁对于一般中小公司,不是特别高并发场景足够了,单机redis小业务也撑得住'

Redlock红锁算法

为什么学

在这里插入图片描述

在这里插入图片描述

线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点,在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;redis 触发故障转移,其中一个 slave 升级为新的 master,此时新上位的master并不包含线程1写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁,'此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据'。
我们加的是排它独占锁,同一时间只能有一个建redis锁成功并持有锁,'严禁出现2个以上的请求线程拿到锁。危险的'。
算法设计理念
Redis也提供了Redlock算法,用来实现'基于多个实例的'分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。

在这里插入图片描述

'设计理念'

该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有NRedis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁客户端执行以下操作:
	1、获取当前时间,以毫秒为单位;
	2、依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁;
	3、客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
	4、如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
	5、如果由于某些原因未能获得锁(无法在至少 N/2 + 1Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
	
方案为了解决数据不一致的问题,'直接舍弃了异步复制只使用 master 节点',同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。本次演示用3台实例来做说明。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
条件2:客户端获取锁的总耗时没有超过锁的有效时间。

在这里插入图片描述

'解决方案'
为什么是奇数?  N = 2X + 1   (N是最终部署机器数,X是容错机器数)
 1 先知道什么是容错
  失败了多少个机器实例后我还是可以容忍的,所谓的容忍就是数据一致性还是可以Ok的,CP数据一致性还是可以满足
  加入在集群环境中,redis失败1台,可接受。2X+1 = 2 * 1+1 =3,部署3台,死了1个剩下2个可以正常工作,那就部署3台。
  加入在集群环境中,redis失败2台,可接受。2X+1 = 2 * 2+1 =5,部署5台,死了2个剩下3个可以正常工作,那就部署5台。
2 为什么是奇数?
   最少的机器,最多的产出效果
  加入在集群环境中,redis失败1台,可接受。2N+2= 2 * 1+2 =4,部署4台
  加入在集群环境中,redis失败2台,可接受。2N+2 = 2 * 2+2 =6,部署6

Redisson改造

在这里插入图片描述

'v9.0改造'

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

从现在开始不再用我们自己手写的锁了
'InventoryService2'
@Service
@Slf4j
public class InventoryService2
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;

    @Autowired
    private Redisson redisson;
    public String saleByRedisson()
    {
        String retMessage = "";
        String key = "zzyyRedisLock";
        RLock redissonLock = redisson.getLock(key);
        redissonLock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
          redissonLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}


上述代码会出现attempt tp unlock lock,not locked by current thread by node id :...

'v9.1版本'
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;

    @Autowired
    private Redisson redisson;
    public String saleByRedisson()
    {
        String retMessage = "";
        String key = "zzyyRedisLock";
        RLock redissonLock = redisson.getLock(key);
        redissonLock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            //需要判断下锁正在被持有,并且持有的正是当前线程才给解锁,否则极高并发的时候会出问题
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
            {
                redissonLock.unlock();
            }
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}

Redisson源码分析

Redis分布式锁过期了,但是业务还没处理完怎么办?
守护线程'续命',额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redission里面就实现了这个方案,使用'看门狗'定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间。
在获取锁成功后,给锁加一个 watchdog,watchdog会起一个定时任务,在锁没有被释放且快要过期的时候会续期。

在这里插入图片描述

上述源码分析

在这里插入图片描述

在这里插入图片描述

通过redisson新建出来的锁key,默认是30秒

RedissonLock.java

lock()—tryAcquire()—tryAcquireAsync()—

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。
在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。

在这里插入图片描述

watch dog自动延期机制
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始

在这里插入图片描述

自动续期的lua脚本

在这里插入图片描述

解锁

在这里插入图片描述

Redlock实现

在这里插入图片描述

这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于 防止了 单节点故障造成整个服务停止运行的情况且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法。

Redisson 分布式锁支持 MultiLock 机制可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。

最低保证分布式锁的有效性及安全性的要求如下:
1.互斥;任何时刻只能有一个client获取锁
2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
3.容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁

网上讲的基于故障转移实现的redis主从无法真正实现Redlock:
因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,这是clientB尝试获取锁,并且能够成功获取锁,导致互斥失效;
代码来源

2022年第8章第4小结(2023年已弃用)

在这里插入图片描述

2023年第8章第4小结

在这里插入图片描述

2023年第8章第3小结(重点)

在这里插入图片描述

案例
docker走起3台redis的master机器,本次设置3台master各自独立无从属关系
docker run -p 6381:6379 --name redis-master-1 -d redis
docker run -p 6382:6379 --name redis-master-2 -d redis
docker run -p 6383:6379 --name redis-master-3 -d redis

在这里插入图片描述

pom

 <dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.19.1</version>
</dependency>
yml
spring.redis.single.address1=192.168.111.185:6381
spring.redis.single.address2=192.168.111.185:6382
spring.redis.single.address3=192.168.111.185:6383

CacheConfiguration 
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {
    @Autowired
    RedisProperties redisProperties;
    @Bean
    RedissonClient redissonClient1() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient2() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient3() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }


    /**
     * 单机
     * @return
     */
    /*@Bean
    public Redisson redisson()
    {
        Config config = new Config();

        config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);

        return (Redisson) Redisson.create(config);
    }*/
}


RedisPoolProperties 
@Data
public class RedisPoolProperties {
    private int maxIdle;
    private int minIdle;
    private int maxActive;
    private int maxWait;
    private int connTimeout;
    private int soTimeout;
    /**
     * 池大小
     */
    private  int size;

}

RedisProperties 
@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperti
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值