文章目录
- 一、Redis中的辅助功能
- 1.1 慢查询
- 1.1.1 慢查询相关的两个参数
- 1.1.2 慢查询使用建议
- 1.2 Redis Shell
- 1.2.1 redis-cli
- 1.2.2 redis-benchmark
- 1.3 Pipeline
- 1.4 事务
- 1.4.1 事务命令
- 1.4.2 Redis事务保证原子性吗,支持回滚吗
- 1.5 Lua
- 1.5.1 使用Lua脚本
- 1.5.2 管理Lua脚本
- 二、Redis客户端
- 2.1 Jedis的使用
- 2.1.1 Jedis的简单使用
- 2.1.2 Jedis常用API
- 2.1.3 Jedis连接池
- 2.2 客户端管理
- 2.2.1 client list
- 2.2.2 client setName和client getName
- 2.2.3 client kill
- 2.2.4 client pause
- 2.2.5 monitor
- 2.2.6 config set
- 2.2.7 info stats
- 2.3 常见异常
- 2.3.1 无法从连接池获取到连接
- 2.3.2 客户端读写超时
- 2.3.3 客户端连接超时
- 2.3.4 客户端缓冲区异常
- 2.3.5 JedisDataException
- 2.3.6 Redis使用的内存超过maxmemory配置
- 2.3.7 客户端连接数过大
- 2.4 Redis客户端的一些问题
- 2.4.1 Redis和Redisson有什么关系?
- 2.4.2 Jedis和Redisson对比有什么优缺点?
一、Redis中的辅助功能
1.1 慢查询
Redis客户端执行一条命令分为如下4个部分:
慢查询只统计命令执行
的时间。
1.1.1 慢查询相关的两个参数
- slowlog-log-slower-than
slowlog-log-slower-than
是预设阀值,单位是微秒,默认值是10000,假如执行了一条“很慢”的命令(例如keys*),如果它的执行时间超过了10000微秒,那么它将被记录在慢查询日志中。
如果slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slower-than<0对于任何命令都不会进行记录。
- slowlog-max-len
Redis使用了一个列表来存储慢查询日志,slowlog-max-len
就是列表的最大长度。一个新的命令满足慢查询条件时被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出,例如slowlog-max-len设置为5,当有第6条慢查询插入的话,那么队头的第一条数据就出列,第6条慢查询就会入列。
Redis中有两种修改配置的方法,一种是修改配置文件,另一种是用config set命令动态修改,示例:
config set slowlog-log-slower-than 20000
config set slowlog-max-len 1000
config rewrite
从上面可以看出慢查询日志是存放在Redis内存列表中的,但是Redis并没有暴露这个列表的键,而是通过一组命令来实现对慢查询日志的访问和管理。慢查询相关命令:
- 1、获取慢查询日志
slowlog get [n]
每个慢查询日志有4个属性组成,分别是慢查询日志的标识id、发生时间戳、命令耗时、执行命令和参数。
- 2、获取慢查询日志列表当前的长度
slowlog len
- 3、清空慢查询日志列表
slowlog reset
1.1.2 慢查询使用建议
slowlog-max-len配置建议:线上建议调大慢查询列表,记录慢查询时Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上。
slowlog-log-slower-than配置建议:默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到1000。因此对于高OPS场景的Redis建议设置为1毫秒。
由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slow get命令将慢查询日志持久化到其他存储中。
1.2 Redis Shell
1.2.1 redis-cli
可以执行redis-cli-help命令来查看该命令的全部参数。
- 1、-r
-r(repeat)选项代表将命令执行多次,示例:
redis-cli -r 3 ping
- 2、-i
-i(interval)选项代表每隔几秒执行一次命令,但是-i选项必须和-r选项一起使用。每隔1秒执行一次ping命令,一共执行5次示例:
redis-cli -r 5 -i 1 ping
- 3、-x
-x选项代表从标准输入(stdin)读取数据作为redis-cli的最后一个参数,将字符串world作为set hello的值示例:
echo "world" | redis-cli -x set hello
- 4、-a
如果Redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要手动输入auth命令。 - 5、–slave
--slave选项是把当前客户端模拟成当前Redis节点的从节点,可以用来获取当前Redis节点的更新操作。 - 6、–rdb
--rdb选项会请求Redis实例生成并发送RDB持久化文件,保存在本地。可用来做持久化文件的定期备份。 - 7、–eval
用于执行指定Lua脚本。 - 8、–latency
latency有三个选项,分别是–latency、–latency-history、–latency-dist,它们都可以检测网络延迟。
--latency
可以测试不同客户端到目标Redis示例的网络延迟。比如客户端B和Redis在机房B,客户端A在机房A,则机房A和机房B是跨地区的:
--latency的执行结果只有一条,如果想以分时段的形式了解延迟信息,可以使用--latency-history
选项,示例:
redis-cli -h 10.10.xx.xx --latency-history
min: 0, max: 1, avg: 0.28 (1330 samples) -- 15.01 seconds range …
min: 0, max: 1, avg: 0.05 (1364 samples) -- 15.01 seconds range
延时信息每15秒输出一次,可以通过-i参数控制间隔时间。
--latency-dist
会使用统计图表的形式从控制台输出延迟统计信息。
- 9、–stat
--stat选项可以实时获取Redis的重要统计信息,示例:
1.2.2 redis-benchmark
redis-benchmark可以为Redis做基准性能测试。
- 1、-c
-c(clients)表示客户端的并发数量(默认是50)。 - 2、-n
-n(num)表示客户端请求总量(默认是100000)。 - 3、-q
-q仅仅显示redis-benchmark的吞吐量信息,示例:
redis-benchmark -c 100 -n 20000 -q
PING_INLINE: 74349.45 requests per second
PING_BULK: 68728.52 requests per second
SET: 71174.38 requests per second …
LRANGE_500 (first 450 elements): 11299.44 requests per second
LRANGE_600 (first 600 elements): 9319.67 requests per second
MSET (10 keys): 70671.38 requests per second
- 4、-r
-r(random)可以向Redis插入更多随机的键。示例:
redis-benchmark -c 100 -n 20000 -r 10000
-r选项会在key、counter键上加一个12位的后缀,-r10000代表只对后四位做随机处理。
- 5、-P
-P表示每个请求pipeline的数据量(默认为1)。 - 6、-k
-k表示客户端是否使用keepalive,1为使用,0为不使用,默认值为1。 - 7、-t
-t可以对指定命令进行基准测试。示例:
redis-benchmark -t get,set -q
SET: 98619.32 requests per second
GET: 97560.98 requests per second
1.3 Pipeline
Redis客户端执行一条命令分为如下四个过程:发送命令、命令排队、命令执行、返回结果。这四步合起来称为Round Trip Time(RTT,往返时间)。
Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。但大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。
Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端。
Redis命令真正执行的时间通常在微秒级别,所以才会有Redis性能瓶颈是网络这样的说法
。
redis-cli的–pipe选项实际上就是使用Pipeline机制。
非Pipeline和Pipeline执行命令的区别:
- Pipeline执行速度一般比逐条执行要快。
- 客户端和服务端的网络延时越大,Pipeline的效果越明显。
原生批量命令与Pipeline的区别:
- 原生批量命令是原子的,Pipeline是非原子的。
- 原生批量命令是一个命令对应多个key,Pipeline支持多个命令。
- 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现。
Pipeline虽然好用,但是每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成。
1.4 事务
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合
。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
。
在 Redis 中,事务具有一致性和隔离性,并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性。但是,Redis中的事务不具有一致性,可能部分命令会执行成功,而另一部分命令执行失败。
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:开始事务、命令入队、执行事务。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
1.4.1 事务命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH实现的。事务功能有以下特点:
- Redis会将一个事务中的所有命令序列化,然后按顺序执行。
redis 不支持回滚
,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令
”, 所以 Redis 的内部可以保持简单且快速。如果在一个事务中的命令出现错误,那么所有的命令都不会执行
;如果在一个事务中出现运行错误,那么正确的命令会被执行
。
- 1、Multi
Multi用于开启一个事务
。 Multi执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被按照先后顺序放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 - 2、Exec
Exec命令用于执行所有事务块内的命令
。这些命令按先后顺序排列。 当操作被打断时,返回空值 nil 。
前两个命令的使用示例: - 3、Discard
discard 命令用于取消事务(清空事务队列)
,放弃执行事务块内的所有命令。示例: - 4、Watch
Watch命令是一个乐观锁,可以为 Redis 事务提供(CAS)行为。其功能是:可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令
。示例: - 5、Unwatch
Unwatch命令用于取消WATCH命令对所有key的监视
。示例:
1.4.2 Redis事务保证原子性吗,支持回滚吗
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行
。
如果想实现回滚,就需要用WATCH命令
,具体的做法是:
需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。
1.5 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.5.1 使用Lua脚本
- 1、eval和evalsha
在Redis中执行Lua脚本有两种方法:eval和evalsha。
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.5.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将不会生效。
二、Redis客户端
几乎所有的主流编程语言都有Redis的客户端,Redis的客户端的特点:
- 客户端与服务端之间的通信协议是在TCP协议之上构建的。
- 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>
对于第三方开发包,版本的选择也是至关重要的,通常来讲选取第三方开发包有如下两个策略:
- 选择比较稳定的版本,也就是尽可能选择稳定的里程碑版本。
- 选择更新活跃的第三方开发包,例如Redis3.0有了Redis Cluster新特性。
- 2、Jedis的使用
就可以查看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中对键通用的操作
方法 | 描述 | 返回值 /补充说明 |
| 判断某个键是否存在 | |
| 新增键值对(key,value) | 返回String类型的OK代表成功 |
| 获取所有key | 返回set 无序集合 |
| 删除指定key | |
| 设置键为key的过期时间为i秒 | |
| 获取key数据项的剩余时间(秒) | |
| 移除键为key属性项的生存时间限制 | |
type(String key) | 查看键为key所对应value的数据类型 |
- 2、Jedis中的字符串操作
字符串类型是Redis中最为基础的数据存储类型,在Redis中字符串类型的Value最多可以容纳的数据长度是512M。
语法 | 描述 |
| 增加(或覆盖)数据项 |
| 不覆盖增加数据项(重复的不插入) |
| 增加数据项并设置有效时间 |
| 删除键为key的数据项 |
| 获取键为key对应的value |
append(String key, String s) | 在key对应value 后边追加字符串 s |
| 增加多个键值对 |
| 获取多个key对应的value |
| 删除多个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中的列表操作
语法 | 描述 |
| 添加一个List , 如果已经有该List对应的key, 则按顺序在左边追加 一个或多个 |
| key对应list右边插入元素 |
lrange(String key,int i,int j) | 获取key对应list区间[i,j]的元素,注:从左边0开始,包头包尾 |
ltrim(String key,int i,int j) | 删除list区间[i,j] 之外的元素 |
| 左弹出一个key对应的元素 |
| 右弹出一个key对应的元素 |
| 获取key对应list的长度 |
| 修改key对应的list指定下标index的元素 |
lindex(String key,int index) | 获取key对应list下标为index的元素 |
- 5、Jedis中的集合操作
语法 | 描述 |
| 添加一个set |
| 获取key对应set的所有元素 |
srem(String key,String val) | 删除集合key中值为val的元素 |
srem(String key, Sting v1, String v2,…) | 删除值为v1, v2 , …的元素 |
| 获取集合key1和集合key2的交集 |
| 获取集合key1和集合key2的并集 |
| 获取集合key1和集合key2的差集 |
- 6、Jedis中的有序集合操作
语法 | 描述 |
| 添加一个ZSet |
| 往 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中的哈希操作
语法 | 描述 |
| 添加一个Hash |
| 向Hash中插入一个元素(K-V) |
| 获取Hash的所有(K-V) 元素 |
| 获取Hash所有元素的key |
| 获取Hash所有元素的value |
| 从Hash中删除一个或多个元素 |
| 获取Hash中元素的个数 |
| 判断Hash中是否存在指定key对应的元素 |
| 获取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
,示例:
造成输入缓冲区过大的原因有哪些?
- 主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey。
- Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区。
监控输入缓冲区异常的方法有两种:
- 通过定期执行
client list
命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。 - 通过
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内存:
监控输出缓冲区的方法依然有两种:
- 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
- 通过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 | 普通客户端 |
M | master节点 |
S | slave节点 |
o | 正在执行monitor命令 |
x | 正在执行事务 |
b | 正在等待阻塞时间 |
u | 客户端未被阻塞 |
A | 尽可能快地关闭连接 |
- 10、client list所有参数
参数 | 含义 |
id | 客户端连接id |
addr | 客户端连接IP和端口 |
fd | socket的文件描述符 |
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毫秒数,在此期间客户端连接将被阻塞。
-
client pause
只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,所以此命令可以用来让主从复制保持一致。 -
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实现的客户端,其AP提供了比较全面的Redis命令的支持。
Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,让使用者将精力更集中地放在处理业务逻辑上。