1.获取Jedis
maven配置加入项目中
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.2</version>
</dependency>
2.Jedis的基本使用
Jedis的使用方法非常简单,只要下面三行代码就可以实现get功能:
import redis.clients.jedis.Jedis;
public class JedisTest {
public static void main(String[] args) {
Jedis jedis=null;
try {
//1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
jedis = new Jedis("59.110.35.177", 6379);
jedis.auth("password");
// 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);
}
catch (Exception e){
e.printStackTrace();
}finally {
if (jedis != null) {
jedis.close();
}
}
}
}
参数除了可以是字符串,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等等,对于序列化工具的选择开发者可以根据自身需求决定,下面以protostuff(Protobuf的Java客户端)为例子进行说明。
1)protostuff的Maven依赖:
<protostuff.version>1.0.11</protostuff.version>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>${protostuff.version}</version>
</dependency>
2)定义实体类:
// 俱乐部
public class Club implements Serializable {
private int id; // id
private String name; // 名称
private String info; // 描述
private Date createDate; // 创建日期
private int rank; // 排名
// 相应的getter setter不占用篇幅
}
3)序列化工具类ProtostuffSerializer提供了序列化和反序列化方法:
package com.sohu.tv.serializer;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import java.util.concurrent.ConcurrentHashMap;
//序列化工具
public class ProtostuffSerializer {
private Schema<Club> schema = RuntimeSchema.createFrom(Club.class);
public byte[] serialize(final Club club) {
final LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
return serializeInternal(club, schema, buffer);
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
public Club deserialize(final byte[] bytes) {
try {
Club club = deserializeInternal(bytes, schema.newMessage(), schema);
if (club != null ) {
return club;
}
} catch (final Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
return null;
}
private <T> byte[] serializeInternal(final T source, final Schema<T>schema, final LinkedBuffer buffer) {
return ProtostuffIOUtil.toByteArray(source, schema, buffer);
}
private <T> T deserializeInternal(final byte[] bytes, final T result, finalSchema<T> schema) {
ProtostuffIOUtil.mergeFrom(bytes, result, schema);
return result;
}
}
4)测试。
生成序列化工具类:
ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
生成Jedis对象:
Jedis jedis = new Jedis("127.0.0.1", 6379);
序列化:
String key = "club:1";
// 定义实体对象
Club club = new Club(1, "AC", "米兰", new Date(), 1);
// 序列化
byte[] clubBtyes = protostuffSerializer.serialize(club);
jedis.set(key.getBytes(), clubBtyes);
反序列化:
byte[] resultBtyes = jedis.get(key.getBytes());
// 反序列化[id=1, clubName=AC, clubInfo=米兰, createDate=Tue Sep 15 09:53:18 CST 2015, rank=1]
Club resultClub = protostuffSerializer.deserialize(resultBtyes);
总体代码为:
import redis.clients.jedis.Jedis;
import java.util.Date;
public class redisSerializerTest {
public static void main(String[] args) {
ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
Jedis jedis=null;
try {
//1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
jedis = new Jedis("59.110.35.177", 6379);
jedis.auth("1.tiantian");
String key = "club:1";
// 定义实体对象
Club club = new Club(1, "AC", "米兰", new Date(), 1);
// 序列化
byte[] clubBtyes = protostuffSerializer.serialize(club);
jedis.set(key.getBytes(), clubBtyes);
// 反序列化[id=1, clubName=AC, clubInfo=米兰, createDate=Tue Sep 15 09:53:18 CST 2015, rank=1]
byte[] resultBtyes = jedis.get(key.getBytes());
Club resultClub = protostuffSerializer.deserialize(resultBtyes);
}catch (Exception e){
e.printStackTrace();
}finally {
if (jedis != null) {
jedis.close();
}
}
}
}
3.Jedis连接池使用
以上是Jedis的直连方式,所谓直连是指Jedis每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式,如下图所示
因此生产环境中一般使用连接池的方式对Jedis连接进行管理,如下图所示,所有Jedis对象预先放在池子中(JedisPool),每次要连接Redis,只需要在池子中借,用完了在归还给池子。
客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化好Jedis连接,所以每次只需要从Jedis连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新建TCP连接的开销。另外直连的方式无法限制Jedis对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效的保护和控制资源的使用。但是直连的方式也并不是一无是处,表4-1给出两种方式各自的优劣势。
Jedis提供了JedisPool这个类作为对Jedis的连接池,同时使用了Apache的通用对象池工具common-pool作为资源的管理工具,下面是使用JedisPool操作Redis的代码示例:
1)Jedis连接池(通常JedisPool是单例的):
// common-pool连接池配置,这里使用默认配置,后面小节会介绍具体配置说明
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
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 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几个比较常用的属性,表4-2给出了Generic-ObjectPoolConfig其他属性及其含义解释。
总体代码为:
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class jedisPoolTest {
public static void main(String[] args) {
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);
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "59.110.35.177", 6379,200,"1.tiantian");
Jedis jedis = null;
try {
// 1. 从连接池获取jedis对象
jedis = jedisPool.getResource();
// 2. 执行操作
String result = jedis.get("hello");
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
// 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
jedis.close();
}
}
}
}
4.Jedis中Pipeline使用
Jedis支持Pipeline特性,我们知道Redis提供了mget、mset方法,但是并没有提供mdel方法,如果想实现这个功能,可以借助Pipeline来模拟批量删除,虽然不会像mget和mset那样是一个原子命令,但是在绝大数场景下可以使用。下面代码是mdel删除的实现过程。
这里为了节省篇幅,没有写try catch finally,没有关闭jedis。
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对象生成一个pipeline对象,直接可以调用jedis.pipelined()。
- 将del命令封装到pipeline中,可以调用pipeline.del(String key),这个方法和jedis.del(String key)的写法是完全一致的,只不过此时不会真正的执行命令。
- 使用pipeline.sync()完成此次pipeline对象的调用。
除了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);
}
输出结果为:
OK
1
总体代码为:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import java.util.List;
import java.util.Set;
public class JedisPipelineTest {
public static void main(String[] args) {
Jedis jedis=null;
try {
jedis = new Jedis("59.110.35.177", 6379);
jedis.auth("1.tiantian");
Set<String> keys = jedis.keys("*");
// 1)生成pipeline对象
Pipeline pipeline = jedis.pipelined();
// 2)pipeline执行命令,注意此时命令并未真正执行
for (String key : keys) {
pipeline.del(key);
}
// 3)执行命令
pipeline.sync();
pipeline.set("hello", "world");
pipeline.incr("counter");
List<Object> resultList = pipeline.syncAndReturnAll();
for (Object object : resultList) {
System.out.println(object);
}
} catch (Exception e){
e.printStackTrace();
}finally {
if (jedis != null) {
// 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
jedis.close();
} }
}
}
5.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。
以一个最简单的Lua脚本为例子进行说明:
return redis.call('get',KEYS[1])
在redis-cli中执行上面的Lua脚本,方法如下:
127.0.0.1:6379> eval "return redis.call('get',KEYS[1])" 1 hello
"world"
在Jedis中执行,方法如下:
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);
总体来说,Jedis的使用还是比较简单的,重点注意以下几点即可:
1)Jedis操作放在try catch finally里更加合理。
2)区分直连和连接池两种实现方式优缺点。
3)jedis.close()方法的两种实现方式。
4)Jedis依赖了common-pool,有关common-pool的参数需要根据不同的使用场景,各不相同,需要具体问题具体分析。
5)如果key和value涉及了字节数组,需要自己选择适合的序列化方法。
总体代码为:
import redis.clients.jedis.Jedis;
public class JedisLuaTest {
public static void main(String[] args) {
Jedis jedis=null;
try {
//1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信
jedis = new Jedis("59.110.35.177", 6379);
jedis.auth("1.tiantian");
String key = "hello";
String script = "return redis.call('get',KEYS[1])";
Object result = jedis.eval(script, 1, key);
// 打印结果为world
System.out.println(result);
String scriptSha = jedis.scriptLoad(script);
System.out.println(scriptSha);
Object result1 = jedis.evalsha(scriptSha, 1, key);
// 打印结果为world
System.out.println(result1);
}catch (Exception e){
e.printStackTrace();
}finally {
if (jedis != null) {
jedis.close();
}
}
}
}