环境
Spring Boot提供了 spring-boot-starter-data-redis
,使用Spring Data Redis对底层的 Lettuce
或者 Jedis
做了封装,默认使用 Lettuce
。
打开 https://start.spring.io/ ,搜索 redis
,添加依赖 Spring Data Redis (Access+Driver)
,如下图所示:
创建项目 test0501_1
,下载 test0501_1.zip
文件,解压生成项目,并打开。
打开 pom.xml
文件,可见相关的依赖为:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
打开 application.properties
文件,添加Redis的配置信息:
spring.redis.host=localhost
spring.redis.port=6379
Spring Data Redis会自动配置 RedisConnectionFactory
、 StringRedisTemplate
等,可以将其注入到其它组件,比如程序中的Dao组件。
RedisConnectionFactory
,Spring Boot就不会自动配置 RedisConnectionFactory
;RedisTemplate
可额外配置很多个,只要额外配置的 RedisTemplate
的 id
不是 redisTemplate
,Spring Boot依然会自动配置 id
为 redisTemplate
的 RedisTemplate
;StringRedisTemplate
则不同,只要你在容器中配置了类型为 StringRedisTemplate
的Bean,自动配置将不再配置它;另外,如果Spring Boot在类加载路径下找到Apache Commons Pool2依赖库( commons-pool2
),就会自动使用连接池来管理连接。
RedisTemplate
和 StringRedisTemplate
RedisTemplate
和 StringRedisTemplate
提供了一系列 opsForXxx()
方法,返回 XxxOperations
对象,用来操作 Xxx
对象。例如:
opsForValue()
:返回可操作 String
对象的 ValueOperations
对象;opsForList()
:返回可操作 List
对象的 ListOperations
对象;opsForSet()
:返回可操作 Set
对象的 Setoperations
对象;opsForZSet()
:返回可操作 ZSet
对象的 ZsetOperations
对象;opsForHash()
:返回可操作 Hash
对象的 HashOperations
对象;例如:
在 Test05011ApplicationTests.java
文件中,注入 StringRedisTemplate
:
@Autowired
private StringRedisTemplate redisTemplate;
然后创建测试:
@Test
void testRedisTemplate() {
redisTemplate.opsForValue().set("mykey1", "hello");
}
运行测试,就会在Redis数据库中添加 mykey1
,其值为 hello
。
注:此处要用 StringRedisTemplate
,而不能用 RedisTemplate
。否则虽然也能添加key,通过 RedisTemplate
也能查询到,但是在 redis-cli
命令行里查看:
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x06mykey1"
127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x06mykey1"
"\xac\xed\x00\x05t\x00\x05hello"
127.0.0.1:6379>
key和value都是一些奇怪的编码,这是因为 RedisTemplate
默认采用的是JDK的序列化策略。
RedisTemplate
和 StringRedisTemplate
还提供了 execute()
方法,用以实现自定义的Redis操作,其参数是 RedisCallback
接口,我们可使用Lamda形式传入所需的代码逻辑。 RedisCallback
提供了 RedisConnection
/ StringRedisConnection
对象来操作Redis。后面会有具体例子演示通过 execute()
方法来实现自定义查询。
此外, RedisTemplate
和 StringRedisTemplate
还提供了一系列直接操作key的方法,如 delete()
、 move()
、 rename()
等。
首先在 src/main/java
目录下创建POJO对象 Person
:
package com.example.test0501_1.pojo;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@RedisHash("person")
public class Person {
@Id
private Integer id;
private String name;
private String sex;
private Integer age;
private Double weight;
private String remark;
// Getters and Setters ......
public Person(String name, String sex, Integer age, Double weight, String remark) {
this.name = name;
this.sex = sex;
this.age = age;
this.weight = weight;
this.remark = remark;
}
public Person() {
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
", weight=" + weight +
", remark='" + remark + '\'' +
'}';
}
}
其中:
@RedisHash
:表明将POJO类映射到Redis的Hash对象,本例中就是将Java的 Person
类映射到Redis的 person
对象;@Id
:表明这是一个ID字段, @Id
是Spring Data所定义的common规则;接下来在 src/main/java
目录下创建 PersonDao
接口:
package com.example.test0501_1.dao;
import com.example.test0501_1.pojo.Person;
import org.springframework.data.repository.CrudRepository;
public interface PersonDao extends CrudRepository<Person, Integer> {
}
注意: PersonDao
只需扩展 CrudRepository
接口,并不需要实现任何方法。
接下来,我们来编写 PersonDao
的测试代码。
在 src/main/test
目录下创建 PersonDaoTest
测试类,并添加测试:
package com.example.test0501_1.dao;
import com.example.test0501_1.pojo.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class PersonDaoTest {
@Autowired
private PersonDao personDao;
@Test
public void testSave() {
Person person = new Person("Tom", "Male", 30, 70.0, "I am Tom");
personDao.save(person);
}
}
在运行程序之前,首先清空Redis DB。打开 redis-cli
命令行:
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty array)
运行 testSave()
测试方法,测试成功。
查看Redis DB:
127.0.0.1:6379> keys *
1) "person"
2) "person:1644175524"
127.0.0.1:6379>
可见,在Redis数据库中创建了2个key:
"person"
:类型为 set
,存储了所有person对象的ID值;"person:1644175524"
:类型为 hash
,存储了特定person对象的所有信息;查看 person
:
127.0.0.1:6379> type person
set
127.0.0.1:6379> smembers person
1) "1644175524"
127.0.0.1:6379>
可见 person
为 set
类型,其中只有一个元素 "1644175524"
(因为目前只创建了一个person),这就是刚才所创建的person对象的ID值,该值是系统自动生成的,因为代码中并没有指定。
注:如果在程序中指定ID值,则Redis会使用该值作为ID。
查看 "person:1644175524"
:
127.0.0.1:6379> type person:1644175524
hash
127.0.0.1:6379> hgetall person:1644175524
1) "_class"
2) "com.example.test0501_1.pojo.Person"
3) "age"
4) "30"
5) "id"
6) "1644175524"
7) "name"
8) "Tom"
9) "remark"
10) "I am Tom"
11) "sex"
12) "Male"
13) "weight"
14) "70.0"
127.0.0.1:6379>
可见,它的命名为 person:
,记录了所指定ID值的person对象的所有信息。
如果是查询某个ID值,那么 CrudRepository
已经提供了相应的 findById()
方法,可以直接使用。
在 PersonDaoTest.java
中添加测试:
@Test
public void testFindById() {
personDao.findById(1644175524).ifPresent(System.out::println);
}
运行测试,输出结果如下:
Person{id=1644175524, name='Tom', sex='Male', age=30, weight=70.0, remark='I am Tom'}
如果想要查询某个非ID字段的值,就需要 @Indexed
注解了。比如,在 name
、 age
、 remakr
上添加 @Indexed
注解:
......
@Indexed
private String name;
private String sex;
@Indexed
private Integer age;
private Double weight;
@Indexed
private String remark;
......
先清空数据库,然后运行测试程序 testSave()
,最后查看数据库:
127.0.0.1:6379> keys *
1) "person"
2) "person:-745517744"
3) "person:-745517744:idx"
4) "person:name:Tom"
5) "person:age:30"
6) "person:remark:I am Tom"
127.0.0.1:6379>
注:顺序是打乱的,为了方便阅读,我把顺序重新调整了一下。
1和2不用说了,3、4、5、6又是什么鬼?
127.0.0.1:6379> type person:-745517744:idx
set
127.0.0.1:6379> smembers person:-745517744:idx
1) "person:remark:I am Tom"
2) "person:name:Tom"
3) "person:age:30"
127.0.0.1:6379>
可见,命名为 person:
的key,比如上面的 "person:-745517744:idx"
,它是一个 set
,记录了所有索引列的信息。
每个索引列,也都会创建一个对应的key,命名为 person:<列名>:<列值>
,比如 "person:name:Tom"
。如下:
127.0.0.1:6379> type "person:name:Tom"
set
127.0.0.1:6379> smembers "person:name:Tom"
1) "-745517744"
127.0.0.1:6379>
"person:name:Tom"
是一个 set
,记录了“所有 name
为 Tom
的person的ID值”。本例中只有一个ID值,假设多个person对象(ID值不同)都包含了 "person:name:Tom"
属性,那么从该key就能直接查询到那些ID值。这就是“索引”的意义。
我们知道,Redis数据库只支持“键值对”存储,相对比较简单,无法支持过滤条件查询。比如前面我们看到的, "person:1644175524"
是一个 hash
,里面记录了该person的所有信息,但是我们无法直接通过查询其 name
属性来做过滤。因此,Redis对每个“索引列的值”单独创建一个key,这样我们就可以通过该列来查询了。
比如,要查找 person
对象的 name
属性为 Tom
的对象,具体方法为:
person:<列名>:<列值>
的命名规则,拼出key "person:name:Tom"
;"person:name:Tom"
(它是一个 set
),获取其对应的所有ID值,本例中只找到一个ID值 "-745517744"
;person:
的命名规则,拼出key "person:-745517744"
;"person:-745517744"
(它是一个 hash
),获取该对象的所有属性值;当然,这些操作细节是Spring Data Redis自动实现的,不需要我们参与。
在 PersonDao.java
中添加如下代码:
......
List<Person> findByName(String name);
List<Person> findByAge(Integer age);
List<Person> findByRemark(String remark);
......
同样,无需编写代码实现。
在 PersonDaoTest.java
中添加测试方法:
@Test
public void testFindByName() {
personDao.findByName("Tom").forEach(System.out::println);
}
运行测试,输出结果如下:
Person{id=-745517744, name='Tom', sex='Male', age=30, weight=70.0, remark='I am Tom'}
findByAge()
和 findByRemark()
也同理,不再赘述。
貌似没有简单方法可以实现。参见下面的“复杂查询”。
在查询的基础上,同样可以通过 save()
方法来更新对象。
在 PersonDaoTest.java
中添加测试方法:
@Test
public void testUpdate() {
personDao.findByName("Tom").forEach(e -> {
e.setAge(e.getAge() + 1);
personDao.save(e);
});
}
运行测试,然后通过 redis-cli
命令行查看:
127.0.0.1:6379> keys *
1) "person:age:31"
2) "person:name:Tom"
3) "person:-745517744"
4) "person:remark:I am Tom"
5) "person"
6) "person:-745517744:idx"
127.0.0.1:6379> hgetall "person:-745517744"
1) "_class"
2) "com.example.test0501_1.pojo.Person"
3) "age"
4) "31"
5) "id"
6) "-745517744"
7) "name"
8) "Tom"
9) "remark"
10) "I am Tom"
11) "sex"
12) "Male"
13) "weight"
14) "70.0"
127.0.0.1:6379>
可见,被索引的key "person:age:30"
已经变成了 "person:age:31"
。同样, "person:-745517744"
里面的 age
也从 30
变成了 31
。
如果要按ID删除,那么 CrudRepository
已经提供了相应的 deleteById()
方法,可以直接使用。
在 PersonDaoTest.java
中添加测试:
@Test
public void testDeleteById() {
personDao.deleteById(-745517744);
}
运行测试,然后通过 redis-cli
命令行查看:
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>
可见,确实删除了。
如果要按索引字段删除,可以在查询索引字段的基础上,通过 delete()
或者 deleteById()
方法来删除对象。
在 PersonDaoTest.java
中添加测试:
@Test
public void testDeleteByName() {
personDao.findByName("Tom").forEach(e -> personDao.delete(e));
// personDao.findByName("Tom").forEach(e -> personDao.deleteById(e.getId()));
}
先运行 testSave()
方法创建一个person对象,然后运行 testDeleteByName()
方法,就可以把它删除。
注:代码中提供了两种方法,都可以删除对象。
同理,如果要按非索引字段删除,可以在查询非索引字段的基础上,通过 delete()
或者 deleteById()
方法来删除对象。
注意:非索引字段查询较为复杂,参见下面的“复杂查询”。
对于更加复杂的查询,需要使用 StringRedisTemplate
来自定义Redis操作。因此,需要:
在 dao
目录下创建 PersonCustomDao
接口:
package com.example.test0501_1.dao;
public interface PersonCustomDao {
}
同时创建其实现类 PersonCustomDaoImpl
,注入 StringRedisTemplate
:
package com.example.test0501_1.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
public class PersonCustomDaoImpl implements PersonCustomDao{
@Autowired
private StringRedisTemplate redisTemplate;
}
然后修改 PersonDao.java
文件,让 PersonDao
接口扩展 PersonCustomDao
接口:
......
public interface PersonDao extends CrudRepository<Person, Integer>, PersonCustomDao {
......
要实现自定义的复杂查询,只需在 PersonCustomDao
接口中声明方法,并在 PersonCustomDaoImpl
类实现即可,被注入的 PersonDao
会自动继承这个方法实现。
我们先在Redis数据库中添加一些测试数据。修改 testSave()
方法如下:
@Test
public void testSave() {
Person person = new Person("Tom", "Male", 30, 70.0, "I am Tom");
personDao.save(person);
person = new Person("Jerry", "Male", 15, 30.0, "I am Jerry");
personDao.save(person);
}
先清空数据库,然后运行 testSave()
测试,数据库中将会创建 Tom
和 Jerry
两个对象:
127.0.0.1:6379> keys *
1) "person"
2) "person:1455268875"
3) "person:1455268875:idx"
4) "person:name:Tom"
5) "person:age:30"
6) "person:remark:I am Tom"
7) "person:511977840"
8) "person:511977840:idx"
9) "person:name:Jerry"
10) "person:age:15"
11) "person:remark:I am Jerry"
127.0.0.1:6379>
查询所有年龄大于等于20岁的人。
在 PersonCustomDao
接口中添加方法:
public List<Person> findByAgeGt(Integer age);
在 PersonCustomDaoImpl
类中实现该方法。具体思路为:
age
是索引字段,所以可以通过 keys "person:age:*"
命令获取所有命名为 "person:age:"
的key,这是一个Set集合;"person:age:"
元素,解析出来
,然后再过滤出大于等于20的,得到过滤后的Set集合;"person:age:"
元素,访问Redis数据库中命名为 "person:age:"
的key,获取其对应ID值的Set集合。这样,就能得到所有符合条件的ID值集合;person:
的key,得到name、age、weight等全部信息,以此来创建Person对象,最后把该Person对象加到指定的List里;具体代码如下,在 PersonCustomDaoImpl.java
文件中实现 findByAgeGt()
方法:
@Override
public List<Person> findByAgeGt(Integer comparedAge) {
List<Person> result = new ArrayList<>();
Set<String> ageSet = redisTemplate.keys("person:age:*");
Set<String> idSet = ageSet.stream()
.filter(e -> Integer.parseInt(((String) e).substring("person:age:".length())) >= comparedAge) // filter age
.map(e -> redisTemplate.opsForSet().members(e)) // get ID
.flatMap(Set::stream) // Stream of ID Set -> Stream of ID
.collect(Collectors.toSet());
idSet.stream().forEach(e -> {
Integer id = Integer.parseInt(e);
String name = (String) redisTemplate.opsForHash().get("person:" + e, "name");
String sex = (String) redisTemplate.opsForHash().get("person:" + e, "sex");
Integer age = Integer.parseInt((String) redisTemplate.opsForHash().get("person:" + e, "age"));
double weight = Double.parseDouble((String) redisTemplate.opsForHash().get("person:" + e, "weight"));
String remark = (String) redisTemplate.opsForHash().get("person:" + e, "remark");
Person person = new Person(name, sex, age, weight, remark);
person.setId(id);
result.add(person);
});
return result;
}
注意代码中的 flatMap()
操作。 ageSet
中的每个元素map到一个 idSet
,最后通过 flatMap()
方法将流扁平化。
在 PersonDaoTest.java
添加测试:
@Test
public void testFindByAgeGt() {
personDao.findByAgeGt(20).forEach(System.out::println);
}
运行测试,输出结果如下:
Person{id=1455268875, name='Tom', sex='Male', age=30, weight=70.0, remark='I am Tom'}
可见,只有满足条件的 Tom
被查询出来了。
注:对于索引字段,当然也可以按下面的非索引字段的方法来查询。二者区别在于:
性能方面,如果对象的数量不多,也没有什么大字段,那么非索引查询(直接遍历所有ID)也许性能反而好一些。但如果对象数量非常多,或者包含了大字段,那么要尽量避免遍历所有ID,使用索引查询应该会好一些。
查询所有体重大于等于25公斤的人。
在 PersonCustomDao
接口中添加方法:
public List<Person> findByWeightGt(Double weight);
在 PersonCustomDaoImpl
类中实现该方法。具体思路为:
person
,得到所有ID值,这是一个Set集合;person:
的key,得到其所有信息;weight
属性值大于等于25,则通过name、age、weight等属性值,创建Person对象,并把该Person对象加到指定List里,否则直接略过;具体代码如下,在 PersonCustomDaoImpl.java
文件中实现 findByWeightGt()
方法:
@Override
public List<Person> findByWeightGt(Double comparedWeight) {
List<Person> result = redisTemplate.execute((RedisCallback<? extends List<Person>>) connection -> {
List<Person> innerResult = new ArrayList<>();
Set<String> idSet = ((StringRedisConnection)connection).sMembers("person");
idSet.stream().forEach(e -> {
Map<String, String> map = ((StringRedisConnection) connection).hGetAll("person:" + e);
String obj = map.get("weight");
if (obj != null) {
double weight = Double.parseDouble(map.get("weight"));
if (weight >= comparedWeight) {
Integer id = Integer.parseInt(e);
String name = map.get("name");
String sex = map.get("sex");
Integer age = Integer.parseInt(map.get("age"));
String remark = map.get("remark");
Person person = new Person(name, sex, age, weight, remark);
person.setId(id);
innerResult.add(person);
}
}
});
return innerResult;
});
return result;
}
注意,该方法使用了 RedisTemplate
的 execute()
方法,其参数是一个 RedisCallback
,在回调中可以拿到 RedisConnection
(需要强转为 StringRedisConnection
),该connection提供了 sMembers()
、 hGetAll()
等方法与Redis数据库交互。
当然,如果不使用 RedisTemplate
的 execute()
方法,而直接使用 RedisTemplate
提供的 opsForXxx()
API方法,也能实现本方法所需的代码逻辑,请各位自行实现。
在 PersonDaoTest.java
添加测试:
@Test
public void testFindByWeightGt() {
personDao.findByWeightGt(25.0).forEach(System.out::println);
}
运行测试,结果如下:
Person{id=511977840, name='Jerry', sex='Male', age=15, weight=30.0, remark='I am Jerry'}
Person{id=1455268875, name='Tom', sex='Male', age=30, weight=70.0, remark='I am Tom'}
可见, Tom
和 Jerry
都满足条件,因此都被查询出来了。
注:本例代码中有一些不严谨的地方,比如没有判断NULL值等,使用时需要注意。
@TimeToLive
该注解用于指定对象的生存时间。
修改 Person.java
文件,添加代码如下:
@TimeToLive
private Long timeout;
// Getters and Setters ......
修改 PersonDaoTest.java
文件的 testSave()
方法,将 timeout
设置为60秒。如下:
@Test
public void testSave() {
Person person = new Person("Tom", "Male", 30, 70.0, "I am Tom");
person.setTimeout(60L);
personDao.save(person);
}
清空数据库,运行测试,然后通过 redis-cli
命令行查看:
127.0.0.1:6379> keys *
1) "person"
2) "person:1511578791"
3) "person:1511578791:idx"
4) "person:name:Tom"
5) "person:age:30"
6) "person:remark:I am Tom"
127.0.0.1:6379>
过一分钟后,再次查看:
127.0.0.1:6379> keys *
1) "person"
2) "person:1511578791:idx"
3) "person:name:Tom"
4) "person:age:30"
5) "person:remark:I am Tom"
127.0.0.1:6379>
可见,key "person:1511578791"
不见了。
但是,其它的key却仍然存在,它们岂不是都成了孤儿?
更何况,现在通过 deleteById()
或者 deleteByName()
方法也无法删除这些key了……
增 | 删 | 改 | 查(简单查询,比如 =) | 查(复杂查询,比如 >=) | |
---|---|---|---|---|---|
By ID | save() |
deleteById() 或者 delete() |
findById() + save() |
findById() |
复杂查询 |
By索引字段 | save() |
findByXxx() + deleteById 或者 delete() |
findByXxx() + save() |
findByXxx() |
复杂查询 |
By非索引字段 | save() |
复杂查询 + deleteById 或者 delete() |
复杂查询 + save() |
复杂查询 | 复杂查询 |