Spring Boot的Spring Data Redis和RedisTemplate(StringRedisTemplate)

环境

  • 操作系统:Ubuntu 20.04
  • Redis:6.2.6
  • 开发工具:IntelliJ IDEA 2022.1 (Community Edition)

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会自动配置 RedisConnectionFactoryStringRedisTemplate 等,可以将其注入到其它组件,比如程序中的Dao组件。

  • 如果配置了自定义的 RedisConnectionFactory ,Spring Boot就不会自动配置 RedisConnectionFactory
  • RedisTemplate 可额外配置很多个,只要额外配置的 RedisTemplateid 不是 redisTemplate ,Spring Boot依然会自动配置 idredisTemplateRedisTemplate
  • StringRedisTemplate 则不同,只要你在容器中配置了类型为 StringRedisTemplate 的Bean,自动配置将不再配置它;

另外,如果Spring Boot在类加载路径下找到Apache Commons Pool2依赖库( commons-pool2 ),就会自动使用连接池来管理连接。

RedisTemplateStringRedisTemplate

RedisTemplateStringRedisTemplate 提供了一系列 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的序列化策略。

RedisTemplateStringRedisTemplate 还提供了 execute() 方法,用以实现自定义的Redis操作,其参数是 RedisCallback 接口,我们可使用Lamda形式传入所需的代码逻辑。 RedisCallback 提供了 RedisConnection / StringRedisConnection 对象来操作Redis。后面会有具体例子演示通过 execute() 方法来实现自定义查询。

此外, RedisTemplateStringRedisTemplate 还提供了一系列直接操作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> 

可见 personset 类型,其中只有一个元素 "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

如果是查询某个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 注解了。比如,在 nameageremakr 上添加 @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::idx 的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 ,记录了“所有 nameTom 的person的ID值”。本例中只有一个ID值,假设多个person对象(ID值不同)都包含了 "person:name:Tom" 属性,那么从该key就能直接查询到那些ID值。这就是“索引”的意义。

我们知道,Redis数据库只支持“键值对”存储,相对比较简单,无法支持过滤条件查询。比如前面我们看到的, "person:1644175524" 是一个 hash ,里面记录了该person的所有信息,但是我们无法直接通过查询其 name 属性来做过滤。因此,Redis对每个“索引列的值”单独创建一个key,这样我们就可以通过该列来查询了。

比如,要查找 person 对象的 name 属性为 Tom 的对象,具体方法为:

  1. 通过 person:<列名>:<列值> 的命名规则,拼出key "person:name:Tom"
  2. 查询key "person:name:Tom" (它是一个 set ),获取其对应的所有ID值,本例中只找到一个ID值 "-745517744"
  3. 通过 person: 的命名规则,拼出key "person:-745517744"
  4. 查询key "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删除

如果要按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() 测试,数据库中将会创建 TomJerry 两个对象:

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 类中实现该方法。具体思路为:

  1. 因为 age 是索引字段,所以可以通过 keys "person:age:*" 命令获取所有命名为 "person:age:" 的key,这是一个Set集合;
  2. 遍历Set中每个 "person:age:" 元素,解析出来 ,然后再过滤出大于等于20的,得到过滤后的Set集合;
  3. 遍历Set中每个 "person:age:" 元素,访问Redis数据库中命名为 "person:age:" 的key,获取其对应ID值的Set集合。这样,就能得到所有符合条件的ID值集合;
  4. 遍历每个ID值,访问Redis数据库中命名为 person: 的key,得到name、age、weight等全部信息,以此来创建Person对象,最后把该Person对象加到指定的List里;
  5. 最终,该List就包含了所有满足条件的Person对象;

具体代码如下,在 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值,然后再做过滤;

性能方面,如果对象的数量不多,也没有什么大字段,那么非索引查询(直接遍历所有ID)也许性能反而好一些。但如果对象数量非常多,或者包含了大字段,那么要尽量避免遍历所有ID,使用索引查询应该会好一些。

查询非索引字段

查询所有体重大于等于25公斤的人。

PersonCustomDao 接口中添加方法:

    public List<Person> findByWeightGt(Double weight);

PersonCustomDaoImpl 类中实现该方法。具体思路为:

  1. 查询 person ,得到所有ID值,这是一个Set集合;
  2. 遍历Set中所有的ID值,访问Redis数据库中命名为 person: 的key,得到其所有信息;
  3. 如果 weight 属性值大于等于25,则通过name、age、weight等属性值,创建Person对象,并把该Person对象加到指定List里,否则直接略过;
  4. 最终,该List就包含了所有满足条件的Person对象;

具体代码如下,在 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;
    }

注意,该方法使用了 RedisTemplateexecute() 方法,其参数是一个 RedisCallback ,在回调中可以拿到 RedisConnection (需要强转为 StringRedisConnection ),该connection提供了 sMembers()hGetAll() 等方法与Redis数据库交互。

当然,如果不使用 RedisTemplateexecute() 方法,而直接使用 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'}

可见, TomJerry 都满足条件,因此都被查询出来了。

注:本例代码中有一些不严谨的地方,比如没有判断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() 复杂查询 复杂查询

你可能感兴趣的:(DB,Java,spring,boot,spring,boot,redis,spring)