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

一、Redis中的特殊功能

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 事务命令

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

  1. Redis会将一个事务中的所有命令序列化,然后按顺序执行。
  2. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  3. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  4. 如果在一个事务中出现运行错误,那么其他正确的命令会被执行
  • 1、Multi
      Multi用于开启一个事务。 Multi执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被按照先后顺序放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • 2、Exec
      Exec命令用于执行所有事务块内的命令。这些命令按先后顺序排列。 当操作被打断时,返回空值nil 。
      使用示例:
  • 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 事务的生命周期

  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 Pipeline

  Redis客户端执行一条命令分为如下四个过程:发送命令、命令排队、命令执行、返回结果。这四步合起来称为Round Trip Time(RTT,往返时间)。
  使用Pipeline可以批量请求,批量返回结果,执行速度比逐条执行要快

  Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。但大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。
  Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端。
  Redis命令真正执行的时间通常在微秒级别,所以才会有Redis性能瓶颈是网络这样的说法
  redis-cli的–pipe选项实际上就是使用Pipeline机制。
  非Pipeline和Pipeline执行命令的区别:

  1. Pipeline执行速度一般比逐条执行要快。
  2. 客户端和服务端的网络延时越大,Pipeline的效果越明显。

  原生批量命令与Pipeline的区别:

  1. 原生批量命令是原子的,Pipeline是非原子的。pipeline命令中途异常退出,之前执行成功的命令不会回滚。
  2. 原生批量命令是一个命令对应多个key,Pipeline支持多个命令。
  3. 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现。

  Pipeline虽然好用,但是每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成。

1.4 发布订阅与Stream*

1.4.1 Redis中的发布/订阅功能

  发布/ 订阅系统是Web系统中比较常用的一个功能。简单点说就是发布者发布消息,订阅者接受消息,这有点类似于我们的报纸/ 杂志社。
  虽然可以使用一个list 列表结构结合lpush和rpop来实现消息队列的功能,但是似乎很难实现实现消息多播的功能:

  为了支持消息多播,Redis不能再依赖于那5种基础的数据结构了,它单独使用了一个模块来支持消息多播,这个模块就是PubSub,也就是PublisherSubscriber (发布者/ 订阅者模式)。

  当Publisher往channel中发布消息时,关注了指定channel的Consumer就能够同时受到消息。这里的问题是,消费者订阅一个频道是必须明确指定频道名称的。这意味着,如果我们想要订阅多个 频道,那么就必须显式地关注多个 名称。
  为了简化订阅的繁琐操作,Redis 提供了模式订阅的功能Pattern Subscribe,这样就可以一次性关注多个频道 了,即使生产者新增了同模式的频道,消费者也可以立即受到消息:
  上图中,所有 位于图片下方的Consumer都能够受到消息。
  Publisher往wmyskxz.chat这个channel中发送了一条消息,不仅仅关注了这个频道的Consumer 1和Consumer 2能够受到消息,图片中的两个channel都和模式wmyskxz.* 匹配,所以Redis此时会同样发送消息给订阅了wmyskxz.* 这个模式的Consumer 3和关注了在这个模式下的另一个频道wmyskxz.log下的Consumer 4和Consumer 5 。
  另一方面,如果接收消息的频道是wmyskxz.chat ,那么Consumer 3也会受到消息。

  • PubSub 的缺点
      尽管Redis实现了PubSub模式来达到了多播消息队列的目的,但在实际的消息队列的领域,几乎找不到特别合适的场景,因为它的缺点十分明显:
  1. 没有Ack机制,也不保证数据的连续: PubSub的生产者传递过来一个消息,Redis会直接找到相应的消费者传递过去。如果没有一个消费者,那么消息会被直接丢弃。如果开始有三个消费者,其中一个突然挂掉了,过了一会儿等它再重连时,那么重连期间的消息对于这个消费者来说就彻底丢失了。
  2. 不持久化消息: 如果Redis停机重启,PubSub的消息是不会持久化的,毕竟Redis宕机就相当于一个消费者都没有,所有的消息都会被直接丢弃。
1.4.2 Stream(持久化的发布/订阅系统)

  Redis Stream从概念上来说,就像是一个仅追加内容的消息链表,把所有加入的消息都一个一个串起来,每个消息都有一个唯一的ID和内容,这很简单。让它复杂的是从Kafka借鉴的另一种概念:消费者组(Consumer Group) (思路一致,实现不同):

  上图就展示了一个典型的Stream结构。每个Stream都有唯一的名称,它就是Redis的key,在我们首次使用xadd指令追加消息时自动创建。
  Consumer Group:消费者组,可以简单看成记录流状态的一种数据结构。消费者既可以选择使用XREAD命令进行独立消费,也可以多个消费者同时加入一个消费者组进行 组内消费。同一个消费者组内的消费者共享所有的Stream信息,同一条消息只会有一个消费者消费到,这样就可以应用在分布式的应用场景中来保证消息的唯一性。
  last_delivered_id:用来表示消费者组消费在 Stream 上 消费位置 的游标信息。每个消费者组都有一个Stream内唯一的名称,消费者组不会自动创建,需要使用 XGROUP CREATE指令来显式创建,并且需要指定从哪一个消息ID开始消费,用来初始化last_delivered_id这个变量。
  pending_ids:每个消费者内部都有的一个状态变量,用来表示已经被客户端 获取,但是还没有ack的消息。记录的目的是为了 保证客户端至少消费了消息一次,而不会在网络传输的中途丢失而没有对消息进行处理。如果客户端没有ack,那么这个变量里面的消息ID就会越来越多,一旦某个消息被ack,它就会对应开始减少。这个变量也被Redis官方称为PEL (Pending Entries List)。

二、Redis客户端

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

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

2.1 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中对键通用的操作
方法描述返回值 /补充说明
boolean exists(String key)判断某个键是否存在
set(String key,String value)新增键值对(key,value)返回String类型的OK代表成功
Set< String > jedis.keys(*)获取所有key返回set 无序集合
del(String key)删除指定key
expire(String key,int i)设置键为key的过期时间为i秒
int jedis.ttl(String key)获取key数据项的剩余时间(秒)
persist(String key)移除键为key属性项的生存时间限制
type(String key)查看键为key所对应value的数据类型
  • 2、Jedis中的字符串操作
      字符串类型是Redis中最为基础的数据存储类型,在Redis中字符串类型的Value最多可以容纳的数据长度是512M。
语法描述
set(String key,String value)增加(或覆盖)数据项
setnx(String key,String value)不覆盖增加数据项(重复的不插入)
setex(String ,int t,String value)增加数据项并设置有效时间
del(String key)删除键为key的数据项
get(String key)获取键为key对应的value
append(String key, String s)在key对应value 后边追加字符串 s
mset(String k1,String V1,String K2,String V2,…)增加多个键值对
String[] mget(String K1,String K2,…)获取多个key对应的value
del(new String[](String K1,String K2,.... ))删除多个key对应的数据项
String getSet(String key,String value)获取key对应value并更新value
String getrange(String key , int i, int j)获取key对应value第i到j字符 ,从0开始,包头包尾
  • 3、Jedis中的增减操作
语法描述
incr(String key)将key对应的value加1
incrBy(String key,int n)将key对应的value加n
decr(String key)将key对应的value减1
decrBy(String key , int n)将key对应的value减n
  • 4、Jedis中的列表操作
语法描述
lpush(String key, String v1, String v2,....)添加一个List , 如果已经有该List对应的key, 则按顺序在左边追加 一个或多个
rpush(String key , String vn)key对应list右边插入元素
lrange(String key,int i,int j)获取key对应list区间[i,j]的元素,注:从左边0开始,包头包尾
ltrim(String key,int i,int j)删除list区间[i,j] 之外的元素
lpop(String key)左弹出一个key对应的元素
rpop(String key)右弹出一个key对应的元素
llen(String key)获取key对应list的长度
lset(String key,int index,String val)修改key对应的list指定下标index的元素
lindex(String key,int index)获取key对应list下标为index的元素
  • 5、Jedis中的集合操作
语法描述
sadd(String key,String v1,String v2,…)添加一个set
smenbers(String key)获取key对应set的所有元素
srem(String key,String val)删除集合key中值为val的元素
srem(String key, Sting v1, String v2,…)删除值为v1, v2 , …的元素
sinter(String key1, String key2)获取集合key1和集合key2的交集
sunion(String key1, String key2)获取集合key1和集合key2的并集
sdiff(String key1, String key2)获取集合key1和集合key2的差集
  • 6、Jedis中的有序集合操作
语法描述
zadd(String key,Map map)添加一个ZSet
hset(String key,int score , int val)往 ZSet插入一个元素(Score-Val)
zrange(String key, int i , int j)获取ZSet 里下表[i,j] 区间元素Val
zscore(String key,String value)获取ZSet里value元素的Score
zrem(String key,String value)删除ZSet里的value元素
zcard(String key)获取ZSet的元素个数
zcount(String key , int i ,int j)获取ZSet总score在[i,j]区间的元素个数
zincrby(String key,int n , String value)把ZSet中value元素的score+=n
  • 7、Jedis中的哈希操作
语法描述
hmset(String key,Map map)添加一个Hash
hset(String key , String key, String value)向Hash中插入一个元素(K-V)
hgetAll(String key)获取Hash的所有(K-V) 元素
hkeys(String key)获取Hash所有元素的key
hvals(String key)获取Hash所有元素的value
hdel(String key , String k1, String k2,…)从Hash中删除一个或多个元素
hlen(String key)获取Hash中元素的个数
hexists(String key,String K1)判断Hash中是否存在指定key对应的元素
hmget(String key,String K1,String K2)获取Hash中一个或多个元素value
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 client list

  client list命令能列出与Redis服务端相连的所有客户端连接信息,示例:

  输出结果的每一行代表一个客户端的信息。

  • 1、id
      客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。
  • 2、addr
      客户端连接的ip和端口。
  • 3、fd
      socket的文件描述符。
  • 4、name
      客户端的名字。
  • 5、qbuf、qbuf-free
      这两个属性都表述输入缓冲区相关的信息。
      Redis服务端为每个Redis客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时服务端从会输入缓冲区拉取命令并执行。图示:

      qbuf代表这个缓冲区的总容量,qbuf-free代表这个缓冲区的剩余容量。Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭
      对于Redis服务端而言,假设一个Redis实例设置了maxmemory(Redis服务端的最大内存)为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况。
      查看Redis服务端内存配置的命令是info memory,示例:

      造成输入缓冲区过大的原因有哪些?
  1. 主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey。
  2. Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区。

  监控输入缓冲区异常的方法有两种:

  1. 通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。
  2. 通过info clients命令,找到最大的输入缓冲区,下面命令中的其中client_biggest_input_buf代表最大的输入缓冲区,例如可以设置超过10M就进行报警:

      client list和info clients的对比:
命令优点缺点
client list能精准分析每个客户端来定位问题执行速度较慢,频繁执行存在阻塞Redis的可能
info clients执行速度比client list快,分析过程较为简单不能精确定位到客户端;
不能显示所有输入缓冲区的总量,只能显示最大量
  • 6、obl、oll、omem
      这三个属性都表述输出缓冲区相关的信息
      Redis服务端为每个Redis客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为服务端和客户端交互返回结果提供缓冲,图示:

      输出缓冲区的容量可以通过参数client-output-buffer-limit来进行设置,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端,图示:

      client-output-buffer-limit命令的使用:client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>。参数:

1、<class>:客户端类型,分为三种。
  normal:普通客户端;
  slave:slave客户端,用于复制;
  pubsub:发布订阅客户端。
2、<hard limit>:如果客户端使用的输出缓冲区大于<hard limit>,客户端会被立即关闭。
3、<soft limit><soft seconds>:如果客户端使用的输出缓冲区超过了<soft limit>并且持续了<soft seconds>秒,客户端会被立即关闭。

  client-output-buffer-limit的默认配置:

  输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果,例如大的字符串、hgetall、smembers命令的结果等。
  固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。
  client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。例如下面代表当前客户端的固定缓冲区的长度为0,动态缓冲区有4869个对象,两个部分共使用了133081288字节=126M内存:

  监控输出缓冲区的方法依然有两种:

  1. 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
  2. 通过info命令的info clients模块,找到输出缓冲区列表最大对象数。示例:

      client_longest_output_list代表输出缓冲区列表最大对象数
      如何预防输出缓冲区出现异常呢?主要方法有以下几种:

  1、监控并设置阀值,超过阀值及时处理。
  2、根据client-output-buffer-limit命令对普通缓冲区设置,示例:client-output-buffer-limit normal 20mb 10mb 120
  3、及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。

  • 7、age和idle
      age代表当前客户端已经连接的时间,idle代表当前客户端最近一次的空闲时间。示例:
      上面这条记录代表当期客户端连接Redis的时间为8888581秒,其中空闲了8888581秒,实际上这种就属于不太正常的情况,当age等于idle时,说明连接一直处于空闲状态
  • 8、和maxclients/timeout配合使用
      Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数:

      可以通过config set maxclients对最大客户端连接数进行动态设置:

      一般来说maxclients=10000在大部分场景下已经绝对够用。同时,Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭。Redis 默认的 timeout 是 0 ,也就是不会检测客户端的空闲
      将timeout设置为30秒示例:

      在实际开发和运维中,需要将timeout设置成大于0,例如可以设置为300秒,这样可以避免Redis的客户端使用不当或者客户端本身的一些问题,造成没有及时释放客户端连接的问题。
  • 9、flags
      flags是用于标识当前客户端的类型,例如flags=S代表当前客户端是slave客户端、flags=N代表当前是普通客户端。客户端类型:
客户端类型说明
N普通客户端
Mmaster节点
Sslave节点
o正在执行monitor命令
x正在执行事务
b正在等待阻塞时间
u客户端未被阻塞
A尽可能快地关闭连接
  • 10、client list所有参数
参数含义
id客户端连接id
addr客户端连接IP和端口
fdsocket的文件描述符
name客户端连接名
age客户端连接存活时间
idle客户端连接空闲时间
flags客户端连接标识
db当前客户端正在使用的数据库索引下标
sub/psub当前客户端订阅的频道数或模式数
multi当前事务中已执行命令个数
qbuf输入缓冲区总容量
qbuf-ree输入缓冲区剩余容量
obl固定缓冲区的长度
oll动态缓冲区列表的长度
omem固定缓冲区和动态缓存区使用的容量
cmd当前客户端最后一次执行的命令
2.2.2 client setName和client getName

  用于给当前客户端设置和获取名称,示例:

2.2.3 client kill

  用法为client kill ip:port,此命令用于杀掉指定IP地址和端口的客户端。

2.2.4 client pause

  用法为’client pause timeout’,表示阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。

  1. client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,所以此命令可以用来让主从复制保持一致。
  2. client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。
2.2.5 monitor

  monitor命令用于监控Redis正在执行的命令。示例:

2.2.6 config set

  该命令用于设置客户端属性。

  • 1、timeout
      检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。示例:
  • 2、maxclients
      客户端最大连接数。示例:
  • 3、tcp-keepalive
      检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。示例:
  • 4、tcp-backlog
      TCP三次握手后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不需要调整,示例:
2.2.7 info stats

   info stats可以统计一些Redis总的状态,示例:

   total_connections_received表示Redis自启动以来处理的客户端连接数总
数。
  rejected_connections表示Redis自启动以来拒绝的客户端连接数。

2.3 常见异常

2.3.1 无法从连接池获取到连接

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

该属性的作用是:连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true。

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

1、高并发下连接池设置过小。
2、没有正确使用连接池,比如没有进行释放。
3、存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢。
4、客户端是正常的,但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞。

2.3.2 客户端读写超时

  SocketTimeoutException,原因有以下几种:

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

2.3.3 客户端连接超时

  JedisConnectionException,原因有以下几种:

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

2.3.4 客户端缓冲区异常

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

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

2.3.5 JedisDataException

  Jedis调用Redis时,如果Redis正在加载持久化文件,会出现JedisDataException

2.3.6 Redis使用的内存超过maxmemory配置

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

2.3.7 客户端连接数过大

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

2.4 Redis客户端的相关问题

2.4.1 Redis和Redisson有什么关系

  Redisson是一个高级的分布式协调Redis客户端,能帮助用户在分布式环境中轻松实现一些Java的对象,如:Bloom filter、BitSet、Set、SortedSet、Map、ConcurrentMap、List、Queue、BlockingQueue、Semaphore、ReadWriteLock、AtomicLong、CountDownLatch等。

2.4.2 Jedis和Redisson对比有什么优缺点

  Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。
  Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,让使用者将精力更集中地放在处理业务逻辑上。

三、持久化

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

  Redis支持两种方式的持久化,一种是 RDB 的方式,一种是 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触发机制*

  总的来说,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

  在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的持久化方式会丢失部分更新数据

3.1.2 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.3 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无法实现实时或者秒级持久化
      4)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 如何选择合适的持久化方式*

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

  • 如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化。
  • 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用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

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐