Redis客户端

Redis客户端

获取Jedis

Jedis属于Java的第三方开发包,在Java中获取第三方开发包通常有两种方式:

  • 直接下载目标版本的Jedis-${version}.jar包加入到项目中。

  • 使用集成构建工具,例如maven、gradle等将Jedis目标版本的配置加入到项目中。

    通常在实际项目中使用第二种方式,但如果只是想测试一下Jedis,第一种方法也是可以的。

    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.3.0</version>
    </dependency>
    

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(final String host, final int port, final int connectionTimeout, final intsoTimeout)
参数说明:

  • host:Redis实例的所在机器的IP。
  • port:Redis实例的端口。
  • connectionTimeout:客户端连接超时。
  • soTimeout:客户端读写超时。

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)

就可以将Java对象序列化为二进制,当应用需要获取Java对象时,使用get(final byte[]key)函数将字节数组取出,然后反序列化为Java对象即可。

RedisTemplate,提供了多种序列化方式。

Jedis连接池的使用方法

客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连
接,而连接池的方式是可以预先初始化好Jedis连接,所以每次只需要从
Jedis连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并
发同步开销,远远小于新建TCP连接的开销。另外直连的方式无法限制Jedis对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效的保护和控制资源的使用。
Jedis直连方式和连接池方式对比
Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的
通用对象池工具common-pool作为资源的管理工具

// common-pool连接池配置,这里使用默认配置,后面小节会介绍具体配置说明
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

获取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();
	}
}

jedis.close();分两种情况

  • 归还连接给连接池,而且Jedis会判断当前连接是否已经断开。
  • 直连模式,jedis.close()代表关闭连接。

GenericObjectPoolConfig相关参数表:
GenericObjectPoolConfig的重要属性

Redis中Pipeline的使用方法

实现mdel删除

public void mdel(List<String> keys) {
	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();
}

Jedis的Lua脚本

Jedis提供了三个重要的函数实现Lua脚本的执行
Object eval(String script, int keyCount, String… params)
Object evalsha(String sha1, int keyCount, String… params)
String scriptLoad(String script)

参数说明:

  • script:Lua脚本内容。
  • keyCount:键的个数。
  • params:相关参数KEYS和ARGV

在java中执行

String key = "hello";
String script = "return redis.call('get',KEYS[1])";
Object result = jedis.eval(script, 1, key);
// 打印结果为world
System.out.println(result)

scriptLoad和evalsha函数要一起使用,首先使用scriptLoad将脚本加载到
Redis中,代码如下:

String scriptSha = jedis.scriptLoad(script);

evalsha函数用来执行脚本的SHA1校验和,它需要三个参数:

  • scriptSha:脚本的SHA1。
  • keyCount:键的个数。
  • params:相关参数KEYS和ARGV。
Stirng key = "hello";
Object result = jedis.evalsha(scriptSha, 1, key);
// 打印结果为world
System.out.println(result);

注意点

1)Jedis操作放在try catch finally里更加合理。
2)区分直连和连接池两种实现方式优缺点。
3)jedis.close()方法的两种实现方式。
4)Jedis依赖了common-pool,有关common-pool的参数需要根据不同的使用场景,各不相同,需要具体问题具体分析。
5)如果key和value涉及了字节数组,需要自己选择适合的序列化方法。

客户端常见异常

  • 无法从连接池获取到连接
    JedisPool中的Jedis对象个数是有限的,默认是8个。这里假设使用的默
    认配置,如果有8个Jedis对象被占用,并且没有归还,此时调用者还要从
    JedisPool中借用Jedis,就需要进行等待(例如设置了maxWaitMillis>0),如
    果在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,那么调用者发现
    池子中没有资源时,会立即抛出异常不进行等待,下面的异常就是

    blockWhenExhausted=false时的效果:
    redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource
    from the pool
    …
    Caused by: java.util.NoSuchElementException: Pool exhausted
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.
    java:464)
    

    对于这个问题,需要重点讨论的是为什么连接池没有资源了,造成没有资源的原因非常多,可能如下:

    • 客户端:高并发下连接池设置过小,出现供不应求,所以会出现上面的错误,但是正常情况下只要比默认的最大连接数(8个)多一些即可,因为正常情况下JedisPool以及Jedis的处理效率足够高。
    • 客户端:没有正确使用连接池,比如没有进行释放,例如下面代码所示。定义JedisPool,使用默认的连接池配置:
      GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
      JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
      
      像JedisPool借用8次连接,但是没有执行归还操作:
      for (int i = 0; i < 8; i++) {
      	Jedis jedis = null;
      	try {
      		jedis = jedisPool.getResource();
      		jedis.ping();
      	} catch (Exception e) {
      		e.printStackTrace();
      	}
      }
      
      当调用者再向连接池借用Jedis时(如下操作),就会抛出异常:
      jedisPool.getResource().ping();
    • 客户端:存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比
      较慢,造成池子满了。
    • 服务端:客户端是正常的,但是Redis服务端由于一些原因造成了客户
      端命令执行过程的阻塞,也会使得客户端抛出这种异常。
  • 客户端读写超时
    Jedis在调用Redis时,如果出现了读写超时后,会出现下面的异常:

    redis.clients.jedis.exceptions.JedisConnectionException:
    java.net.SocketTimeoutException: Read timed out
    

    造成该异常的原因也有以下几种:

    • 读写超时间设置得过短。
    • 命令本身就比较慢。
    • 客户端与服务端网络不正常。
    • Redis自身发生阻塞。
  • 客户端连接超时
    Jedis在调用Redis时,如果出现了连接超时后,会出现下面的异常:

    redis.clients.jedis.exceptions.JedisConnectionException:
    java.net.SocketTimeoutException: connect timed out
    

    造成该异常的原因也有以下几种:
    1)连接超时设置得过短,可以通过下面代码进行设置:

    // 毫秒
    jedis.getClient().setConnectionTimeout(time);
    

    2)Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败。
    3)客户端与服务端网络不正常。

  • 客户端缓冲区异常
    Jedis在调用Redis时,如果出现客户端数据流异常,会出现下面的异常:

    redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    

    造成这个异常的原因可能有如下几种:
    1)输出缓冲区满。例如将普通客户端的输出缓冲区设置为1M1M60:

    config set client-output-buffer-limit "normal 1048576 1048576 60 slave 268435456
    67108864 60 pubsub 33554432 8388608 60"
    

    如果使用get命令获取一个bigkey(例如3M),就会出现这个异常。
    2)长时间闲置连接被服务端主动断开,上节已经详细分析了这个问题。
    3)不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现上述异常。

  • Lua脚本正在执行
    如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,此时Jedis调用Redis时,会收到下面的异常。

    redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a
    script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
    
  • Redis正在加载持久化文件
    Jedis调用Redis时,如果Redis正在加载持久化文件,那么会收到下面的异常:

    redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the
    dataset in memory
    
  • Redis使用的内存超过maxmemory配置
    Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会收到下面的异常,此时应该调整maxmemory并找到造成内存增长的原因:redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > ‘maxmemory’.

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

    redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached
    

    此时新的客户端连接执行任何命令,返回结果都是如下:

    127.0.0.1:6379> get hello
    (error) ERR max number of clients reached
    

    这个问题可能会比较棘手,因为此时无法执行Redis命令进行问题修复,一般来说可以从两个方面进行着手解决:

    • 客户端:如果maxclients参数不是很小的话,应用方的客户端连接数基本不会超过maxclients,通常来看是由于应用方对于Redis客户端使用不当造成的。此时如果应用方是分布式结构的话,可以通过下线部分应用节点(例如占用连接较多的节点),使得Redis的连接数先降下来。从而让绝大部分节点可以正常运行,此时再通过查找程序bug或者调整maxclients进行问题的修复。
    • 服务端:如果此时客户端无法处理,而当前Redis为高可用模式(例如Redis Sentinel和Redis Cluster),可以考虑将当前Redis做故障转移。此问题不存在确定的解决方式,但是无论从哪个方面进行处理,故障的快速恢复极为重要,当然更为重要的是找到问题的所在,否则一段时间后客户端连接数依然会超过maxclients。

问答

什么时候使用pipeline,什么时候使用lua

当多个redis命令之间没有依赖、顺序关系(例如第二条命令依赖第一条命令的结果)时,建议使用pipline;如果命令之间有依赖或顺序关系时,pipline就无法使用,此时可以考虑才用lua脚本的方式来使用。

Jedis、Spring Data Redis和Redisson区别

Jedis是Redis官方推荐的面向Java的操作Redis的客户端,而RedisTemplate是SpringDataRedis中对JedisApi的高度封装。
Spring Data Redis:Spring-data-redis是Spring大家族的一部分,提供了在srping应用中通过简单的连接池配置访问redis服务,对Reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装。其中的RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。而且RedisTemplate还支持对象缓存操作。
Redisson:一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),它充分利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类(5中常见数据类型、布隆过滤器、分布式锁),让使用Redis更加简单、便捷,从而让使用者能够将更多精力集中到业务逻辑处理上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值