文章目录

本系列文章:
  Redis(一)数据类型、常用命令
  Redis(二)Redis的高级特性、客户端的基本使用、持久化
  Redis(三)主从复制、哨兵、集群
  Redis(四)缓存、分布式锁

一、Redis中的高级特性

  为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题。

1.1 事务

  事务的原理是将一个事务范围内的若干命令发送给Redis,然后再让Redis依次执行这些命令。

  1)事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  2)事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

  Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
  总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令

广义上的事务有ACID特性:
 原子性(A):指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
 一致性(C):事务前后数据的完整性必须保持一致。
 隔离性(I):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
 持久性(D):指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

  在Redis中,事务具有一致性和隔离性,并且当Redis运行在某种特定的持久化模式下时,事务也具有持久性。但是,Redis中的事务不具有一致性,可能部分命令会执行成功,而另一部分命令执行失败。

  Redis事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  1. 批量操作在发送EXEC命令前被放入队列缓存。
  2. 收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  3. 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

  一个事务从开始到执行会经历以下三个阶段:开始事务、命令入队、执行事务。

  事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

1.1.1 事务命令(Multi开启/Exec执行/Discard取消/Watch监控,CAS实现的回滚/Unwatch取消监控)

  Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH实现的。事务功能有以下特点:

  1. Redis会将一个事务中的所有命令序列化,然后按顺序执行。
  2. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  3. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  4. 如果在一个事务中出现运行错误,那么其他正确的命令会被执行
  • 1、Multi
      Multi用于开启一个事务。 Multi执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被按照先后顺序放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • 2、Exec
      Exec命令用于执行所有事务块内的命令。这些命令按先后顺序排列。 当操作被打断时,返回空值nil 。即multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的。
      使用示例:
  • 3、Discard
      discard命令用于取消事务(清空事务队列),放弃执行事务块内的所有命令。示例:
  • 4、Watch
      Watch命令是一个乐观锁,可以为Redis事务提供(CAS)行为。其功能是:可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令(执行EXEC命令之后,就会自动取消监控)。可以简单理解为监控某个key是否被修改。示例:
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> set name 1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name 2
QUEUED
127.0.0.1:6379> set gender 1
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get gender
(nil)

  上面的代码中:

  1. watch name开启了对name这个key的监控。
  2. 修改name的值。
  3. 开启事务a。
  4. 在事务a中设置了name和gender的值。
  5. 使用EXEC命令进提交事务。
  6. 使用命令get gender发现不存在,即事务a没有执行。
  • 5、Unwatch
      Unwatch命令用于取消WATCH命令对所有key的监视。示例:
1.1.2 事务的生命周期(MULTI开启/EXEC提交)

  1、使用MULTI开启一个事务。
  3、在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行。
  3、EXEC命令进行提交事务。

  一个事务范围内某个命令出错不会影响其他命令的执行,不保证原子性。示例:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> set b 1 2
QUEUED
127.0.0.1:6379> set c 3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
3) OK
1.1.3 Redis事务保证原子性吗,支持回滚吗*

  Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行
  如果想实现回滚,就需要用WATCH命令,具体的做法是:

  需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
  当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。

1.1.4 Redis事务支持隔离性吗

  Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis的事务是总是带有隔离性的。

1.2 Lua

  Redis通过LUA脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或Redis命令被执行,实现组合命令的原子操作。
  lua脚本作用:

1、Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
2、Lua脚本可以将多条命令一次性打包,有效地减少网络开销。

  Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格)。

  • 1、字符串的简单使用
      定义一个字符串示例:
	local strings val = "world"

  local代表val是一个局部变量,如果没有local代表是全局变量。打印该变量示例:

	# 结果是 "world"
	print(hello)
  • 2、数组的简单使用
      如果要使用类似数组的功能,可以用tables类型。Lua的数组下标从1开始计算。定义tables类型的变量示例:
	local tables myArray = {"redis", "jedis", true, 88.0}
	# true
	print(myArray[3])

  使用for遍历数组示例:

local int sum = 0
for i = 1, 100
do
	sum = sum + i
end
# 输出结果为 5050
print(sum)

  table类型变量前加一个#,就代表table类型变量的长度。示例:

for i = 1, #myArray
do
	print(myArray[i])
end

  遍历索引和值示例:

for index,value in ipairs(myArray)
do
	print(index)
	print(value)
end

  除了可以用for,也可以用while来遍历,示例:

local int sum = 0
local int i = 0
while i <= 100
do
	sum = sum +i
	i = i + 1
end
-- 输出结果为 5050
print(sum)
  • 3、哈希的简单使用
      如果要使用类似哈希的功能,同样可以使用tables类型。示例定义一个tables,每个元素包含了key和value,其中strings1…string2是将两个字符串进行连接:
local tables user_1 = {age = 28, name = "tome"}
--user_1 age is 28
print("user_1 age is " .. user_1["age"])

  如果要遍历user_1,可以使用Lua的内置函数pairs,示例:

for key,value in pairs(user_1)
do print(key .. value)
end
  • 4、函数定义
      在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体。示例:
function funcName()
	...
end
contact 函数将两个字符串拼接:
function contact(str1, str2)
	return str1 .. str2
end
--"hello world"
print(contact("hello ", "world"))
1.2.1 使用Lua脚本
  • 1、eval和evalsha
      在Redis中执行Lua脚本有两种方法:eval和evalsha。 eval 命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。
      eval使用语法:
	eval 脚本内容 key 个数 key 列表 参数列表

  如果Lua脚本较长,还可以使用redis-cli–eval直接执行文件。
  eval命令和–eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端,过程图示:

  除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。使用evalsha执行Lua脚本过程图示:

  如图所示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
  加载脚本:script load命令可以将脚本内容加载到Redis内存中,例如将lua_get.lua加载到Redis中,得到SHA1:

	# redis-cli script load "$(cat lua_get.lua)"
	"7413dc2440db1fea7c0a0bde841fa68eefaf149c"

  执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和eval一致:

	evalsha 脚本 SHA1 值 key 个数 key 列表 参数列表
  • 2、Lua的Redis API
      Lua可以使用redis.call函数实现对Redis的访问,Lua使用redis.call调用了Redis的set和get操作示例:
	redis.call("set", "hello", "world")
	redis.call("get", "hello")

  除此之外,Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本。

1.2.2 管理Lua脚本

  Redis提供了4个命令实现对Lua脚本的管理。

  • 1、script load
	script load script

  此命令用于将Lua脚本加载到Redis内存中。

  • 2、script exists
	scripts exists sha1 [sha1 … ]

  此命令用于判断sha1是否已经加载到Redis内存中。示例:

127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 1

  返回结果代表sha1[sha1…]被加载到Redis内存的个数。

  • 3、script flush
      此命令用于清除Redis内存已经加载的所有Lua脚本。
  • 4、script kill
      此命令用于杀掉正在执行的Lua脚本。
      Redis提供了一个lua-time-limit参数,默认是5秒,它是Lua脚本的“超时时间”,但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个busy的脚本。
      如果当前Lua脚本正在执行写操作,那么script kill将不会生效。

1.3 【发布订阅(pub/sub)】

  Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到对应的消息。

  订阅者可订阅一个、多个、匹配的订阅 topic。
  发布主题会被立即转发到订阅该主题的消费者,如果没有消费者该消息会被丢弃。因此,会存在丢消息的风险(当宕机,断网,或者网络闪断丢失报文时)。由于该种特性,导致简单的 pubsub 会存在丢失响应的风险.

1.3.1 命令(publish发布/subscribe订阅/unsubscribe取消订阅)

  1、发布消息的命令:

publish channel message

  下面的命令会向channel:sports频道发布一条消息“Tim won the championship”,返回结果为订阅者个数,因为此时没有订阅,所以返回结果为0:

  2、订阅消息的命令:

subscribe channel [channel ...

  订阅者可以订阅一个或多个频道,下面操作为当前客户端订阅了channel:sports频道:

  如果此时另一个客户端发布了消息:

  订阅者就可以收到该消息:

  需要注意的是,新开启的订阅客户端,无法收到该频道之前的消息,因为Rdis不会对发布的消息进行持久化。
  3、取消订阅的命令:

unsubscribe [channel [channel ...]

  客户端可以通过unsubscribe命令取消对指定频道的订阅,取消成功后,不会再收到该频道的发布消息:

  4、按照模式订阅和取消订阅的命令:

psubscribe pattern [pattern...
punsubscribe [pattern [pattern ..]

  例如下面操作订阅以it开头的所有频道:

  5、查看活跃频道的命令:

pubsub channels [pattern]

  所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式。示例:

  6、查看频道订阅数的命令:

pubsub numsub [channel...]

  比如当前channel:sports频道的订阅数为2:

  7、查看模式订阅数的命令:

pubsub numpat

  比如当前只有一个客户端通过模式来订阅:

1.3.2 使用场景(聊天室、公告牌、服务之间通信)

  聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,以简单的服务解耦进行说明。如图所示,图中有两套业务,上面为视频管理系统,负责管理视频信息;下面为视频服务面向客户,用户可以通过各种客户端(手机、浏览器、接口)获取到视频信息。
  假如视频管理员在视频管理系统中对视频信息进行了变更,希望及时通知给视频服务端,就可以采用发布订阅的模式,发布视频信息变化的消息到指定频道,视频服务订阅这个频道及时更新视频信息,通过这种方式可以有效解决两个业务的耦合性。

1.4 【消息传递(Stream)】

  Redis Stream 是 Redis 5.0 版本中引入的一种新的数据结构,它用于实现简单但功能强大的消息传递模式。
  Stream是一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。图示:

  每个 Redis Stream 都有唯一的名称 ,对应唯一的 Redis Key 。
  同一个 Stream 可以挂载多个消费组 ConsumerGroup , 消费组不能自动创建,需要使用 XGROUP CREATE 命令创建。
  每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动 ,标识当前消费组消费到哪条消息了。
  消费组 ConsumerGroup 同样可以挂载多个消费者 Consumer , 每个 Consumer 并行的读取消息,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
  消费者内部有一个属性 pending_ids , 记录了当前消费者读取但没有回复 ACK 的消息 ID 列表 。

1.4.1 Stream命令
  • 1、XADD( 向Stream末尾添加消息)
      使用 XADD 向队列添加消息,如果指定的队列不存在,则创建一个队列。示例:
XADD key ID field value [field value ...]

key :队列名称,如果不存在就创建
ID :消息 id,使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
field value : 记录。

  示例:

127.0.0.1:6379> XADD mystream * name1 value1 name2 value2
"1712473185388-0"
127.0.0.1:6379> XLEN mystream
(integer) 1
127.0.0.1:6379> XADD mystream * name2 value2 name3 value3
"1712473231761-0"

  消息 ID 的格式: 毫秒级时间戳 + 序号 , 例如:1712473185388-5 , 它表示当前消息在毫秒时间戳 1712473185388 产生 ,并且该毫秒内产生到了第5条消息。
  在添加队列消息时,也可以指定队列的长度。示例:

127.0.0.1:6379> XADD mystream MAXLEN 100 * name value1 age 30
"1713082205042-0"

  上述命令的含义:使用 XADD 命令向 mystream 的 stream 中添加了一条消息,并且指定了最大长度为 100。消息的 ID 由 Redis 自动生成,消息包含两个字段 name 和 age,分别对应的值是 value1 和 30。

  • 2、XRANGE( 获取消息列表)
      使用 XRANGE 获取消息列表,会自动过滤已经删除的消息。示例:
XRANGE key start end [COUNT count]

key :队列名
start :开始值, - 表示最小值
end :结束值, + 表示最大值
count :数量

  示例:

127.0.0.1:6379> XRANGE mystream - + COUNT 2
1) 1) "1712473185388-0"
   2) 1) "name1"
      2) "value1"
      3) "name2"
      4) "value2"
2) 1) "1712473231761-0"
   2) 1) "name2"
      2) "value2"
      3) "name3"
      4) "value3"

  得到两条消息,第一层是消息 ID ,第二层是消息内容 ,消息内容是 Hash 数据结构 。

  • 3、XREAD(以阻塞/非阻塞方式获取消息列表)
      使用 XREAD 以阻塞或非阻塞方式获取消息列表。示例:
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]

count :数量
milliseconds :可选,阻塞毫秒数,没有设置就是非阻塞模式
key :队列名
id :消息 ID

  示例:

127.0.0.1:6379> XREAD streams mystream 0-0
1) 1) "mystream"
   2) 1) 1) "1712473185388-0"
         2) 1) "name1"
            2) "value1"
            3) "name2"
            4) "value2"
      2) 1) "1712473231761-0"
         2) 1) "name2"
            2) "value2"
            3) "name3"
            4) "value3"

  XRED 读消息时分为阻塞和非阻塞模式,使用 BLOCK 选项可以表示阻塞模式,需要设置阻塞时长。非阻塞模式下,读取完毕(即使没有任何消息)立即返回,而在阻塞模式下,若读取不到内容,则阻塞等待。

127.0.0.1:6379> XREAD block 1000 streams mystream $
(nil)
(1.07s)

  使用 Block 模式,配合 $ 作为 ID ,表示读取最新的消息,若没有消息,命令阻塞!等待过程中,其他客户端向队列追加消息,则会立即读取到。典型的队列就是 XADD 配合 XREAD Block 完成。XADD 负责生成消息,XREAD 负责消费消息。

  • 4、XGROUP CREATE(创建消费者组)
      使用 XGROUP CREATE 创建消费者组,分两种情况:从头开始消费和从尾部开始消费。
      从头开始消费:
XGROUP CREATE mystream consumer-group-name 0-0  

  从尾部开始消费:

XGROUP CREATE mystream consumer-group-name $
  • 5、XREADGROUP GROUP(读取消费组中的消息)
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]

group :消费组名
consumer :消费者名。
count : 读取数量。
milliseconds : 阻塞毫秒数。
key : 队列名。
ID : 消息 ID。

  示例:

127.0.0.1:6379>  XREADGROUP group mygroup consumerA count 1 streams mystream >
1) 1) "mystream"
   2) 1) 1) "1712473185388-0"
         2) 1) "name1"
            2) "value1"
            3) "name2"
            4) "value2"

  上述的含义:消费者组 mygroup 中的消费者 consumerA ,从 名为 mystream 的 Stream 中读取消息。

COUNT 1 表示一次最多读取一条消息。
表示消息的起始位置是当前可用消息的 ID,即从当前未读取的最早消息开始读取。

  • 6、XACK(消息消费确认)
      接收到消息之后,我们要手动确认一下(ack):
xack key group-key ID [ID ...]

  示例:

127.0.0.1:6379> XACK mystream mygroup 1713089061658-0
(integer) 1

  消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 ack 确认消息已经被消费完成,整个流程的执行:

  可以使用 xpending 命令查看消费者未确认的消息ID:

127.0.0.1:6379> xpending mystream mygroup
1) (integer) 1
2) "1713091227595-0"
3) "1713091227595-0"
4) 1) 1) "consumerA"
      2) "1"
  • 7、XTRIM(限制 Stream 长度)
      使用 XTRIM 对流进行修剪,限制长度。示例:
127.0.0.1:6379> XADD mystream * field1 A field2 B field3 C field4 D
"1712535017402-0"
127.0.0.1:6379> XTRIM mystream MAXLEN 2
(integer) 4
127.0.0.1:6379> XRANGE mystream - +
1) 1) "1712498239430-0"
   2) 1) "name"
      2) "zhangyogn"
2) 1) "1712535017402-0"
   2) 1) "field1"
      2) "A"
      3) "field2"
      4) "B"
      5) "field3"
      6) "C"
      7) "field4"
      8) "D"
1.4.2 Stream做消息队列的优(消息不丢失)缺(只能当作轻量级消息队列)点

  stream 用于消息队列最大的进步在于:实现了发布订阅模型。
  发布订阅模型具有如下特点:

  消费独立。相比队列模型的匿名消费方式,发布订阅模型中消费方都会具备的身份,一般叫做订阅组(订阅关系),不同订阅组之间相互独立不会相互影响。
  一对多通信。基于独立身份的设计,同一个主题内的消息可以被多个订阅组处理,每个订阅组都可以拿到全量消息。因此发布订阅模型可以实现一对多通信。

  • 优点
      用List 数据结构用做队列时,因为消费时没有 Ack 机制,应用异常挂掉导致消息偶发丢失的情况,Redis Stream解决了这个问题。用Stream实现时,消费者内部有一个属性 pending_ids , 记录了当前消费者读取但没有回复 ACK 的消息 ID 列表 。当消费者重新上线,这些消息可以重新被消费
  • 缺点
      Redis 本身定位是内存数据库,它的设计之初都是为缓存准备的,并不具备消息堆积的能力。而专业消息队列一个非常重要的功能是数据中转枢纽,Redis 的定位很难满足。Redis 非常适合轻量级消息队列解决方案,轻量级意味着:数据量可控 + 业务模型简单 。

二、Redis客户端

  Redis支持的Java客户端有Redisson、Jedis、lettuce,官方推荐使用Redisson。
  几乎所有的主流编程语言都有Redis的客户端,Redis的客户端的特点:

  1. 客户端与服务端之间的通信协议是在TCP协议之上构建的。
  2. Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

2.1 Java客户端Jedis

  Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。

2.1.1 Jedis的简单使用
  • 1、引入依赖
      创建一个SpringBoot项目,引入Jedis的依赖:
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.8.2</version>
</dependency>

  对于第三方开发包,版本的选择也是至关重要的,通常来讲选取第三方开发包有如下两个策略:

  1. 选择比较稳定的版本,也就是尽可能选择稳定的里程碑版本。
  2. 选择更新活跃的第三方开发包,例如Redis3.0有了Redis Cluster新特性。
  • 2、Jedis的使用
      示例:
	//生成一个Jedis对象,初始化了Redis实例的IP和端口,这个对象和指定Redis实例进行通信
	Jedis jedis = new Jedis("127.0.0.1", 6379);
	jedis.set("hello", "world");
	System.out.println(jedis.get("hello")); //world

  除了上面的Jedis构造方法,还有一个包含了四个参数的构造函数是比较常用的:

	Jedis(final String host, final int port, final int connectionTimeout, 
		final int soTimeout)

  4个参数的意义:

host:Redis实例的所在机器的IP。
port:Redis实例的端口。
connectionTimeout:客户端连接超时。
soTimeout:客户端读写超时。

  在实际使用Jedis时,肯定要注意关闭流之类的操作:

		Jedis jedis = null;
		try {
			jedis = new Jedis("127.0.0.1", 6379);
			System.out.println(jedis.get("hello"));
		} catch (Exception e) {
			System.out.println(e.getMessage());
		} finally {
			//关闭流
			if (jedis != null) {
				jedis.close();
			}
		}

  Jedis对于Redis五种数据结构的简单操作示例:

	Jedis jedis = new Jedis("127.0.0.1", 6379);
	// 1.string
	jedis.set("hello", "world"); 
	System.out.println(jedis.get("hello")); //world
	// 2.hash
	jedis.hset("myhash", "f1", "v1");
	jedis.hset("myhash", "f2", "v2");
	System.out.println(jedis.hgetAll("myhash")); //{f2=v2, f1=v1}
	// 3.list
	jedis.rpush("mylist", "1");
	jedis.rpush("mylist", "2");
	jedis.rpush("mylist", "3");
	System.out.println(jedis.lrange("mylist", 0, -1)); //[1, 2, 3]
	// 4.set
	jedis.sadd("myset", "a");
	jedis.sadd("myset", "b");
	jedis.sadd("myset", "a");
	System.out.println(jedis.smembers("myset")); //[a, b]
	// 5.zset
	jedis.zadd("myzset", 99, "tom");
	jedis.zadd("myzset", 66, "peter");
	jedis.zadd("myzset", 33, "james");
	System.out.println(jedis.zrange("myzset", 0, -1)); //[james, peter, tom]
2.1.2 Jedis常用API
  • 1、Jedis中对键通用的操作
//判断某个键是否存在
public Boolean exists(String key)
// 新增或更新数据,返回"OK"代表成功
public String set(final String key, String value)
//获取所有符合格式的key,如jedis.keys(*)则是返回所有key
public Set<String> keys(final String pattern)
//删除key
public Long del(String key)
//设置键为key的过期时间为seconds秒
public Long expire(final String key, final int seconds) 
//获取key数据项的剩余时间(秒)
public Long ttl(final String key)
//移除键为key属性项的生存时间限制
public Long persist(final String key)
//查看键为key所对应value的数据类型
public String type(final String key)
  • 2、Jedis中的字符串操作
      字符串类型是Redis中最为基础的数据存储类型,在Redis中字符串类型的Value最多可以容纳的数据长度是512M。
//不覆盖增加数据,设置成功返回1,设置失败返回0
public Long setnx(final String key, final String value)
//增加数据并设置有效时间,相当于SET + EXPIRE命令
public String setex(final String key, final int seconds, final String value)
//获取键为key对应的value
public String get(final String key)
//在key对应value 后边追加字符串
public Long append(final String key, final String value)
//获取多个key对应的value列表
public List<String> hmget(final String key, final String... fields)
//更新key对应的value并返回旧value
public String getSet(final String key, final String value)

  Jedis中的增减操作:

//将key对应的value加1并返回
public Long incr(final String key)
//将key对应的value加指定数值并返回
public Long incrBy(final String key, final long integer)
//将key对应的value减1并返回
public Long decr(final String key)
//将key对应的value减n并返回
public Long decrBy(final String key, final long integer)
  • 3、Jedis中的列表操作
//添加一个List , 如果已经有该List对应的key, 则按顺序在左边追加 一个或多个
public Long lpush(final String key, final String... strings)
//添加一个List , 如果已经有该List对应的key, 则按顺序在右边追加 一个或多个
public Long rpush(final String key, final String... strings) 
//获取key对应list区间[i,j]的元素,注:从左边0开始,包头包尾
public List<String> lrange(final String key, final long start, final long end)
//删除list区间[i,j] 之外的元素
public String ltrim(final String key, final long start, final long end)
//左弹出一个key对应的元素
public String lpop(final String key)
//右弹出一个key对应的元素
public String rpop(final String key) 
//获取key对应list的长度
public Long llen(final String key)
//修改key对应的list指定下标index的元素
public String lset(final String key, final long index, final String value)
//获取key对应list下标为index的元素
public String lindex(final String key, final long index)
  • 4、Jedis中的集合操作
//向一个set中添加元素
public Long sadd(final String key, final String... members)
//获取key对应set的所有元素
public Set<String> smembers(final String key)
//删除set中的特定值
public Long srem(final String key, final String... members)
//获取集合交集
public Set<String> sinter(final String... keys)
//获取集合的并集
public Set<String> sunion(final String... keys)
//获取集合的差集
public Set<String> sdiff(final String... keys)
  • 5、Jedis中的有序集合操作
//向有序集合中新增或更新元素
public Long zadd(final String key, final Map<String, Double> scoreMembers)
//向有序集合中新增元素
public Long hset(final String key, final String field, final String value) 
//获取有序集合中下标[i,j] 区间元素Val
public Set<String> zrange(final String key, final long start, final long end)
//获取有序集合中制定元素的Score
public Double zscore(final String key, final String member)
//删除有序集合里的指定元素
public Long zrem(final String key, final String... members)
//获取有序集合的元素个数
public Long zcard(final String key)
//获取有序集合中score在[i,j]区间的元素个数
public Long zcount(final String key, final double min, final double max)
public Long zcount(final String key, final String min, final String max)
//把有序集合中中value元素的score+=n
public Double zincrby(final String key, final double score, final String member)
  • 6、Jedis中的哈希操作
//新增或更新一个Hash 
public String hmset(final String key, final Map<String, String> hash)
//新增或更新Hash中的指定元素
public Long hset(final String key, final String field, final String value)
//获取Hash的所有元素
public Map<String, String> hgetAll(final String key)
//获取Hash中所有元素的key
public Set<String> hkeys(final String key)
//获取Hash所有元素的value
public List<String> hvals(final String key)
//从Hash中删除一个或多个元素
public Long hdel(final String key, final String... fields)
//获取Hash中元素的个数
public Long hlen(final String key)
//判断Hash中是否存在指定key对应的元素
public Boolean hexists(final String key, final String field)
//获取Hash中一个或多个元素value
public List<String> hmget(final String key, final String... fields)
2.1.3 Jedis连接池

  客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化好Jedis连接,每次只需要从Jedis连接池借用即可,只有少量的并发同步开销,远远小于新建TCP连接的开销。两者的比较:

优点缺点
直连简单方便,适用于少量长期连接的场景1、存在每次新建/关闭TCP连接开销;
2、资源无法控制,可能会出现连接泄露;
3、Jedis对象线程不安全
连接池1、无需每次连接都生成Jedis对象,降低开销;
2、使用连接池控制开销
使用较麻烦,要熟悉各个参数的意义

  连接池的基本使用:

		//使用默认配置
		GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
		//初始化Jedis连接池
		JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
		Jedis jedis = null;
		try {
			//从连接池获取jedis对象
			jedis = jedisPool.getResource();
			System.out.println(jedis.get("hello")); //world
		} catch (Exception e) {
		} finally {
			if (jedis != null) {
				//close操作不是关闭连接,代表归还连接池
				jedis.close();
			}
		}

  在GenericObjectPoolConfig中,可以设置很多关于Redis连接池的属性,一些较常用的设置:

	GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
	//设置最大连接数为默认值的 5 倍
	poolConfig.setMaxTotal(GenericObjectPoolConfig.DEFAULT_MAX_TOTAL * 5);
	//设置最大空闲连接数为默认值的 3 倍
	poolConfig.setMaxIdle(GenericObjectPoolConfig.DEFAULT_MAX_IDLE * 3);
	//设置最小空闲连接数为默认值的 2 倍
	poolConfig.setMinIdle(GenericObjectPoolConfig.DEFAULT_MIN_IDLE * 2);
	//设置开启 jmx 功能
	poolConfig.setJmxEnabled(true);
	//设置连接池没有连接后客户端的最大等待时间 ( 单位为毫秒 )
	poolConfig.setMaxWaitMillis(3000);

  GenericObjectPoolConfig的重要属性:

参数名含义默认值
maxActive连接池中最大连接数8
maxIdle连接池中最大空闲的连接数8
minIdle连接池中最少空闲的连接数0
maxWaitMillis当连接池资源耗尽后,调用者的最大等待时间(单位为毫秒),一般不建议使用默认值-1永远不超时,一直等:
jmxEnabled是否开启jmx监控,如果应用开启了jmx端口,并且jmxEnabled设置为true,就可以通过jconsole或jvisualvm看到关于连接池的相关统计,有助于了解连接池的使用情况,并且可以针对做监控统计true
minEvictableIdleTimeMillis连接的最小空闲时间,达到此值后空闲连接将被移除1000L x 60L x 30毫秒 = 30分钟
numTestsPerEvictionRun做空闲连接检测时,每次的采样数3
testOnBorrow向连接池借用连接时是否做连接有效性检测(ping),无效连接将被移除,每次借用多执行一次ping命令false
testOnReturn是否做周期性空闲检测false
testWhileIdle向连接池借用连接时是否做连接空闲检测,空闲超时的连接会被移除false
timeBetweenEvictionRunsMillis空闲连接的检测周期(单位为毫秒)-1:表示不做检测
blockWhenExhausted当连接池用尽后,调用者是否要等待,这个参数和maxWaitMillis对应,当此参数为true时,maxWaitMillis 才生效false

2.2 常见异常

2.2.1 无法从连接池获取到连接(最大连接数设置过小/使用后未释放连接)

  2种情况:

  1、假设JedisPool中的Jedis对象个数是8个,8个Jedis对象被占用,并且没有归还,此时还要从JedisPool中借用Jedis,就需要进行等待(例如设置maxWaitMillis>0),如果在maxWaitMillis时间内仍然无法获取到Jedis对象就会抛出JedisConnectionException。
  2、设置了blockWhenExhausted=false,那么调用者发现池子中没有Jedis资源时,会立即抛出异常不进行等待。

  一些可能会造成连接池中资源被耗尽的原因:

  1. 高并发下连接池设置过小
      解决方法:可以增大连接池的最大连接数。
  2. 没有正确使用连接池,比如没有进行释放
      解决方法:每次使用完连接池后释放连接。
  3. 存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢
      慢查询的情况较多,比如:使用复杂度过高的命令、操作的value很大、很多key在同一时间集中过期。此时就要根据不同的情况分别进行处理。
2.2.2 客户端读写超时(读写超时间设置得过短/命令执行时间过长)

  SocketTimeoutException,原因有以下几种:

1、读写超时间设置得过短。
2、命令本身执行就比较慢。
3、客户端与服务端网络不正常。
4、Redis服务端自身发生阻塞。

2.2.3 客户端连接超时(连接超时时间设置得过短/服务端阻塞)

  JedisConnectionException,原因有以下几种:

1、连接超时设置得过短,修改示例:jedis.getClient().setConnectionTimeout(time);,单位毫秒。
2、Redis服务端发生阻塞造成新的连接失败。
3、客户端与服务端网络不正常。

2.2.4 客户端缓冲区异常

  Jedis在调用Redis时,如果出现客户端数据流异常,会出现JedisConnectionException。原因有以下几种:

1、输出缓冲区满。
2、长时间闲置连接被服务端主动断开。
3、不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现该异常。

2.2.5 JedisDataException(Redis正在加载持久化文件/内存超过maxmemory配置/客户端连接数过大)

  Jedis调用Redis时,如果Redis正在加载持久化文件:

  Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会报如下异常:

  如果客户端连接数超过了maxclients,新申请的连接就会出现如下异常:

三、持久化

  Redis 是内存型数据库,为了之后重用数据(比如重启机器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置,需要将内存中的数据持久化到硬盘上。
  Redis提供了RDB(快照)和AOF(只追加文件)两种持久化方式,默认是只开启RDB。
  Redis的持久化机制有两种,第一种是快照,第二种是AOF日志。快照是一次全量备份AOF日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,而AOF日志记录的是内存数据修改的指令记录文本。AOF日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载AOF日志进行指令重放,这个时间就会无比漫长。所以需要定期进行AOF重写,给AOF日志进行瘦身。一般将两者结合使用。

3.1 【RDB(数据快照)】

  RDB(Redis DataBase)是Redis默认的持久化方式。RDB持久化的方式是:按照一定的时间规律将(某个时间点的)内存的数据以快照(快照是数据存储的某一时刻的状态记录)的形式保存到硬盘中,对应产生的数据文件为dump.rdb(二进制文件)。可以通过配置文件(redis.conf,在Windows系统上是redis.windows.conf)中的save参数来定义快照的周期。
  Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。

3.1.1 RDB触发机制(save/bgsave)

  总的来说,RDB持久化过程的触发方式分为手动触发(用户执行SAVE或BGSAVE命令)和自动触发。
  RDB的三种主要触发机制:

  • 1、save命令(同步数据到磁盘上)
      由于save命令是同步命令,会占用Redis的主进程。若Redis数据非常多时,save命令执行速度会非常慢,并且该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。因此很少在生产环境直接使用SAVE 命令,可以使用BGSAVE 命令代替。如果在BGSAVE命令的保存数据的子进程发生错误的时,用 SAVE命令保存最新的数据是最后的手段。示例:
redis 127.0.0.1:6379> SAVE 
OK

SAVE命令执行快照的过程会阻塞所有客户端的请求,应避免在生产环境使用此命令。

  • 2、bgsave命令(异步保存数据到磁盘上)
      bgsave 是主流的触发 RDB 持久化的方式。
      Redis使用Linux系统的fock()生成一个子进程来将DB数据保存到磁盘,主进程继续提供服务以供客户端调用。如果操作成功,可以通过客户端命令LASTSAVE来检查操作结果。示例:
127.0.0.1:6379> BGSAVE
Background saving started
127.0.0.1:6379> LASTSAVE
(integer) 1632563411

BGSAVE命令可以在后台异步进行快照操作,快照的同时服务器还可以继续响应客户端的请求,因此需要手动执行快照时推荐使用BGSAVE命令。

  • 3、自动生成RDB
      除了手动执行save和bgsave命令实现RDB持久化以外,Redis还提供了自动自动生成RDB的方式。
      你可以通过配置文件对 Redis 进行设置, 让它在“N秒内数据集至少有M个key改动”这一条件被满足时, 自动进行数据集保存操作(其实Redis本身也是这样做的)。比如:
save 300 10

  该命令表示“如果距离上一次创建RDB文件已经过去了300秒,并且服务器的所有数据库总共已经发生了不少于10次修改,那么系统自动执行BGSAVE命令”。

  自动触发RDB持久化不仅仅有save m n这一种方式,以下方式均能自动触发RDB持久化:

1)使用save相关配置,如save m n,表示m秒内数据集存在n次修改时,自动触发bgsave。
2)如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
3)执行debug reload命令重新加载Redis时,也会自动触发save操作。
4)默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave

3.1.2 Redis默认的RDB配置

  在redis.conf配置文件中默认有此下配置:

#在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 900 1              

#在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10            

#在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000        

  根据配置,快照将被写入dbfilename(默认值是dump.rdb)指定的文件里面,并存储在dir(默认值是./)选项指定的路径上。
  举个例子:假设Redis的上一个快照是2:35开始创建的,并且已经创建成功。下午3:06时,Redis又开始创建新的快照,并且在下午3:08快照创建完毕之前,有35个键进行了更新。如果在下午3:06到3:08期间,系统发生了崩溃,导致Redis无法完成新快照的创建工作,那么Redis将丢失下午2:35之后写入的所有数据。另一方面,如果系统恰好在新的快照文件创建完毕之后崩溃,那么Redis将丢失35个键的更新数据。
  结论:RDB的持久化方式会丢失部分更新数据

  RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。
  当遇到坏盘或磁盘写满等情况时,可以通过config set dir{newDir}在线修改文件路径到可用的磁盘路径,之后执行bgsave进行磁盘切换,同样适用于AOF持久化文件。

3.1.3 RDB持久化流程(fork出子进程去生产RDB文件)

  bgsave命令的执行流程:

  1、执行BGSAVE命令。
  2、Redis父进程判断当前是否存在正在执行的子进程,如果存在,BGSAVE命令直接返回。
  3、父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞。
  4、父进程fork完成后,父进程继续接收并处理客户端的请求,而子进程开始将内存中的数据写进硬盘的临时文件。
  5、当子进程写完所有数据后会用该临时文件替换旧的RDB文件。

  Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。通过RDB方式的持久化,一旦Redis异常退出,就会丢失最近一次持久化以后更改的数据。

  RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行config set dir {newDir}config set dbfilename {newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。
  Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config set rdbcompression {yes|no}动态修改。

3.1.4 【RDB的优(恢复速度快/占用空间小/不影响对外读写)缺(达不到实时性要求)点】
  • 1、RDB的优点
      1)RDB文件是紧凑的二进制文件(一个文件dump.rdb),比较适合做冷备,全量复制的场景,方便持久化;
      2) 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速
      3)RDB对Redis对外提供的读写服务,影响非常小,可以让Redis保持高性能,因为Redis主进程使用单独子进程来进行持久化,主进程不会进行任何IO操作。
  • 2、RDB的缺点
      1) RDB方式数据无法做到实时持久化或者秒级持久化。因为BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本比较高。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦Redis进程宕机,那么会丢失最近5分钟的数据
      2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒;
      3)RDB文件使用特定二进制格式保存,Redis版本升级过程中有多个格式的RDB版本,存在老版本Redis无法兼容新版RDB格式的问题。

  如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。

3.2 AOF(数据更新命令)

  AOF持久化:将写命令添加到AOF文件(Append Only File)的末尾。
  与RDB持久化通过保存数据库中键值对来保存数据库的状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库的状态。当重启Redis会重新将持久化的日志中文件恢复数据。
  与快照持久化相比,AOF持久化的实时性更好,因此已成为主流的持久化方案。
  开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名通过appendfilename设置,默认文件名是appendonly.aof。
  当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复
  AOF的主要作用是解决了数据持久化的实时性问题。

3.2.1 AOF原理*

  AOF日志存储的是Redis服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令记录。
  假设AOF日志记录了自Redis实例创建以来所有的修改性指令序列,那么就可以通过对一个空的Redis实例顺序执行所有的指令,也就是「重放」,来恢复Redis当前实例的内存数据结构的状态。
  Redis会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到AOF日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已经存储到AOF日志的指令进行重放一下就可以恢复到宕机前的状态。
  Redis在长期运行的过程中,AOF的日志会越变越长。如果实例宕机重启,重新读取整个AOF日志会非常耗时,导致长时间Redis无法对外提供服务。所以需要对AOF日志瘦身。
  默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过appendfsync参数设置同步的时机。

appendfsync always //每次写入aof文件都会执行同步,最安全最慢,不建议配置
appendfsync everysec //既保证性能也保证安全,建议配置
appendfsync no //由操作系统决定何时进行同步操作
3.2.2 AOF的工作流程*

  AOF的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。

  1. 所有的写入命令会追加到aof_buf(缓冲区)中。
  2. AOF缓冲区根据对应的策略向硬盘做同步操作。
  3. 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。AOF文件重
    写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
  4. 当Redis服务器重启时,可以加载AOF文件进行数据恢复。
  • 步骤1、命令写入
      AOF命令写入的内容直接是文本格式。如set hello world这条命令,在AOF缓冲区会追加如下文本:*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
      AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。
  • 步骤2、文件同步
      Redis提供了多种AOF缓冲区同步文件策略,由Redis的配置文件(redis.conf,在Windows系统上是redis.windows.conf)中的参数appendfsync控制(其实就是AOF持久化的三种策略):
可配置值说明
always命令写入aof_buf后调用系统fsync操作同步到AOF文件,fsync完成后线程返回
everysec命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由专门的线程每秒调用一次
no命令写入aof_bf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒

  系统调用write和fsync说明:
  write操作会触发延迟写(delayed write)机制。Linux在内核提供页缓冲区用来提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
  fsync针对单个文件操作(比如AOF文件),做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化。

  这三种同步方式特点:
  1)appendfsync always

  可以实现将数据丢失减到最少,不过这种方式需要对硬盘进行大量的写入而且每次只写入一个命令,十分影响Redis的速度。另外使用固态硬盘的用户谨慎使用appendfsync always选项,因为这会明显降低固态硬盘的使用寿命。

  2)appendfsync everysec

  默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失1秒的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

  3)appendfsync no

  一般不推荐,这种方案会使Redis丢失不定量的数据而且如果用户的硬盘处理写入操作的速度不够的话,那么当缓冲区被等待写入的数据填满时,Redis的写入操作将被阻塞,这会导致Redis的请求速度变慢。

  • 步骤3:重写:
      随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
      重写后的AOF文件可以变小的原因:
  1. 进程内已经超时的数据不再写入文件。
  2. 旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
  3. 多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。

  AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF文件可以更快地被Redis加载。
  AOF重写过程可以手动触发和自动触发:

  1. 手动触发:直接调用bgrewriteaof命令。
  2. 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。

  BGREWRITEAOF命令会通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件来减小AOF文件的体积。
  auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
  auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。
  自动触发时机:

	aof_current_size > auto-aof-rewrite-min-size
		&&(aof_current_size-aof_base_size)/aof_base_size >= auto-aof-rewrite-percentage

  具体例子,假如上面两个属性为以下配置:

auto-aof-rewrite-percentage 100  
auto-aof-rewrite-min-size 64mb

  那么当AOF文件体积大于64mb,并且AOF的体积比上一次重写之后的体积大了至少一倍(100%)的时候,Redis将执行BGREWRITEAOF命令。

  AOF重写运作流程:

1)执行AOF重写请求。
  如果当前进程正在执行AOF重写,请求不执行。
  如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行。
2)父进程执行fork创建子进程,开销等同于bgsave过程。
 3)主进程fork操作完成后,继续响应其他命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步到硬盘,保证原有AOF机制正确性。由于fork操作运用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部分新数据,防止新AOF文件生成期间丢失这部分数据。
4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞。
 5)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息。父进程把AOF重写缓冲区的数据写入到新的AOF文件。使用新AOF文件替换老文件,完成AOF重写。

  • 步骤4:重启加载:
      AOF和RDB文件都可以用于服务器重启时的数据恢复,恢复流程:

      如果加载的AOF文件损坏时会拒绝启动,日志:

      对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof–fix命令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可以人工修改补全。
      AOF文件可能存在结尾不完整的情况,比如机器突然掉电导致AOF尾部文件命令写入不全。Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启。
3.2.3 【AOF的优(数据丢失少)缺(占用空间大/恢复速度慢)点】
  • AOF的优点
      1、AOF可以更好的保护数据不丢失,可以配置AOF每秒执行一次fsync操作,如果Redis进程挂掉,最多丢失1秒的数据。
      2、AOF以 append-only 的模式写入,所以没有磁盘寻址的开销,写入性能非常高。而且文件不容易破损,即使文件尾部破损,也很容易修复。
      3、AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件ready 的时候,再交换新老日志文件即可。
      4、AOF 日志文件的命令通过可读较强的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台rewrite 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。
  • AOF的缺点
      1、AOF 文件比 RDB 文件大,且恢复速度慢
      2、数据集大的时候,比 rdb 启动效率低
      3、AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒fsync 一次日志文件,当然,每秒一次 fsync ,性能也还是很高的。(如果实时写入,那么 QPS 会大降,Redis 性能会大大降低)

3.3 Redis持久化的相关问题

3.3.1 AOF和RDB对比*
RDBAOF
启动优先级
体积
恢复速度
数据安全性丢数据根据策略决定
3.3.2 【Redis4.0对持久化机制的优化】

  Redis4.0开始支持RDB和AOF的混合持久化,该功能通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,默认是禁用的,可通过config set修改。
  如果把混合持久化打开,AOF重写的时候就直接把RDB的内容写到AOF文件开头。这样做的好处是可以结合RDB和AOF的优点, 快速加载同时避免丢失过多的数据。

  重启Redis时,我们很少使用rdb来恢复内存状态,因为会丢失大量数据。通常使用AOF日志重放,但是重放AOF日志性能相对rdb来说要慢很多,这样在Redis实例很大的情况下,启动需要花费很长的时间。
  Redis 4.0为了解决这个问题,带来了一个新的持久化选项——混合持久化。将rdb文件的内容和增量的AOF日志文件存在一起。这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF日志,通常这部分AOF日志很小。

  于是在Redis重启的时候,可以先加载rdb的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重启效率因此大幅得到提升。

3.3.3 【如何选择合适的持久化方式(同时使用两种持久化方案,优先使用AOF恢复数据)】

  通常来说,应该同时使用两种持久化方案,以保证数据安全。

  • 如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化。
  • 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可。
  • 如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启。
  • 如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡。

  当RDB与AOF两种方式都开启时,Redis会优先使用AOF恢复数据,因为AOF保存的文件比RDB文件更完整。

  1、不要仅仅使用 RDB,因为那样会导致你丢失很多数据;
  2、也不要仅仅使用 AOF,因为那样有两个问题:第一,你通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug;
  3、Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。

3.3.4 如何禁用持久化

  一般生产环境不会用到,命令:config set save ""

3.3.5 如何查询AOF是否开启

  命令:config get appendonly

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐