《Redis开发与运维》- 核心知识整理二(Lua脚本、发布订阅、客户端等)
目录
- 1 事物
- 2 Lua脚本
- 2.1 Lua脚本的好处
- 2.2 Lua脚本的使用
- 2.3 script kill
- 3 Bitmaps
- 3.1 数据结构模型
- 3.2 Bitmaps的指令
- 3.3 Bitmaps分析
- 4 发布订阅
- 4.1 基本概念
- 4.2 命令
- 4.3 使用场景
- 5 客户端通信协议
- 6 Java客户端Jedis
- 6.1 Jedis的基本使用方法
- 6.2 Jedis连接池的使用方法
- 7 客户端API
- 7.1 client list
- 7.2 monitor
- 7.3 客户端相关配置
1 事物
Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的,例如下面操作实现了上述用户关注问题。
127.0.0.1:6379> multi OK 127.0.0.1:6379> sadd user:a:follow user:b QUEUED 127.0.0.1:6379> sadd user:b:fans user:a QUEUED可以看到sadd命令此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中。如果此时另一个客户端执行sismember user:a:follow user:b返回结果应该为0。
127.0.0.1:6379> sismember user:a:follow user:b (integer) 0只有当exec执行后,用户A关注用户B的行为才算完成,如下所示返回的两个结果对应sadd命令。
127.0.0.1:6379> exec 1) (integer) 1 2) (integer) 1 127.0.0.1:6379> sismember user:a:follow user:b (integer) 1如果要停止事务的执行,可以使用discard命令代替exec命令即可。
127.0.0.1:6379> discard OK 127.0.0.1:6379> sismember user:a:follow user:b (integer) 0如果事务中的命令出现错误,Redis的处理机制也不尽相同。
1.命令错误
例如下面操作错将set写成了sett,属于语法错误,会造成整个事务无法执行,key和counter的值未发生变化:
127.0.0.1:6388> mget key counter 1) "hello" 2) "100" 127.0.0.1:6388> multi OK 127.0.0.1:6388> sett key world (error) ERR unknown command 'sett' 127.0.0.1:6388> incr counter QUEUED 127.0.0.1:6388> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6388> mget key counter 1) "hello" 2) "100"2.运行时错误
例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的:
127.0.0.1:6379> multi OK 127.0.0.1:6379> sadd user:a:follow user:b QUEUED 127.0.0.1:6379> zadd user:b:fans 1 user:a QUEUED 127.0.0.1:6379> exec 1) (integer) 1 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> sismember user:a:follow user:b (integer) 1可以看到Redis并不支持回滚功能,sadd user:a:follow user:b命令已经执行成功,开发人员需要自己修复这类问题。 有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来解决这类问题,下表展示了两个客户端执行命令的时序。
事务中watch命令演示时序
可以看到“客户端-1”在执行multi之前执行了watch命令,“客户端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果为nil),整个代码如下所示:
#T1:客户端1 127.0.0.1:6379> set key "java" OK #T2:客户端1 127.0.0.1:6379> watch key OK #T3:客户端1 127.0.0.1:6379> multi OK #T4:客户端2 127.0.0.1:6379> append key python (integer) 11 #T5:客户端1 127.0.0.1:6379> append key jedis QUEUED #T6:客户端1 127.0.0.1:6379> exec (nil) #T7:客户端1 127.0.0.1:6379> get key "javapython"Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了Redis的“keep it simple”的特性,Lua脚本同样可以实现事务的相关功能,但是功能要强大很多。
2 Lua脚本
2.1 Lua脚本的好处
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
·Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
·Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
·Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
2.2 Lua脚本的使用
下面以一个例子说明Lua脚本的使用,当前列表记录着热门用户的id,假设这个列表有5个元素,如下所示:
127.0.0.1:6379> lrange hot:user:list 0 -1 1) "user:1:ratio" 2) "user:8:ratio" 3) "user:3:ratio" 4) "user:99:ratio" 5) "user:72:ratio"user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio 1) "986" 2) "762" 3) "556" 4) "400" 5) "101"现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行,此功能可以利用Lua脚本来实现。
1)将列表中所有元素取出,赋值给mylist:
local mylist = redis.call("lrange", KEYS[1], 0, -1)2)定义局部变量count=0,这个count就是最后incr的总次数:
local count = 03)遍历mylist中所有元素,每次做完count自增,最后返回count:
for index,key in ipairs(mylist) do redis.call("incr",key) count = count + 1 end return count将上述脚本写入lrange_and_mincr.lua文件中,并执行如下操作,返回结果为5。
redis-cli --eval lrange_and_mincr.lua hot:user:list (integer) 5执行后所有用户的热度自增1:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio 1) "987" 2) "763" 3) "557" 4) "401" 5) "102"本节给出的只是一个简单的例子,在实际开发中,开发人员可以发挥自己的想象力创造出更多新的命令。
2.3 script kill
此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。下面我们模拟一个Lua脚本阻塞的情况进行说明。下面的代码会使Lua进入死循环:
while 1 == 1 do end执行Lua脚本,当前客户端会阻塞:
127.0.0.1:6379> eval 'while 1==1 do end' 0Redis提供了一个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的脚本:
127.0.0.1:6379> get hello (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待,但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选择script kill,当script kill执行之后,客户端调用会恢复:
127.0.0.1:6379> script kill OK 127.0.0.1:6379> get hello "world"但是有一点需要注意,如果当前Lua脚本正在执行写操作,那么script kill将不会生效。例如,我们模拟一个不停的写操作:
while 1==1 do redis.call("set","k","v") end此时如果执行script kill,会收到如下异常信息:
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.上面提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么使用shutdown save停掉Redis服务。可见Lua脚本虽然好用,但是使用不当破坏性也是难以想象的。
3 Bitmaps
3.1 数据结构模型
许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。Redis提供了Bitmaps这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:
·Bitmaps本身不是一种数据结构,实际上它就是字符串(如下图所示),但是它可以对字符串的位进行操作。
·Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。
字符串"big"用二进制表示
3.2 Bitmaps的指令
本节将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id。
1.设置值
setbit key offset value设置键的第offset个位的值(从0算起),假设现在有20个用户, userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图所示。
setbit使用
具体操作过程如下,unique:users:2016-04-05代表2016-04-05这天的独立访问用户的Bitmaps:
127.0.0.1:6379> setbit unique:users:2016-04-05 0 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 5 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 11 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 15 1 (integer) 0 127.0.0.1:6379> setbit unique:users:2016-04-05 19 1 (integer) 0如果此时有一个userid=50的用户访问了网站,那么Bitmaps的结构变成了下图所示,第20位~49位都是0。
userid=50用户访问
很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
2.获取值
getbit key offset获取键的第offset位的值(从0开始算),下面操作获取id=8的用户是否在2016-04-05这天访问过,返回0说明没有访问过:
127.0.0.1:6379> getbit unique:users:2016-04-05 8 (integer) 0由于offset=1000000根本就不存在,所以返回结果也是0:
127.0.0.1:6379> getbit unique:users:2016-04-05 1000000 (integer) 03.获取Bitmaps指定范围值为1的个数
bitcount [start][end]下面操作计算2016-04-05这天的独立访问用户数量:
127.0.0.1:6379> bitcount unique:users:2016-04-05 (integer) 5[start]和[end]代表起始和结束字节数,下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数,对应的用户id是11,15,19。
127.0.0.1:6379> bitcount unique:users:2016-04-05 1 3 (integer) 34.Bitmaps间的运算
bitop op destkey key[key....]bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。假设2016-04-04访问网站的userid=1,2,5,9,如图所示。
2016-04-04访问网站的用户Bitmaps
下面操作计算出2016-04-04和2016-04-03两天都访问过网站的用户数量,如图所示。
127.0.0.1:6379> bitop and unique:users:and:2016-04-04_03 unique: users:2016-04-03 unique:users:2016-04-03 (integer) 2 127.0.0.1:6379> bitcount unique:users:and:2016-04-04_03 (integer) 2如果想算出2016-04-04和2016-04-03任意一天都访问过网站的用户数量(例如月活跃就是类似这种),可以使用or求并集,具体命令如下:
127.0.0.1:6379> bitop or unique:users:or:2016-04-04_03 unique: users:2016-04-03 unique:users:2016-04-03 (integer) 2 127.0.0.1:6379> bitcount unique:users:or:2016-04-04_03 (integer) 6
利用bitop and命令计算两天都访问网站的用户
3.3 Bitmaps分析
假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps分别存储活跃用户可以得到下表:
set和Bitmaps存储一天活跃用户的对比
很明显,这种情况下使用Bitmaps能节省很多的内存空间。但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万(大量的僵尸用户),那么两者的对比如下表所示,很显然,这时候使用Bitmaps就不太合适了,因为基本上大部分位都是0。
set和Bitmaps存储一天活跃用户的对比(独立用户比较少)
4 发布订阅
4.1 基本概念
Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息,如图所示。Redis提供了若干命令支持该功能,在实际应用开发时,能够为此类问题提供实现方法。
Redis发布订阅模型
4.2 命令
Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。
1.发布消息
publish channel message下面操作会向channel:sports频道发布一条消息“Tim won the championship”,返回结果为订阅者个数,因为此时没有订阅,所以返回结果为0:
127.0.0.1:6379> publish channel:sports "Tim won the championship" (integer) 02.订阅消息
subscribe channel [channel ...]订阅者可以订阅一个或多个频道,下面操作为当前客户端订阅了 channel:sports频道:
127.0.0.1:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "channel:sports" 3) (integer) 1此时另一个客户端发布一条消息:
127.0.0.1:6379> publish channel:sports "James lost the championship" (integer) 1当前订阅者客户端会收到如下消息:
127.0.0.1:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) ... 1) "message" 2) "channel:sports" 3) "James lost the championship"如果有多个客户端同时订阅了channel:sports,整个过程如图3-17所示。有关订阅命令有两点需要注意:
·客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。
·新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
多个客户端同时订阅频道channel:sports
开发提示
和很多专业的消息队列系统(例如Kafka、RocketMQ)相比,Redis的发布订阅略显粗糙,例如无法实现消息堆积和回溯。但胜在足够简单,如果当前场景可以容忍的这些缺点,也不失为一个不错的选择。
3.取消订阅
unsubscribe [channel [channel ...]]客户端可以通过unsubscribe命令取消对指定频道的订阅,取消成功后,不会再收到该频道的发布消息:
127.0.0.1:6379> unsubscribe channel:sports 1) "unsubscribe" 2) "channel:sports" 3) (integer) 04.3 使用场景
聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,下面以简单的服务解耦进行说明。如图所示,图中有两套业务,上面为视频管理系统,负责管理视频信息;下面为视频服务面向客户,用户可以通过各种客户端(手机、浏览器、接口)获取到视频信息。
发布订阅用于视频信息变化通知
假如视频管理员在视频管理系统中对视频信息进行了变更,希望及时通知给视频服务端,就可以采用发布订阅的模式,发布视频信息变化的消息到指定频道,视频服务订阅这个频道及时更新视频信息,通过这种方式可以有效解决两个业务的耦合性。
·视频服务订阅video:changes频道如下:
·视频管理系统发布消息到video:changes频道如下:
publish video:changes "video1,video3,video5"·当视频服务收到消息,对视频信息进行更新,如下所示:
for video in video1,video3,video5 update {video}5 客户端通信协议
几乎所有的主流编程语言都有Redis的客户端, 不考虑Redis非常流行的原因,如果站在技术的角度看原因还有两个:
第一,客户端与服务端之间的通信协议是在TCP协议之上构建的。
第二,Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。例如客户端发送一条set hello world命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔):
这样Redis服务端能够按照RESP将其解析为set hello world命令,执行后回复的格式如下:
+OK可以看到除了命令(set hello world)和返回结果(OK)本身还包含了一些特殊字符以及数字,下面将对这些格式进行说明。
1.发送命令格式
RESP的规定一条命令的格式如下,CRLF代表"\r\n"。
依然以set hell world这条命令进行说明。 参数数量为3个,因此第一行为:
*3参数字节数分别是355,因此后面几行为:
$3 SET $5 hello $5 world有一点要注意的是,上面只是格式化显示的结果,实际传输格式为如下代码,整个过程如图所示:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n2.返回结果格式
Redis的返回结果类型分为以下五种,如下图所示:
·状态回复:在RESP中第一个字节为"+“。
·错误回复:在RESP中第一个字节为”-“。
·整数回复:在RESP中第一个字节为”:“。
·字符串回复:在RESP中第一个字节为”$“。
·多条字符串回复:在RESP中第一个字节为”*"。
客户端和服务端使用RESP标准进行数据交互
Redis五种回复类型在RESP下的编码
6 Java客户端Jedis
Java有很多优秀的Redis客户端(详见:http://redis.io/clients#java),这里介绍使用较为广泛的客户端Jedis。
6.1 Jedis的基本使用方法
Jedis的使用方法非常简单,只要下面三行代码就可以实现get功能:
# 1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信 Jedis jedis = new Jedis("127.0.0.1", 6379); # 2. jedis执行set操作 jedis.set("hello", "world"); # 3. jedis执行get操作, value="world" String value = jedis.get("hello");可以看到初始化Jedis需要两个参数:Redis实例的IP和端口,除了这两个参数外,还有一个包含了四个参数的构造函数是比较常用的:
Jedis(final String host, final int port, final int connectionTimeout, final int soTimeout)参数说明:
·host:Redis实例的所在机器的IP。
·port:Redis实例的端口。
·connectionTimeout:客户端连接超时。
·soTimeout:客户端读写超时。
如果想看一下执行结果:
String setResult = jedis.set("hello", "world"); String getResult = jedis.get("hello"); System.out.println(setResult); System.out.println(getResult);输出结果为:
OK world可以看到jedis.set的返回结果是OK,和redis-cli的执行效果是一样的,只不过结果类型变为了Java的数据类型。上面的这种写法只是为了演示使用,在实际项目中比较推荐使用try catch finally的形式来进行代码的书写:一方面可以在Jedis出现异常的时候(本身是网络操作),将异常进行捕获或者抛出;另一个方面无论执行成功或者失败,将Jedis连接关闭掉,在开发中关闭不用的连接资源是一种好的习惯,代码类似如下:
Jedis jedis = null; try {jedis = new Jedis("127.0.0.1", 6379); jedis.get("hello"); } catch (Exception e) { logger.error(e.getMessage(),e); } finally { if (jedis != null) { jedis.close(); } }下面用一个例子说明Jedis对于Redis五种数据结构的操作,为了节省篇幅,所有返回结果放在注释中。
// 1.string // 输出结果:OK jedis.set("hello", "world"); // 输出结果:world jedis.get("hello"); // 输出结果:1 jedis.incr("counter"); // 2.hash jedis.hset("myhash", "f1", "v1"); jedis.hset("myhash", "f2", "v2"); // 输出结果:{f1=v1, f2=v2} jedis.hgetAll("myhash"); // 3.list jedis.rpush("mylist", "1"); jedis.rpush("mylist", "2"); jedis.rpush("mylist", "3"); // 输出结果:[1, 2, 3] jedis.lrange("mylist", 0, -1); // 4.set jedis.sadd("myset", "a"); jedis.sadd("myset", "b"); jedis.sadd("myset", "a"); // 输出结果:[b, a] jedis.smembers("myset"); // 5.zset jedis.zadd("myzset", 99, "tom"); jedis.zadd("myzset", 66, "peter"); jedis.zadd("myzset", 33, "james"); // 输出结果:[[["james"],33.0], [["peter"],66.0], [["tom"],99.0]] jedis.zrangeWithScores("myzset", 0, -1);参数除了可以是字符串,Jedis还提供了字节数组的参数,例如:
public String set(final String key, String value) public String set(final byte[] key, final byte[] value) public byte[] get(final byte[] key) public String get(final String key)有了这些API的支持,就可以将Java对象序列化为二进制,当应用需要获取Java对象时,使用get(final byte[]key)函数将字节数组取出,然后反序列化为Java对象即可。和很多NoSQL数据库(例如Memcache、Ehcache)的客户端不同,Jedis本身没有提供序列化的工具,也就是说开发者需要自己引入序列化的工具。序列化的工具有很多,例如XML、Json、谷歌的Protobuf、Facebook的Thrift等等,对于序列化工具的选择开发者可以根据自身需求决定。
6.2 Jedis连接池的使用方法
之前介绍的是Jedis的直连方式,所谓直连是指Jedis每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式,如图所示。
Jedis直连Redis
因此生产环境中一般使用连接池的方式对Jedis连接进行管理,如图所示,所有Jedis对象预先放在池子中(JedisPool),每次要连接Redis,只需要在池子中借,用完了在归还给池子。
Jedis连接池使用方式
客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化好Jedis连接,所以每次只需要从Jedis连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新建TCP连接的开销。另外直连的方式无法限制Jedis对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效的保护和控制资源的使用。但是直连的方式也并不是一无是处,下表给出两种方式各自的优劣势。
Jedis直连方式和连接池方式对比
Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的通用对象池工具common-pool作为资源的管理工具,下面是使用JedisPool操作Redis的代码示例:
1)Jedis连接池(通常JedisPool是单例的):
2)获取Jedis对象不再是直接生成一个Jedis对象进行直连,而是从连接池直接获取,代码如下:
Jedis jedis = null; try {// 1. 从连接池获取jedis对象 jedis = jedisPool.getResource(); // 2. 执行操作 jedis.get("hello"); } catch (Exception e) { logger.error(e.getMessage(),e); } finally { if (jedis != null) { // 如果使用JedisPool,close操作不是关闭连接,代表归还连接池 jedis.close(); } }这里可以看到在finally中依然是jedis.close()操作,为什么会把连接关闭呢,这不和连接池的原则违背了吗?但实际上Jedis的close()实现方式如下:
public void close() { // 使用Jedis连接池 if (dataSource != null) { if (client.isBroken()) { this.dataSource.returnBrokenResource(this); } else { this.dataSource.returnResource(this); } // 直连 } else { client.close(); } }参数说明:
·dataSource!=null代表使用的是连接池,所以jedis.close()代表归还连接给连接池,而且Jedis会判断当前连接是否已经断开。
·dataSource=null代表直连,jedis.close()代表关闭连接。
前面GenericObjectPoolConfig使用的是默认配置,实际它提供有很多参数,例如池子中最大连接数、最大空闲连接数、最小空闲连接数、连接活性检测,等等,例如下面代码:
上面几个是GenericObjectPoolConfig几个比较常用的属性,下表给出了Generic-ObjectPoolConfig其他属性及其含义解释。
GenericObjectPoolConfig的重要属性
7 客户端API
7.1 client list
client list命令能列出与Redis服务端相连的所有客户端连接信息,例如下面代码是在一个Redis实例上执行client list的结果:
127.0.0.1:6379> client list id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=300210 addr=10.2.xx.215:61972 fd=3342 name= age=8054103 idle=8054103 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=5448879 addr=10.16.xx.105:51157 fd=233 name= age=411281 idle=331077 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ttl id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=7125108 addr=10.10.xx.103:33403 fd=139 name= age=241 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del id=7125109 addr=10.10.xx.101:58658 fd=140 name= age=241 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del ...输出结果的每一行代表一个客户端的信息,可以看到每行包含了十几个属性,它们是每个客户端的一些执行状态,理解这些属性对于Redis的开发和运维人员非常有帮助。下面将选择几个重要的属性进行说明,其余通过表格的形式进行展示。
(1)标识:id、addr、fd、name
这四个属性属于客户端的标识:
·id:客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。
·addr:客户端连接的ip和端口。
·fd:socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1 代表当前客户端不是外部客户端,而是Redis内部的伪装客户端。
·name:客户端的名字,后面的client setName和client getName两个命令会对其进行说明。
(2)输入缓冲区:qbuf、qbuf-free
Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能,如图所示。
client list中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。下面是Redis源码中对于输入缓冲区的硬编码:
输入缓冲区基本模型
输入缓冲使用不当会产生两个问题:
·一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
·输入缓冲区不受maxmemory控制,假设一个Redis实例设置了 maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况(如图所示)。
输入缓冲区超过了maxmemory
执行效果如下:
127.0.0.1:6390> info memory # Memory used_memory_human:5.00G ... maxmemory_human:4.00G ....上面已经看到,输入缓冲区使用不当造成的危害非常大,那么造成输入缓冲区过大的原因有哪些?输入缓冲区过大主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况。还有一种情况就是Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区, 造成了输入缓冲区过大。那么如何快速发现和监控呢?监控输入缓冲区异常的方法有两种:
·通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。
·通过info命令的info clients模块,找到最大的输入缓冲区,例如下面命令中的其中client_biggest_input_buf代表最大的输入缓冲区,例如可以设置超过10M就进行报警:
这两种方法各有自己的优劣势,下表对两种方法进行了对比。
对比client list和info clients监控输入缓冲区的优劣势
运维提示
输入缓冲区问题出现概率比较低,但是也要做好防范,在开发中要减少bigkey、减少Redis阻塞、合理的监控报警。
(3)输出缓冲区:obl、oll、omem
Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲,如图所示。与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-output-buffer-limit来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端,如图所示。
客户端输出缓冲区模型
三种不同类型客户端的输出缓冲区
对应的配置规则是:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>·class:客户端类型,分为三种。a)normal:普通客户端;b) slave:slave客户端,用于复制;c)pubsub:发布订阅客户端。
·hard limit:如果客户端使用的输出缓冲区大于,客户端会被立即关闭。
·soft limit和soft seconds:如果客户端使用的输出缓冲区超过了并且持续了秒,客户端会被立即关闭。
Redis的默认配置是:
client-output-buffer-limit normal 0 0 0 client-output-buffer-limit slave 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等情况。
监控输出缓冲区的方法依然有两种:
·通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
·通过info命令的info clients模块,找到输出缓冲区列表最大对象数,例如:
其中,client_longest_output_list代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的,这里就不再赘述了。相比于输入缓冲区,输出缓冲区出现异常的概率相对会比较大,那么如何预防呢?方法如下:
·进行上述监控,设置阀值,超过阀值及时处理。
·限制普通客户端输出缓冲区的,把错误扼杀在摇篮中,例如可以进行如下设置:
·适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。
·限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令。
·及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。
(4)客户端的存活状态
client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间:
例如上面这条记录代表当期客户端连接Redis的时间为603382秒,其中空闲了331060秒:
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get例如上面这条记录代表当期客户端连接Redis的时间为8888581秒,其中空闲了8888581秒,实际上这种就属于不太正常的情况,当age等于idle时,说明连接一直处于空闲状态。 为了更加直观地描述age和idle,下面用一个例子进行说明:
String key = "hello"; // 1) 生成jedis,并执行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println(jedis.get(key)); // 2) 休息10秒 TimeUnit.SECONDS.sleep(10); // 3) 执行新的操作ping System.out.println(jedis.ping()); // 4) 休息5秒 TimeUnit.SECONDS.sleep(5); // 5) 关闭jedis连接 jedis.close();下面对代码中的每一步进行分析,用client list命令来观察age和idle参数的相应变化。
注意
为了与redis-cli的客户端区分,本次测试客户端IP地址:10.7.40.98。
1)在执行代码之前,client list只有一个客户端,也就是当前的redis-cli,下面为了节省篇幅忽略掉这个客户端。
127.0.0.1:6379> client list id=45 addr=127.0.0.1:55171 fd=6 name= age=2 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client2)使用Jedis生成了一个新的连接,并执行get操作,可以看到IP地址为10.7.40.98的客户端,最后执行的命令是get,age和idle分别是1秒和0秒:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=1 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get3)休息10秒,此时Jedis客户端并没有关闭,所以age和idle一直在递增:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=9 idle=9 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get4)执行新的操作ping,发现执行后age依然在增加,而idle从0计算,也就是不再闲置:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=11 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping5)休息5秒,观察age和idle增加:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=15 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping6)关闭Jedis,Jedis连接已经消失:
redis-cli client list | grep "10.7.40.98”为空(5)客户端的限制maxclients和timeout
Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数:
可以通过config set maxclients对最大客户端连接数进行动态设置:
127.0.0.1:6379> config get maxclients 1) "maxclients" 2) "10000" 127.0.0.1:6379> config set maxclients 50 OK 127.0.0.1:6379> config get maxclients 1) "maxclients" 2) "50"一般来说maxclients=10000在大部分场景下已经绝对够用,但是某些情况由于业务方使用不当(例如没有主动关闭连接)可能存在大量idle连接, 无论是从网络连接的成本还是超过maxclients的后果来说都不是什么好事,因此Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭,例如设置timeout为30秒:
#Redis默认的timeout是0,也就是不会检测客户端的空闲 127.0.0.1:6379> config set timeout 30 OK下面继续使用Jedis进行模拟,整个代码和上面是一样的,只不过第2)步骤休息了31秒:
String key = "hello"; // 1) 生成jedis,并执行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println(jedis.get(key)); // 2) 休息31秒 TimeUnit.SECONDS.sleep(31); // 3) 执行get操作 System.out.println(jedis.get(key)); // 4) 休息5秒 TimeUnit.SECONDS.sleep(5); // 5) 关闭jedis连接 jedis.close();执行上述代码可以发现在执行完第2)步之后,client list中已经没有了Jedis的连接,也就是说timeout已经生效,将超过30秒空闲的连接关闭掉:
127.0.0.1:6379> client list id=16 addr=10.7.40.98:63892 fd=6 name= age=19 idle=19 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get # 超过timeout后,Jedis连接被关闭 redis-cli client list | grep “10.7.40.98”为空同时可以看到,在Jedis代码中的第3)步抛出了异常,因为此时客户端已经被关闭,所以抛出的异常是JedisConnectionException,并且提示Unexpected end of stream:
stream: world Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.如果将Redis的loglevel设置成debug级别,可以看到如下日志,也就是客户端被Redis关闭的日志:
12885:M 26 Aug 08:46:40.085 - Closing idle clientRedis的默认配置给出的timeout=0,在这种情况下客户端基本不会出现上面的异常,这是基于对客户端开发的一种保护。例如很多开发人员在使用JedisPool时不会对连接池对象做空闲检测和验证,如果设置了timeout>0,可能就会出现上面的异常,对应用业务造成一定影响,但是如果Redis的客户端使用不当或者客户端本身的一些问题,造成没有及时释放客户端连接,可能会造成大量的idle连接占据着很多连接资源,一旦超过maxclients;后果也是不堪设想。所在在实际开发和运维中,需要将timeout设置成大于0,例如可以设置为300秒,同时在客户端使用上添加空闲检测和验证等等措施,例如JedisPool使用common-pool提供的三个属性:minEvictableIdleTimeMillis、
testWhileIdle、timeBetweenEvictionRunsMillis。
(6)客户端类型
client list中的flag是用于标识当前客户端的类型,例如flag=S代表当前客户端是slave客户端、flag=N代表当前是普通客户端,flag=O代表当前客户端正在执行monitor命令,下表列出了11种客户端类型。
(7)其他
上面已经将client list中重要的属性进行了说明,下表列出之前介绍过以及一些比较简单或者不太重要的属性。
client list命令结果的全部属性
7.2 monitor
monitor命令用于监控Redis正在执行的命令,如图4-11所示,我们打开了两个redis-cli,一个执行set get ping命令,另一个执行monitor命令。可以看到monitor命令能够监听其他客户端正在执行的命令,并记录了详细的时间戳。
monitor命令演示
monitor的作用很明显,如果开发和运维人员想监听Redis正在执行的命令,就可以用monitor命令,但事实并非如此美好,每个客户端都有自己的输出缓冲区,既然monitor能监听到所有的命令,一旦Redis的并发量过大,monitor客户端的输出缓冲会暴涨,可能瞬间会占用大量内存,下图展示了monitor命令造成大量内存使用。
高并发下monitor命令使用大量输出缓冲区
7.3 客户端相关配置
·timeout:检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。
·maxclients:客户端最大连接数,前面已进行分析,这里不再赘述,但是这个参数会受到操作系统设置的限制。
·tcp-keepalive:检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。
·tcp-backlog:TCP三次握手后,会将接受的连接放入队列中,tcp-
backlog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不需要调整,但是这个参数会受到操作系统的影响,例如在Linux操作系统中,如果/proc/sys/net/core/somaxconn小于tcp-backlog,那么在Redis启动时会看到如下日志,并建议将/proc/sys/net/core/somaxconn设置更大。
修改方法也非常简单,只需要执行如下命令:
echo 511 > /proc/sys/net/core/somaxconn总结
以上是生活随笔为你收集整理的《Redis开发与运维》- 核心知识整理二(Lua脚本、发布订阅、客户端等)的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: DevExpress.Utils.Too
- 下一篇: mysql 常用命令(一)