Spring Boot 2.x 缓存应用 Redis注解与非注解方式入门教程

Redis 在 Spring Boot 2.x 中相比 1.5.x 版本,有一些改变。redis 默认链接池,1.5.x 使用了 jedis,而2.x 使用了 lettuce

Redis 接入 Spring Boot 缓存,使用的注解跟 Ehcache 接入缓存的注解是一样的,Spring Boot 缓存应用 Ehcache 入门教程

安装 Redis 请参见 如何在 Mac 下安装 Redis 和 如何在 Window 下安装 Redis

本文仅仅适用

  • spring boot 2.x
  • redis
  • jdk 1.8+

本项目源码下载

0 注意

本章代码与 Spring Boot 缓存应用 Ehcache 入门教程 基本是相同的,不同的是 Ehcache 换成了 Redis。有几点需要注意

  • 本示例代码是基于 Spring Boot 2.1.6,Redis for Spring Boot 2.x 的配置与 Spring Boot 1.5.x 配置有细微的差别,主要是 Redis 默认连接池区别。
  • 实体类 UserDO 必须要求支持序列化,继承与 implements Serializable
  • Redis 没有进行 xml 配置文件配置,使用了 @Configuration 进行配置

1 新建 Spring Boot Maven 示例工程项目

注意:是用来 IDEA 开发工具

  1. File > New > Project,如下图选择 Spring Initializr 然后点击 【Next】下一步
  2. 填写 GroupId(包名)、Artifact(项目名) 即可。点击 下一步
    groupId=com.fishpro
    artifactId=redis
  3. 选择依赖 Spring Web Starter 前面打钩。
  4. 项目名设置为 spring-boot-study-redis.

2 引入依赖 Pom

  • fastjson
  • redis
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.44</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

3 Redis 使用注解实现缓存

使用注解方式,不需要做配置也可以运行示例,我们这里不用编写配置文件,只要在启动类上加上 @EnableCaching 注解就可以使用

3.1 示例代码

整个业务代码包括了
模拟一个增删改查来设置缓存示例,包括了

  • UserService 用户服务类
  • UserServiceImpl 用户服务实现类
  • UserController 用户控制层类使用了 RestController

UserService 用户接口(路径 src/main/java/com/fishpro/redis/service/UserService.java)

public interface UserService {

    List<UserDO> list();
    UserDO get(Integer id);
    UserDO save(UserDO user);
    UserDO update(UserDO user);
    void delete(Integer id);

}

UserServiceImpl(路径 src/main/java/com/fishpro/redis/service/impl/UserServiceImpl.java)

// fpcache 对应 ehcache.xml 中的 fpcache 节点
@CacheConfig(cacheNames = {"fpcache"})
@Service
public class UserServiceImpl implements UserService {
    @Override
    @Cacheable("fpcache")
    public List<UserDO> list() {
        List<UserDO> list=new ArrayList<>();
        list.add( new UserDO(1,"fishpro","123456",1));
        list.add( new UserDO(2,"fishpro2","123456",1));
        list.add( new UserDO(3,"fishpro3","123456",1));
        System.out.println("获取用户列表使用 @Cacheable 注意执行第二次的时候不会有本语句输出了,部分删除掉缓存");
        return list;
    }

    @Override
    @Cacheable(value = "fpcache",key = "#id")
    public UserDO get(Integer id) {
        System.out.println("获取单个用户 get user by"+id);
        return new UserDO(1,"fishpro","123456",1);

    }

    @Override
    @CachePut(value = "fpcache",key = "#user.id")
    public UserDO save(UserDO user) {
        System.out.println("保存用户使用 @CachePut 每次都会执行语句并缓存 save user by "+user.getUserName());
        return user;

    }

    @Override
    @CachePut(value = "fpcache",key = "#user.id")
    public UserDO update(UserDO user) {
        System.out.println("更新用户使用 @CachePut 每次都会执行语句并缓存 update user by "+user.getUserName());
        return user;

    }

    @Override
    @CacheEvict(allEntries = true)
    public void delete(Integer id) {
        System.out.println("删除用户根据用户ID,如果 allEntries = true 则不论 key 是啥都全部删除缓存"+id);
    }
}

UserController(路径 src/main/java/com/fishpro/redis/controller/UserController.java)


@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/test")
    public String testCache(){
        System.out.println("============以下第一次调用 ================");
        userService.list();
        userService.get(1);
        userService.save(new UserDO(1,"fishpro","123456",1));
        userService.update(new UserDO(1,"fishpro","123456434",1));

        System.out.println("============以下第二次调用 观察 list 和 get 方法 ================");

        userService.list();
        userService.get(1);
        userService.save(new UserDO(1,"fishpro","123456",1));
        userService.update(new UserDO(1,"fishpro","123456434",1));


        System.out.println("============以下第三次调用 先删除 观察 list 和 get 方法 ================");
        userService.delete(1);
        userService.list();
        userService.get(1);
        userService.save(new UserDO(1,"fishpro","123456",1));
        userService.update(new UserDO(1,"fishpro","123456434",1));
        return  "";
    }
}

3.2 运行

右键 RedisApplication 选择 Run RedisApplication 在浏览器中输入 http://localhost:8080/test

spring boot 2.x 使用 redis 注解运行效果

4 Redis 使用 RedisTemplate 方式实现

与使用注解方式不同,注解方式可以零配置,只需引入依赖并在启动类上加上 @EnableCaching 注解就可以使用;而使用 RedisTemplate 方式麻烦些,需要做一些配置

本示例中设置 redis 为 StringRedisSerializer

4.1 配置

#2.x版本中由于引入了不同客户端,需要指定配置哪种连接池
#jedis客户端
spring:
  cache:
    type: redis
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    jedis:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

然后写个 RedisConfig.java 配置类,本示例中设置 redis 为 StringRedisSerializer

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object,Object> template = new RedisTemplate<>();
        //设置连接池
        template.setConnectionFactory(redisConnectionFactory);
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
         
        template.setValueSerializer(serializer);
        //hash value的序列化问题
        template.setHashValueSerializer(serializer);
        //key 的序列化
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        return  template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory){

        // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置缓存的默认过期时间,也是使用Duration设置
        config = config.entryTtl(Duration.ofMinutes(1))
                .disableCachingNullValues();     // 不缓存空值

        // 设置一个初始化的缓存空间set集合
        Set<String> cacheNames =  new HashSet<>();
        cacheNames.add("my-redis-cache1");
        cacheNames.add("my-redis-cache2");

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("my-redis-cache1", config);
        configMap.put("my-redis-cache2", config.entryTtl(Duration.ofSeconds(120)));

        // 使用自定义的缓存配置初始化一个cacheManager
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .initialCacheNames(cacheNames)  // 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
                .withInitialCacheConfigurations(configMap)
                .build();
        return cacheManager;
    }
}

4.3 Redis 数据结构简介

Redis 可以存储键与5种不同数据结构类型之间的映射,这5种数据结构类型分别为String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。

结构 结构存储的值范围
String 可以是字符串、整数或者浮点数
List 一个链表,链表上的每个节点都包含了一个字符串
Set 包含字符串的无序收集器(unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同
Hash 包含键值对的无序散列表
Zset 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定

4.4 StringRedisTemplate 与 RedisTemplate

StringRedisTemplate 与 RedisTemplate 区别

  • StringRedisTemplate 继承 RedisTemplate
  • RedisTemplate 是一个泛型类,而 StringRedisTemplate 不是
  • StringRedisTemplate 只能对 key=String,value=String 的键值操作。
  • 他们各自序列化的方式不同,但是都会得到一个字节数组。StringRedisTemplate 使用的是 StringRedisSerializer 类;RedisTemplate 使用的是 JdkSerializationRedisSerializer 类。反序列化,则是一个得到 String,一个得到 Object
  • 两者的数据是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的数据,RedisTemplate 只能管理 RedisTemplate中 的数据。

4.5 代码实例

RedisController(路径 src/main/java/com/fishpro/redis/controller/RedisController(路径.java)

package com.fishpro.redis.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;
import java.util.function.Consumer;

@RestController
public class RedisController {

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public static final String CACHE_PREFIX = "FP:";

    @RequestMapping("/redis")
    public String testRedis(){

        System.out.println("字符串和散列Value和Hash-------------------------------------------------------");
        //set字符串
        redisTemplate.opsForValue().set("string_key", "string value");
        System.out.println("-------------set字符串-------------------: " + redisTemplate.opsForValue().get("string_key"));

        //注意这里使用了JDK的序列化器,所以redis保存时不是整数,不能运算
        redisTemplate.opsForValue().set("int_key", "1");
        System.out.println("-------------set int_key-------------------: " + redisTemplate.opsForValue().get("int_key"));

        stringRedisTemplate.opsForValue().set("int", "1");
        System.out.println("-------------stringRedisTemplate set  int-------------------: " + redisTemplate.opsForValue().get("int"));

        //使用运算
        stringRedisTemplate.opsForValue().increment("int", 1);
        System.out.println("-------------使用运算+1-------------------: " + redisTemplate.opsForValue().get("int"));
 
        //定义一个hashmap散列
        Map<String, String> hash = new HashMap<String, String>();
        hash.put("field1", "value1");
        hash.put("field2", "value2");
        //存入一个散列数据类型
        stringRedisTemplate.opsForHash().putAll("hash", hash);
        System.out.println("-------------存入一个散列数据类型-------------------: ");
        System.out.println("-------------map 遍历-------------------: ");
        redisTemplate.opsForHash().entries("hash").forEach((k, v) -> {
            System.out.println(k + ": " + v);
        });
        System.out.println("-------------map->set 遍历-------------------: ");
        redisTemplate.opsForHash().keys("hash").forEach(key -> {
            System.out.println(key + ": " + redisTemplate.opsForHash().get("hash", key));
        });
        //新增一个字段
        stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
        System.out.println("-------------新增一个字段 field3-------------------: ");
        redisTemplate.opsForHash().entries("hash").forEach((k, v) -> {
            System.out.println(k + ": " + v);
        });
        //绑定散列操作的key,这样可以连续对同一个散列数据进行操作
        BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
        //删除两个字段
        hashOps.delete("field1", "field2");
        System.out.println("-------------删除两个字段 field1 field2-------------------: ");
        hashOps.entries().forEach((k, v) -> {
            System.out.println(k + ": " + v);
        });
        //新增一个字段
        hashOps.put("field5", "value5");
        System.out.println("-------------新增一个字段 field5-------------------: ");
        hashOps.entries().forEach((k, v) -> {
            System.out.println(k + ": " + v);
        });


        System.out.println("列表(链表)List-------------------------------------------------------");
        //list 链表
        //插入两个链表,注意它们在链表中的顺序
        //链表从左到右的顺序为v10,v8,v6,v4,v2
        stringRedisTemplate.opsForList().leftPushAll("list1", "v2", "v4", "v6", "v8", "v10");
        System.out.println("----------链表从左到右的顺序为v10,v8,v6,v4,v2----------------:");
        stringRedisTemplate.opsForList().range("list1", 0, stringRedisTemplate.opsForList().size("list1") - 1).forEach(s -> System.out.println(s));

        //从左到右顺序为v1,v2,v3,v4,v5,v6
        stringRedisTemplate.opsForList().rightPushAll("list2", "v1", "v2", "v3", "v4", "v5", "v6");
        System.out.println("----------从左到右顺序为v1,v2,v3,v4,v5,v6----------------:");
        stringRedisTemplate.opsForList().range("list2", 0, stringRedisTemplate.opsForList().size("list2") - 1).forEach(s -> System.out.println(s));
        // 绑定list2链表操作
        BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
        //从右边弹出一个成员
        String result1 = (String)listOps.rightPop();
        System.out.println("----------从右边弹出一个成员----------------: " + result1);
        listOps.range(0, listOps.size() - 1).forEach(s -> System.out.println(s));
        //获取定位元素,redis从0开始运算
        String result2 = (String)listOps.index(1);
        System.out.println("----------获取定位元素,redis从0开始运算----------------: " + result2);
        listOps.range(0, listOps.size() - 1).forEach(s -> System.out.println(s));
        //从左边插入链表
        listOps.leftPush("v0");
        System.out.println("----------从左边插入链表----------------: " + "v0");
        listOps.range(0, listOps.size() - 1).forEach(s -> System.out.println(s));
        //链表长度
        Long size = listOps.size();
        //求链表下标区间成员,整个链表下标范围为0到size-1,这里不取最后一个元素
        List elements = listOps.range(0, size - 2);
        System.out.println("----------求链表下标区间成员0->size-2 ----------------: " + "v0");
        elements.forEach(s -> System.out.println(s));





        System.out.println("集合Set示例-------------------------------------------------------");
        //set
        //请注意,这里v1重复两次,因为集合不允许重复,所以只是插入5个成员到集合中
        stringRedisTemplate.opsForSet().add("set1", "v1", "v1", "v2", "v3", "v4", "v5");
        stringRedisTemplate.opsForSet().add("set2", "v2", "v4", "v6", "v8");
        //绑定set1集合操作
        BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
        //增加两个元素
        setOps.add("v6", "v7");
        //删除两个元素
        setOps.remove("v1", "v7");
        //返回所有元素
        Set set1 = setOps.members();
        // 成员数
        size = setOps.size();
        //求交集
        Set inner = setOps.intersect("set2");
        //求交集并且用新集合inter保存
        setOps.intersectAndStore("set2", "inner");
        //求差集
        Set diff = setOps.diff("set2");
        //求差集,并且用新集合diff保存
        setOps.diffAndStore("set2", "diff");
        //求并集
        Set union = setOps.union("set2");
        //求并集并且用新集合union保存
        setOps.unionAndStore("set2", "union");


        //ZSET
        Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
        for (int i = 1; i <= 9; i++) {
            //分数
            double score = i * 0.1;
            //创建一个TypedTuple对象,存入值和分数
            ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score);
            typedTupleSet.add(typedTuple);
        }
        //往有序集合插入元素
        stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
        //绑定zset1有序集合操作
        BoundZSetOperations zsetOps = stringRedisTemplate.boundZSetOps("zset1");
        System.out.println("------------init----------------");
        zsetOps.rangeWithScores(0, zsetOps.size() - 1).forEach(new Consumer() {

            @Override
            public void accept(Object t) {
                ZSetOperations.TypedTuple<String> s = (ZSetOperations.TypedTuple)t;
                System.out.println(s.getValue() + " : " + s.getScore());
            }

        });
        //增加一个元素
        zsetOps.add("value10", 0.26);
        System.out.println("------------增加一个元素 value10----------------");
        zsetOps.rangeWithScores(0, zsetOps.size() - 1).forEach(new Consumer() {

            @Override
            public void accept(Object t) {
                ZSetOperations.TypedTuple<String> s = (ZSetOperations.TypedTuple)t;
                System.out.println(s.getValue() + " : " + s.getScore());
            }

        });
        // 获得range 1---6
        Set<String> setRange = zsetOps.range(1, 6);
        System.out.println("------------获得range 1---6---------------");
        Iterator itor = setRange.iterator();
        if (itor.hasNext()) {
            String s = (String)itor.next();
            System.out.println(s);
        }
        //按分数排序获得有序集合
        Set<String> setScore = zsetOps.rangeByScore(0.2, 0.6);
        System.out.println("------------按分数排序获得有序集合 (0.2, 0.6)---------------");
        Iterator itor2 = setScore.iterator();
        if (itor.hasNext()) {
            String s = (String)itor.next();
            System.out.println(s);
        }
        //自定义范围
        RedisZSetCommands.Range range = new RedisZSetCommands.Range();
        range.gt("value3");//大于value3
        //        range.gte("value3");//大于等于value3
        //        range.lt("value8");//小于value8
        range.lte("value8");//小于等于value8
        //按值排序,请注意这个排序是按字符串排序
        Set<String> setLex = zsetOps.rangeByLex(range);
        System.out.println("------------自定义范围 (value3, value8)---------------");
        Iterator itor3 = setLex.iterator();
        if (itor.hasNext()) {
            String s = (String)itor.next();
            System.out.println(s);
        }

        //删除元素
        zsetOps.remove("value9", "value2");
        System.out.println("------------删除元素 value9, value2---------------");
        zsetOps.rangeWithScores(0, zsetOps.size() - 1).forEach(new Consumer() {

            @Override
            public void accept(Object t) {
                ZSetOperations.TypedTuple<String> s = (ZSetOperations.TypedTuple)t;
                System.out.println(s.getValue() + " : " + s.getScore());
            }

        });

        //求分数
        Double score = zsetOps.score("value8");
        System.out.println("-----------求分数 value8---------------:" + score);
        //在下标区间下,按分数排序,同时返回value和score
        Set<ZSetOperations.TypedTuple<String>> rangeSet = zsetOps.rangeWithScores(1, 6);
        System.out.println("-----------在下标区间下,按分数排序 (1, 6)---------------");
        rangeSet.forEach(new Consumer() {

            @Override
            public void accept(Object t) {
                ZSetOperations.TypedTuple<String> s = (ZSetOperations.TypedTuple)t;
                System.out.println(s.getValue() + " : " + s.getScore());
            }

        });
        //在分数区间下,按分数排序,同时返回value和score
        Set<ZSetOperations.TypedTuple<String>> scoreSet = zsetOps.rangeByScoreWithScores(0.1, 0.6);
        System.out.println("-----------在分数区间下,按分数排序 (0.1, 0.6)---------------");
        scoreSet.forEach(new Consumer() {

            @Override
            public void accept(Object t) {
                ZSetOperations.TypedTuple<String> s = (ZSetOperations.TypedTuple)t;
                System.out.println(s.getValue() + " : " + s.getScore());
            }

        });
        //按从大到小排序
        Set<String> reverseSet = zsetOps.reverseRange(0, zsetOps.size() - 1);
        System.out.println("-----------按从大到小排序---------------");
        reverseSet.forEach(s -> System.out.println(s));

        System.out.println("redis开启事务--------------------------");
        redisTemplate.opsForValue().set("key1", "value1");
        List list = (List)redisTemplate.execute((RedisOperations operations) -> {
            //设置要监控的Key
            operations.watch("key1");
            //开启事务。在exec命令执行前,全部都只是进入队列
            operations.multi();
            operations.opsForValue().set("key2", "value2");
            //获取值为null,因为redis只是把命令放入队列
            Object value2 = operations.opsForValue().get("key2");
            System.out.println("命令在队列中,所以key2为null【" + value2 + "】");
            operations.opsForValue().set("key3", "value3");
            System.out.println("命令在队列中,所以key3为null【" + value2 + "】");
            //执行exec命令,将先判断key1是否在监控后被修改过,如果是则不执行事务,否则就执行事务
            return operations.exec();
        });


        System.out.println("redis流水线--------------------------");
        Long start = System.currentTimeMillis();
        List list = redisTemplate.executePipelined((RedisOperations operations) -> {
            for (int i = 1; i <= 100000; i++) {
                operations.opsForValue().set("pipeline_" + i, "value" + i);
                String value = (String)operations.opsForValue().get("pipeline_" + i);
                if (i == 100000) {
                    System.out.println("命令在队列中,所以值为null【" + value + "】");
                }
            }
            return null;
        });
        Long end = System.currentTimeMillis();
        System.out.println("耗时: " + (end - start) + "毫秒");

        return "";
    }
}

4.6 运行示例

右键 RedisApplication 选择 Run RedisApplication 在浏览器中输入 http://localhost:8080/redis

spring boot 2.x 使用 redisTemplate

本项目源码下载


参考

https://www.cnblogs.com/maria-ld/p/10010219.html
https://blog.csdn.net/Sadlay/article/details/83821629

猜你喜欢

转载自www.cnblogs.com/fishpro/p/spring-boot-study-redis.html