Redis 开发与运维客户端

Posted 木兮同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 开发与运维客户端相关的知识,希望对你有一定的参考价值。

文章目录


一、客户端通信协议

  • 几乎所有的主流编程语言都有 Redis 的客户端,不考虑 Redis 非常流行的原因,如果站在技术的角度看原因还有两个
    • 第一,客户端与服务端之间的通信协议是在 TCP 协议 之上构建的。
    • 第二,Redis 制定了 RESP(REdis Serialization Protocol,Redis 序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

二、Java 客户端 Jedis

获取Jedis

  • 添加maven依赖
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.8.2</version>
</dependency>

Jedis 的基本使用

  • 演示获取 Jedis 对象,进行简单地set、get操作,要注意开发中关闭不用的连接资源。
	Jedis jedis = null;
	try 
		// 1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
		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");
	
		System.out.println(value);
	 catch (Exception e) 
		log.error(e.getMessage(), e);
	 finally 
		if (jedis != null) 
			jedis.close();
		
	
  • Jedis 操作其他五种数据结构演示
	// 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对象即可。

Jedis 连接池使用

  • Jedis 直连方式和连接池方式对比

上面是 Jedis 的直连方式,所谓直连是指Jedis每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式,因此生产环境中一般使用连接池的方式对Jedis连接进行管理

方式优点缺点
直连简单方便,适用于少量长期连接的场景1.存在每次新建/关闭TCP连接开销 2.资源无法控制,极端情况会出现连接泄漏 3. Jedis 对象线程不安全
连接池1.无需每次连接都生成 Jedis 对象,降低开销 2.使用连接池的形式保护和控制资源的使用相对于直连使用相对麻烦,尤其在资源的管理上需要很多参数来保证,一旦规划不合理也会出现问题
  • 连接池方式使用
	// common-pool连接池配置,这里使用默认配置,后面小节会介绍具体配置说明
    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    // 初始化Jedis连接池
    JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

    Jedis jedis = null;
    try 
        // 1. 从连接池获取jedis对象
        jedis = jedisPool.getResource();
        // 2. 执行操作
        jedis.get("hello");
     catch (Exception e) 
        log.error(e.getMessage(), e);
     finally 
        if (jedis != null) 
            // 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
            jedis.close();
        
    

Jedis 中 Pipeline 的使用

前面介绍了 Pipeline 能够将一组 Redis 命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端。

  • 比如来实现一个批量删除 mdel 的操作,实际上Redis提供了mget、mset方法,但是并没有提供mdel方法
    Jedis jedis = new Jedis("127.0.0.1");
    // 1)生成pipeline对象
    Pipeline pipeline = jedis.pipelined();
    // 2)pipeline执行命令,注意此时命令并未真正执行
    for (String key : keys) 
        pipeline.del(key);
    
    // 3)执行命令
    pipeline.sync();
  • 除了 pipeline.sync() ,还可以使用 pipeline.syncAndReturnAll() 将pipeline的命令进行返回,例如下面代码将 set 和 incr 做了一次 pipeline 操作,并顺序打印了两个命令的结果
    Jedis jedis = new Jedis("127.0.0.1");
    Pipeline pipeline = jedis.pipelined();
    pipeline.set("hello", "world");
    pipeline.incr("counter");
    List<Object> resultList = pipeline.syncAndReturnAll();
    for (Object object : resultList) 
        System.out.println(object);
    

Jedis 的 Lua 脚本使用

  • Jedis 中执行 Lua 脚本和 redis-cli 十分类似,Jedis 提供了三个重要的函数实现 Lua 脚本的执行
	Object eval(String script, int keyCount, String... params)
	Object evalsha(String sha1, int keyCount, String... params)
	String scriptLoad(String script)
  • eval函数
    • script:Lua脚本内容
    • keyCount:键的个数
    • params:相关参数KEYS和ARGV
  • scriptLoad和evalsha函数 要一起使用
    • scriptSha:脚本的SHA1
    • keyCount:键的个数
    • params:相关参数KEYS和ARGV
    // 首先使用scriptLoad将脚本加载到Redis中
    String scriptSha = jedis.scriptLoad(script);
    String key = "hello";
    Object result = jedis.evalsha(scriptSha, 1, key);
    // 打印结果为world
    System.out.println(result);

三、客户端管理

Redis 提供了客户端相关 API 对其状态进行监控和管理。

客户端 API

  • client list:列出与 Redis 服务端相连的所有客户端连接信息
  • client setName 和 client getName:分别用于给客户端设置和获取名字,这样比较容易表示出客户端的来源
  • client kill:用于杀掉指定 IP 地址和端口的客户端
  • client pause:用于阻塞客户端 timeout 毫秒数,在此期间客户端连接将被阻塞。
  • monitor:用于监控 Redis 正在执行的命令

客户端常见异常

  1. 无法从连接池获取到连接
  • JedisPool 中的 Jedis 对象个数是有限的,默认是 8 个。如果都被占用的,并且没有归还,此时调用者还要从 JedisPool 中借用 Jedis,就需要进行等待,如果在 maxWaitMillis 时间内仍然无法获取到 Jedis 对象就会抛如下异常:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
...
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)
  • 还有一种情况,就是设置了 blockWhenExhausted=false,那么调用者发现池子中没有资源时,会立即抛出异常不进行等待,如下:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
...
Caused by: java.util.NoSuchElementException: Pool exhausted as org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:446)
  • 对于这个问题,造成没有资源的原因非常多,可能如下:
    • 客户端:高并发下连接池设置过小
    • 客户端:没有正确使用连接池,比如没有释放
    • 客户端:存在慢查询操作
    • 服务端:Redis 服务端由于一些原因造成了客户端命令执行过程的阻塞
  1. 客户端读写超时
  • 出现时会抛出下面的异常:
redis.clients.jedis.exceptions.JedisConnectionException: 
java.net.SocketTimeoutException: Read timed out
  • 造成原因可能有:
    • 读写超时时间设置得过短
    • 命令本身就比较慢
    • 客户端与服务端网络不正常
    • Redis 自身发生阻塞
  1. 客户端连接超时
  • 出现时会抛出下面的异常:
redis.clients.jedis.exceptions.JedisConnectionException: 
java.net.SocketTimeoutException: connect timed out
  • 造成原因可能有:
    • 连接超时设置过短
    • Redis 发生阻塞
    • 客户端与服务端网络不正常
  1. 客户端缓冲区异常
  • 出现时会抛出下面的异常:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
  • 造成原因可能有:
    • 输出缓冲区满了
    • 长时间闲置连接被服务端主动断开
    • 不正常并发读写:Jedis 对象同时被多个线程并发操作,可能会出现上述问题
  1. Lua 脚本正在执行
  2. Redis 正在加载持久化文件
  • 出现时会抛出下面的异常:
redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory
  1. Redis 使用的内存超过 maxmemory 配置
  • 出现时会抛出下面的异常:
redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'.
  1. 客户端连接数过大
redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached

四、客户端案例分析

Redis 内存陡增

  • 可能原因
    • 确实有大量写入
    • 排查是否有客户端缓冲区造成主节点内存陡增,使用 info clients 命令查询。
  • 处理方法
    • 使用 client kill 命令杀掉这个连接,让其他客户段恢复正常些数据即可。
  • 后期处理
    • 从运维层面禁止 monitor 命令
    • 从开发层面进行培训,禁止在生产环境中使用 monitor 命令
    • 限制输出缓冲区的大小
    • 使用专业的 Redis 运维工具,比如后面介绍的 CacheCloud,上述问题都会得到报警,快速发现和定位问题

客户端周期性超时

  • 现象
    • 客户端现象:客户端出现大量超时
    • 服务端现象:服务端并没有明显的异常,只是有一些慢查询操作
  • 分析
    • 网络原因:服务端和客户端之间的网络出现周期性问题
    • Redis 本身:查看 Redis 日志统计
    • 客户端:对比慢查询日志的历史记录,看看是否慢查询导致
  • 后期处理
    • 从运维层面,监控慢查询,一旦超过阈值,就发出报警
    • 从开发层面,加强对于 Redis 的理解,避免不正确的使用方式
    • 使用专业的 Redis 运维工具,同上

来源:《Redis 开发与运维》第 4 章 客户端

以上是关于Redis 开发与运维客户端的主要内容,如果未能解决你的问题,请参考以下文章

Redis开发与运维

Redis 开发与运维客户端

Redis 开发与运维Redis 的噩梦:阻塞

Redis 开发与运维Redis 的噩梦:阻塞

Redis 开发与运维Redis 的噩梦:阻塞

Redis 开发与运维Redis Cluster 集群