Redis客户端


Redis为不同的开发语言提供了客户端,如Java的Jedis、Python的redis-py等。

Jedis

Jedis是Java使用的较为广泛的客户端。

获取Jedis

Jedis属于Java的第三方开发包,可以在https://github.com/xetorthio/jedis获取。

Jedis的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class JedisTest {

public static void main(String[] args){
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 设置开启jmx功能
poolConfig.setJmxEnabled(true);
// 设置连接池没有连接后客户端的最大等待时间(单位为毫秒)
poolConfig.setMaxWaitMillis(3000);
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "192.168.1.138", 6379);

Jedis jedis = null;

try {
// 从连接池获取jedis对象
jedis = jedisPool.getResource();

jedis.set("hello", "world");
System.out.println(jedis.get("hello"));

} catch(Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
// 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
jedis.close();
}
}
}
}
Jedis连接池

Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的通用对象池工具common-pool作为资源的管理工具。

使用连接池的方式可以预先初始化好Jedis连接,所以每次只需要从Jedis连接池借用即可。

上面的代码创建了一个连接池,从连接池直接获取Jedis对象,然后执行redissetget操作。

GenericObjectPoolConfig提供有很多参数,例如池子中最大连接数、最大空闲连接数、最小空闲连接数、连接活性检测,等等。下表是Generic-ObjectPoolConfig参数的含义:

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

哈希操作

1
2
3
4
jedis.hset("myhash", "f1", "v1");
jedis.hset("myhash", "f2", "v2");
// 结果为:{f1=v1, f2=v2}
jedis.hgetAll("myhash");

列表操作

1
2
3
4
5
jedis.rpush("mylist", "1");
jedis.rpush("mylist", "2");
jedis.rpush("mylist", "3");
// 结果为:[1, 2, 3]
jedis.lrange("mylist", 0, -1);

集合操作

1
2
3
4
5
jedis.sadd("myset", "a");
jedis.sadd("myset", "b");
jedis.sadd("myset", "b");
// 结果为:[b, a]
jedis.smembers("myset");

有序集合操作

1
2
3
4
5
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);

Pipeline

Redis提供了mgetmset方法,但是并没有提供mdel方法,如果想实现这个功能,可以借助Pipeline来模拟批量删除,虽然不会像mgetmset那样是一个原子命令,但是在绝大数场景下可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;

public class JedisTest {

public static void main(String[] args){
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 设置开启jmx功能
poolConfig.setJmxEnabled(true);
// 设置连接池没有连接后客户端的最大等待时间(单位为毫秒)
poolConfig.setMaxWaitMillis(3000);
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "192.168.1.138", 6379);

Jedis jedis = null;

try {
// 从连接池获取jedis对象
jedis = jedisPool.getResource();

String[] keys = {"hello", "myhash", "mylist", "myset", "myzset"};
// 生成pipeline对象
Pipeline pipeline = jedis.pipelined();
for (String key : keys) {
pipeline.del(key);
}
// 执行命令
pipeline.sync();

} catch(Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
// 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
jedis.close();
}
}
}
}

上面的代码使用jedis.pipelined()生成一个pipeline对象,将del命令封装到pipeline中,此时命令并未执行,最后,使用pipeline.sync()执行命令。

可以使用pipeline.syncAndReturnAll()获得pipeline的执行命令后的返回值,例如下面代码将setincr做了一次pipeline操作,并顺序打印两个命令的结果:

1
2
3
4
5
6
7
8
Pipeline pipeline = jedis.pipelined();

pipeline.set("hello", "world");
pipeline.incr("counter");
List<Object> resultList = pipeline.syncAndReturnAll();
for (Object object : resultList) {
System.out.println(object);
}

输出结果为:

OK
1

redis-py

redis-py是Python使用的较为广泛的客户端。

获取redis-py

pip install redis

redis-py的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
test redis
"""

__author__ = 'heqingliang'


import redis


def test_redis():
r = redis.StrictRedis(host='192.168.1.138', port=6379, db=0)
r.set('hello', 'world')
# 结果为:b'world'
r.get('hello')


if __name__ == '__main__':
test_redis()

哈希操作

1
2
3
4
r.hset('myhash', 'f1', 'v1')
r.hset('myhash', 'f2', 'v2')
# 结果为:{b'f1': b'v1', b'f2': b'v2'}
r.hgetall('myhash')

列表操作

1
2
3
4
5
r.rpush('mylist', '1')
r.rpush('mylist', '2')
r.rpush('mylist', '3')
# 结果为:[b'1', b'2', b'3']
r.lrange('mylist', 0, -1)

集合操作

1
2
3
4
5
r.sadd('myset', 'a')
r.sadd('myset', 'b')
r.sadd('myset', 'b')
# 结果为:{b'a', b'b'}
r.smembers('myset')

有序集合操作

1
2
3
4
5
r.zadd('myzset', '99', 'tom')
r.zadd('myzset', '60', 'peter')
r.zadd('myzset', '50', 'james')
# 结果为:[(b'james', 50.0), (b'peter', 60.0), (b'tom', 99.0)]
r.zrange('myzset', 0, -1, withscores=True)

Pipeline

1
2
3
4
5
6
def mdel():
r = redis.StrictRedis(host='192.168.1.138', port=6379)
pipeline = r.pipeline(transaction=False)
for key in ('hello', 'myhash', 'mylist', 'myset', 'myzset'):
pipeline.delete(key)
return pipeline.execute()

创建Pipeline时,参数transaction=False代表不使用事务。pipeline.delete(key)并没有执行命令,只是把命令封装到Pipeline中。调用pipeline.execute()真正执行命令,并返回相应的值。

1
2
3
4
pipeline.set('hello', 'world')
pipeline.incr('counter')

print(pipeline.execute())

输出结果为:

[True, 2]

客户端API

client list

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

127.0.0.1:6379> client list
id=31 addr=192.168.1.1:52651 fd=6 name= age=2260 idle=2260 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=48 addr=127.0.0.1:51352 fd=7 name= age=816 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=client

属性的含义:

参数 含义
id 客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0
addr 客户端连接的ip和端口
fd socket的文件描述符
name 客户端的连接名
age 客户端连接存活时间
idle 客户端连接空闲时间
flags 客户端类型标识
db 当前客户端正在使用的数据库索引下标
sub/pub 当前客户端订阅的频道或者模式数
multi 当前事务中已执行命令的个数
qbuf 输入缓冲区总容量
qbuf-free 输入缓冲区剩余容量
obl 固定缓冲区的长度
oll 动态缓冲区的列表的长度
omem 固定缓冲区和动态缓冲区使用的容量
events 文件描述符事件(r/w):r和w分别代表客户端套接字可读和可写
cmd 当前客户端最后一次执行的命令,不包括参数

输入缓冲区:qbuf、qbuf-free

Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis会从输入缓冲区读取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能。

client listqbufqbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。

输入缓冲使用不当会产生两个问题:

  • 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
  • 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况。

造成输入缓冲区过大的原因:

  • 输入缓冲区的命令包含了大量bigkey
  • Redis发生了阻塞,短期内不能处理命令

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

  • 通过定期执行client list命令,收集qbufqbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。
  • 通过info命令的info clients模块,找到最大的输入缓冲区,client_biggest_input_buf代表最大的输入缓冲区。

输出缓冲区:obl、oll、omem

与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-outputbuffer-limit来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同
分为三种:普通客户端输出缓冲区、发布订阅客户端输出缓冲区、slave客户端输出缓冲区。

对应redis.conf的配置规则:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

<class>:客户端类型,分为三种:

  • normal:普通客户端
  • slave:slave客户端
  • pubsub:发布订阅客户端

<hard limit>:如果客户端使用的输出缓冲区大于<hard limit>,客户端会被立即关闭。

<soft limit><soft seconds>:如果客户端使用的输出缓冲区超过了<soft limit>并且持续了<soft limit>秒,客户端会被立即关闭。

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等情况。

实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果,例如大的字符串、hgetallsmembers命令的结果等。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中。

client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。

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

  • 通过定期执行client list命令,收集oblollomem找到异常的连接记录并分析,最终找到可能出问题的客户端。
  • 通过info命令的info clients模块,找到输出缓冲区列表最大对象数,client_longest_output_list代表输出缓冲区列表最大对象数。

输出缓冲区预防:

  • 进行上述监控,设置阀值,超过阀值及时处理。
  • 限制普通客户端输出缓冲区,可以进行如下设置:

    client-output-buffer-limit normal 20mb 10mb 120
    
  • 适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。
  • 限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令。
  • 及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。

客户端的限制maxclients和timeout:

Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数:

127.0.0.1:6379> info clients
# Clients
connected_clients:1
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
127.0.0.1:6379> 

Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭,Redis默认的timeout是0,也就是不会检测客户端的空闲,例如设置timeout为30秒:

127.0.0.1:6379> config set timeout 30
OK

客户端类型:

client list中的flag是用于标识当前客户端的类型,例如flag=S代表当前客户端是slave客户端、flag=N代表当前是普通客户端。

客户端类型 说明
N 普通客户端
M 当前客户端是master节点
S 当前客户端是slave节点
O 当前客户端正在执行monitor命令
x 当前客户端正在执行事务
b 当前客户端正在等待阻塞事件
d 一个受监视的键已被修改,EXEC命令失败
u 客户端未被阻塞
c 回复完整输出后,关闭连接
A 尽可能快地关闭连接