一、环境配置
- IntelliJ IDEA版本:IntelliJ IDEA 2019.1 x64
- Maven版本:apache-maven-3.6.1
- Redis:redis-5.0.5
二、Java项目创建
创建一个quickstart模板的Maven项目,导入jedis的maven依赖:
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
由于没有实际的项目可以供我操作,所以我找了一篇英文小说,通过java将其分词进行了一个wordcount过程,将获得的wordcount结果存储在了一个HashMap中,结构为{key:单词,value:出现次数}。wordcount过程代码如下:
private HashMap<String,Integer> wordCountMap=null;
public HashMap<String,Integer> wordsSplit(String absolutePath) throws IOException {
wordCountMap=new HashMap<>();
BufferedReader bufferedReader=null;
try {
bufferedReader=new BufferedReader(new FileReader(new File(absolutePath)));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
String readLine=null;
while((readLine=bufferedReader.readLine())!=null){
String[] lines=readLine.split("\\s+");
for(String line:lines){
if(wordCountMap.containsKey(line)){
wordCountMap.put(line,wordCountMap.get(line)+1);
}
else{
wordCountMap.put(line,1);
}
}
}
bufferedReader.close();
return wordCountMap;
}
三、创建Java与Redis的连接
通过Jedis,我分别通过下述三种方法创建java与Redis的连接:
- 通过Jedis类直接连接。
- 通过JedisPool类创建jedis连接池,再从连接池中获取jedis连接。
-
通过ShardedJedisPool类创建分布式jedis连接池,再从连接池中获取jedis连接。
我们分别来看这三种连接方式的实现方法:
1.通过Jedis类直接连接
由于我的Redis服务器是有密码的,所以先创建一个JedisShardInfo对象,装载着Redis服务器的地址、端口号和密码,通过该对象来实例化一个jedis连接。
/**
* 普通jedis连接
*/
private static Jedis jedis=null;
JedisShardInfo jedisShardInfo=new JedisShardInfo("192.168.184.133",6379);
jedisShardInfo.setPassword("123456");
jedis=new Jedis(jedisShardInfo);
2.通过JedisPool类创建jedis连接池,再从连接池中获取jedis连接
首先创建一个连接池配置对象JedisPoolConfig,通过该对象和jedis服务器的地址、端口号、密码和设置的超时时间来创建一个连接池对象,然后可以通过连接池对象的getResource方法获得一个jedis连接对象。
/**
*jedis连接池
*/
private static JedisPool jedisPool=null;
JedisPoolConfig config=new JedisPoolConfig();
config.setMaxTotal(100);//最大连接数
config.setMaxIdle(50);//最大空闲连接
config.setMaxWaitMillis(3000);//最大等待时间
config.setTestOnBorrow(true);//获取连接时检测其有效性
config.setTestOnReturn(true);
jedisPool=new JedisPool(config,"192.168.184.133",6379,3000,"123456");
//通过jedispool获取jedis连接
Jedis jedis=jedisPool.getResource();
3.通过ShardedJedisPool类创建分布式jedis连接池,再从连接池中获取jedis连接
如果部署的Redis服务器时通过集群方式来提供服务的,则需要通过分布式jedis连接池来获取jedis连接对象。如下方代码所示,如果是分布式Redis服务,则多创建几个JedisShardInfo将其加入到List中来实例化ShardedJedisPool对象。与JedisPool 相同,ShardedJedisPool对象也可以通过getResource()方法获取jedis连接对象。
/**
* 分布式Jedis池
*/
private static ShardedJedisPool pool=null;
JedisPoolConfig config=new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(50);//最大空闲连接
config.setMaxWaitMillis(3000);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
JedisShardInfo jedisShardInfo=new JedisShardInfo("192.168.184.133",6379);
jedisShardInfo.setPassword("123456");
//如果这里是集群则多创建几个JedisShardInfo
List<JedisShardInfo> list=new LinkedList<>();
list.add(jedisShardInfo);
pool=new ShardedJedisPool(config,list);
四、通过Jedis操作Redis数据库
1.Jedis操作String类型键值对
①插入数据
我将上述wordcount过程的结果直接插入到redis数据库中,下面由两种不太相同的插入方法,分别是逐条插入和基于事务的统一插入方法:
逐条插入:
/**
* Jedis方法,向redis中插入类型为String的键值对,运行时间12211ms
* @param wordCountMap
* @return
*/
public boolean jedisInsert(HashMap<String ,Integer> wordCountMap){
// 这里也可以通过JedisPool来获取jedis
// JedisPool jedisPool=RedisUtil.getJedisPool();
// Jedis jedis=jedisPool.getResource();
Jedis jedis= RedisUtil.getJedis();
Set<String> keys=wordCountMap.keySet();
for(String key:keys){
jedis.set(key,wordCountMap.get(key).toString());
}
return true;
}
基于事务统一插入:
/**
* Jedis通过事务实现向redis中插入类型为String的键值对,运行时间273ms
* @param wordCountMap
* @return
*/
public boolean jedisTransactionInsert(HashMap<String ,Integer> wordCountMap){
Jedis jedis= RedisUtil.getJedis();
Set<String> keys=wordCountMap.keySet();
Transaction transaction=jedis.multi();
for(String key:keys){
transaction.set(key,wordCountMap.get(key).toString());
}
transaction.exec();
return true;
}
这两种插入方法的执行结果并无不同,但执行时间差距巨大,逐条插入为12s左右,基于事务的仅为0.3s不到。这充分的提现了 Redis事务的特点:
事务可以一次执行多个命令, 并且带有以下两个重要的保证:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
但Redis的事务不支持回滚,如果一条命令发生了错误,Redis会接着执行余下的命令。
所以我们使用Redis事务的意义在于可以明显的提升Redis命令的执行效率。
②删除数据
这里我调用了基于事务的删除操作,批量的删除了出现次数小于3次的单词。
/**
* 基于事务的方法,将redis库中value小于3的删除掉
* @param wordCountMap
*/
public void jedisTransactionDelete(HashMap<String ,Integer> wordCountMap){
int deleteCounter=0;
Jedis jedis= RedisUtil.getJedis();
Set<String> keys=wordCountMap.keySet();
Transaction transaction=jedis.multi();
for(String key:keys){
if(wordCountMap.get(key)<3){
transaction.del(key);//将删除命令加入到事务队列中
deleteCounter++;
}
}
System.out.println("即将删除"+deleteCounter+"项数据!");
transaction.exec();//执行删除命令
}
③查询数据
对于查询操作,与之前的增删操作有所不同,前面说过事务中的所有命令都会序列化、按顺序地执行,所以每一次的transaction.get(key)过程并不会获得redis数据库汇总真实的值,而是在执行过 transaction.exec()后统一进行查询操作,所以这里要用到一个Response<String> 对象来接受每一次查询的虚拟结果(类似一个指针,不过在未执行exec之前是空的,在执行了exec之后才会填充这个指针)。
/**
* 基于事务的方法,查询jedis库中value大于200的值
* @param wordCountMap
*/
public void jedisTransactionSearch(HashMap<String ,Integer> wordCountMap){
Jedis jedis= RedisUtil.getJedis();
Set<String> keys=wordCountMap.keySet();
Transaction transaction=jedis.multi();
List<Response<String>> responses=new LinkedList<>();//用于存储每一次查询的返回值
for(String key:keys){
Response<String> value=transaction.get(key);
responses.add(value);//存储
}
transaction.exec();
for(Response<String> response:responses){
if(200 < Integer.parseInt(response.get())){
System.out.println(Integer.parseInt(response.get()));
}
}
}
2.Jedis操作Set类型键值对
为了测试Set类型数据的插入,我将之前wordcount的数据稍作修改,将所有出现次数相同的word放到一个Set里,在以出现次数为键,Set为值存储到一个HashMap中。
HashMap<Integer, Set<String>> setHashMap=new HashMap<>();
Set<String> keys=wordCountMap.keySet();
for(String key:keys){
if(setHashMap.containsKey(wordCountMap.get(key))){
setHashMap.get(wordCountMap.get(key)).add(key);
} else {
Set<String> set=new HashSet<>();
set.add(key);
setHashMap.put(wordCountMap.get(key),set);
}
}
①插入数据
首先我们来看以下set集合插入函数的源码,第二个value的参数是String ...类型的,该类型是不定长度字符串,我们可以直接给他一个数组作为参数。在下方的代码中,我通过java Set接口的toArray方法将集合转换成了一个字符串数组,作为sadd的参数传给了事务操作。
/**
* 基于事务的方法,向redis中插入类型为Set的键值对
* @param wordCountMap
* @return
*/
public boolean jedisTransactionInsert(HashMap<Integer, Set<String>> wordCountMap){
Jedis jedis=RedisUtil.getJedis();
Transaction transaction=jedis.multi();
Set<Integer> keys=wordCountMap.keySet();
for(Integer key:keys){
String []strs = new String[wordCountMap.get(key).size()];
wordCountMap.get(key).toArray(strs);
transaction.sadd(key.toString(),strs);
}
transaction.exec();
return true;
}
②查询数据
通过调用事务(如果不使用事务jedis也有该方法)的smembers方法,可以获取key值对应的Set的全部内容。
/**
* 基于事务的方法,查询类型为set的数据,并获取每个set所有成员项
* @param wordCountMap
*/
public void jedisTransactionSearch(HashMap<Integer, Set<String>> wordCountMap){
Jedis jedis=RedisUtil.getJedis();
Transaction transaction=jedis.multi();
Set<Integer> keys=wordCountMap.keySet();
List<Response<Set<String>>> responses=new LinkedList<>();
for(Integer key:keys){
Response<Set<String>> response=transaction.smembers(key.toString());
responses.add(response);
}
transaction.exec();
for(Response<Set<String>> response:responses){
for(String str:response.get()){
System.out.print(str+" ");
}
System.out.println();
}
}
3.Jedis操作List类型键值对
①插入数据
与set集合类型数据的操作类似,list类型的数据可以通过lpush或rpush进行数据的插入。
public boolean jedisTransactionInsert(HashMap<Integer, Set<String>> wordCountMap){
Jedis jedis= RedisUtil.getJedis();
Transaction transaction=jedis.multi();
Set<Integer> keys=wordCountMap.keySet();
for(Integer key:keys){
String []strs = new String[wordCountMap.get(key).size()];
wordCountMap.get(key).toArray(strs);
transaction.lpush(key.toString(),strs);
}
transaction.exec();
return true;
}
②查询数据
而对于list的查询操作,如果使用lpop或rpop不太容易一次性将一个list中的数据全部获取出来,这里可以使用lrange方法,由于这是在事务中,我们一般不知道list中的元素个数 (这时候是没法调用llen()方法的) ,所以无法指定长度,于是我们用-1代表list中的全部数据。
public void jedisTransactionSearch(HashMap<Integer, Set<String>> wordCountMap){
Jedis jedis= RedisUtil.getJedis();
Transaction transaction=jedis.multi();
Set<Integer> keys=wordCountMap.keySet();
List<Response<List<String>>> responses=new LinkedList<>();
for(Integer key:keys){
//System.out.println(jedis.llen(key.toString()));会报错JedisDataException: Cannot use Jedis when in Multi.
Response<List<String>> response=transaction.lrange(key.toString(),0,-1);
responses.add(response);
}
transaction.exec();
for(Response<List<String>> response:responses){
for(String str:response.get()){
System.out.print(str+" ");
}
System.out.println();
}
}
4.Jedis操作Hash类型键值对
为了准备验证操作Hash类型数据是否成功,我们先构造一个键和值均为String类型的HashMap,我构建的该HashMap是以word出现次数n为键,以出现n次的单词个数为值的。
HashMap<String, String> hashHashMap=new HashMap<>();
Set<String> keys=wordCountMap.keySet();
for(String key:keys){
if(hashHashMap.containsKey(wordCountMap.get(key).toString())){
hashHashMap.put(wordCountMap.get(key).toString(),""+(Integer.parseInt(hashHashMap.get(wordCountMap.get(key).toString()))+1));
} else {
hashHashMap.put(wordCountMap.get(key).toString(),"1");
}
}
①插入数据
调用事务的hmset方法,将一个HashMap对象作为参数,向Redis中插入一个hash类型的数据。
/**
* 基于事务的方法,向redis中插入类型为Hash的键值对
* @param wordCountMap
* @return
*/
public boolean jedisTransactionInsert(HashMap<String, String> wordCountMap){
Jedis jedis= RedisUtil.getJedis();
Transaction transaction=jedis.multi();
transaction.hmset("hash",wordCountMap);
transaction.exec();
return true;
}
②查询数据
直接调用事务的hgetAll方法既可获取到一个hash类型对象。
public void jedisTransactionSearch(HashMap<String, String> wordCountMap){
Jedis jedis= RedisUtil.getJedis();
Transaction transaction=jedis.multi();
transaction.hgetAll("hash");
transaction.exec();
}