详解Spring Boot 访问Redis的三种方式

2022-12-17

这篇文章主要介绍了Spring Boot 访问Redis的三种方式,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

目录
  • 前言
    • 开始准备
    • RedisTemplate
    • JPA Repository
    • Cache
  • 总结

    前言

    最近在极客时间上面学习丁雪丰老师的《玩转 Spring 全家桶》,其中讲到访问Redis的方式,我专门把他们抽出来,在一起对比下,体验一下三种方式开发上面的不同, 分别是这三种方式

    • RedisTemplate
    • JPA Repository
    • Cache

    开始准备

    开始之前我们需要有Redis安装,我们采用本机Docker运行Redis, 主要命令如下

    docker pull redis
    docker run --name my_redis -d -p 6379:6379 redis
    docker exec -it my_redis bash
    redis-cli
    

    前面两个命令是启动redis docker, 后两个是连接到docker, 在使用redis-cli 去查看redis里面的内容,主要查看我们存在redis里面的数据。

    RedisTemplate

    我们先从RedisTemplate开始,这个是最好理解的一种方式,我之前在工作中也使用过这种方式,先看代码示例
    我们先定义一个POJO类

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Book implements Serializable {
        private Long id;
        private String name;
    
        private String author;
    }

    一个很简单的BOOK类,三个字段: id,name和author.
    再来一个RedisTemplate的Bean

        @Bean
        public RedisTemplate<String, Book> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Book> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }

    再定义一个使用这个RedisTemplate的Service类

        public Optional<Book> findOneBook(String name) {
            HashOperations<String, String, Book> hashOperations = redisTemplate.opsForHash();
            if (redisTemplate.hasKey(CACHE) && hashOperations.hasKey(CACHE, name)) {
                log.info("Get book {} from Redis.", name);
                return Optional.of(hashOperations.get(CACHE, name));
            }
    
            Optional<Book> book = bookRepository.getBook(name);
            log.info("Book Found: {}", book);
            if (book.isPresent()) {
                log.info("Put book {} to Redis.", name);
                hashOperations.put(CACHE, name, book.get());
                redisTemplate.expire(CACHE, 10, TimeUnit.MINUTES);
            }
            return book;
        }

    我们使用Hash来存储这个Book信息,在上面的方法中查找书名存不存在Redis中,如果存在就直接返回,如果不存在就去持久化存储中找,找到就再通过Template写入到Redis中, 这是缓存的通用做法。 使用起来感觉很方便。
    我们这里为了简单没有使用持久化存储,就硬编码了几条数据, 代码如下

    @Repository
    public class BookRepository {
        Map<String, Book> bookMap = new HashMap<>();
        public BookRepository(){
            bookMap.put("apache kafka", Book.builder()
                    .name("apache kafka").id(1L).author("zhangsan")
                    .build());
            bookMap.put("python", Book.builder()
                    .name("python").id(2L).author("lisi")
                    .build());
        }
    
        public Optional<Book> getBook(String name){
            if(bookMap.containsKey(name)){
                return Optional.of(bookMap.get(name));
            }
            else{
                return Optional.empty();
            }
        }
    }
    

    我们调用 bookService.findOneBook("python")和bookService.findOneBook("apache kafka"); 来把数据写入到换存中

    我们来看下存储在Redis的数据长什么样子。

    127.0.0.1:6379> keys *
    1) "\xac\xed\x00\x05t\x00\x04book"
    127.0.0.1:6379> type "\xac\xed\x00\x05t\x00\x04book"
    hash
    127.0.0.1:6379> hgetall "\xac\xed\x00\x05t\x00\x04book"
    1) "\xac\xed\x00\x05t\x00\x06python"
    2) "\xac\xed\x00\x05sr\x00&com.ken.redistemplatesample.model.Book=\x19\x96\xfb\x7f\x7f\xda\xbe\x02\x00\x03L\x00\x06authort\x00\x12Ljava/lang/String;L\x00\x02idt\x00\x10Ljava/lang/Long;L\x00\x04nameq\x00~\x00\x01xpt\x00\x04lisisr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x00\x00\x00\x00\x02t\x00\x06python"
    3) "\xac\xed\x00\x05t\x00\x0capache kafka"
    4) "\xac\xed\x00\x05sr\x00&com.ken.redistemplatesample.model.Book=\x19\x96\xfb\x7f\x7f\xda\xbe\x02\x00\x03L\x00\x06authort\x00\x12Ljava/lang/String;L\x00\x02idt\x00\x10Ljava/lang/Long;L\x00\x04nameq\x00~\x00\x01xpt\x00\bzhangsansr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x00\x00\x00\x00\x01t\x00\x0capache kafka"
    

    我们可以看到数据被存在了key是“\xac\xed\x00\x05t\x00\x04book”的一个Hash表中, Hash里面有两条记录。 大家发现一个问题没有? 就是这个key不是我们想象的用“book”做key,而是多了一串16进制的码, 这是因为RedisTemplate使用了默认的JdkSerializationRedisSerializer 去序列化我们的key和value, 如果大家都用Java语言那没有问题, 如果有人用Java语言写,有人用别的语言读,那就有问题,就像我开始的时候用hgetall "book"始终拿不到数据那样。
    RedisTemplate也提供了StringRedisTemplate来方便大家需要使用String来序列化redis里面的数据。简单看下代码

        @Bean
        public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
        {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
    	
        public Optional<String> getBookString(String name){
            HashOperations<String, String, String> hashOperations = stringRedisTemplate.opsForHash();
            if (stringRedisTemplate.hasKey(STRINGCACHE) && hashOperations.hasKey(STRINGCACHE, name)) {
                log.info("Get book {} from Redis.", name);
                return Optional.of(hashOperations.get(STRINGCACHE, name));
            }
    
            Optional<Book> book = bookRepository.getBook(name);
            log.info("Book Found: {}", book);
            if (book.isPresent()) {
                log.info("Put book {} to Redis.", name);
                hashOperations.put(STRINGCACHE, name, book.get().getAuthor());
                stringRedisTemplate.expire(STRINGCACHE, 10, TimeUnit.MINUTES);
                return Optional.of(book.get().getAuthor());
            }
            return Optional.empty();
        }

    使用上就没有那么方便,你就得自己写需要存的是哪个字段,读出来是哪个字段。

    127.0.0.1:6379> keys *
    1) "string_book"
    127.0.0.1:6379> hgetall string_book
    1) "python"
    2) "lisi"
    3) "apache kafka"
    4) "zhangsan"

    如上图所示,使用客户端读出来看起来就比较清爽一些。也可以看到占用的Size会小很多,我们这个例子相差7倍,如果是数据量大,这个还是比较大的浪费。

    127.0.0.1:6379> keys *
    1) "\xac\xed\x00\x05t\x00\x04book"
    2) "string_book"
    127.0.0.1:6379> memory usage "string_book"
    (integer) 104
    127.0.0.1:6379> memory usage "\xac\xed\x00\x05t\x00\x04book"
    (integer) 712

    JPA Repository

    我们知道使用JPA Repository来访问DataBase的时候,增删改查那样的操作能够很方便的实现,基本就是定义个接口,代码都不用写,Spring就帮我们完成了大部分的工作,那么访问Redis是不是也可以这样呢? 答案是肯定的,我们来看代码
    首先我们还是定义一个POJO

    @RedisHash(value = "cache-book", timeToLive = 600)
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class CacheBook implements Serializable {
    
        @Id
        private Long userId;
    
        @Indexed
        private String name;
    
        private String author;
    }

    这个类与我们上面template上面的类的区别就是我们加了两个注解, 在类开头加了
    @RedisHash(value = "cache-book", timeToLive = 600)
    在字段上面加了@Id和@Indexed
    定义一个Repository的接口

    public interface CacheBookRepository extends CrudRepository<CacheBook, Long> {
        Optional<CacheBook> findOneByName(String name);
    }

    再定义一个service和上面那个例子template一样,缓存中有就到缓存中拿,没有就到持久化存储中找,并写入缓存

    @Slf4j
    @Service
    public class BookService {
        private static final String CACHE = "repository-book";
        @Autowired
        private CacheBookRepository cacheRepository;
    
        @Autowired
        private BookRepository bookRepository;
    
        public Optional<CacheBook> findOneBook(String name) {
            Optional<CacheBook> optionalCacheBook = cacheRepository.findOneByName(name);
            if(!optionalCacheBook.isPresent())
            {
                Optional<CacheBook> book = bookRepository.getBook(name);
                log.info("Book Found: {}", book);
                if (book.isPresent()) {
                    log.info("Put book {} to Redis.", name);
                    cacheRepository.save(book.get());
                }
                return book;
            }
            return optionalCacheBook;
        }
    }

    代码很简单,简单到不敢相信是真的。
    还是一样,调用这个方法,我们来看存在Redis里面的数据

    127.0.0.1:6379> keys *
    1) "repository-book:2"
    2) "repository-book:2:idx"
    3) "repository-book"
    4) "repository-book:name:apache kafka"
    5) "repository-book:name:python"
    6) "repository-book:1:idx"
    7) "repository-book:1"
    

    哇,感觉存的内容有些多, 不用怕我们来看下各自存什么数据
    首先看最短的一个

    127.0.0.1:6379> smembers repository-book
    1) "1"
    2) "2"

    它里面存的是我们的id所有的value, 可以用来判断id是否存在
    再来看

    127.0.0.1:6379> hgetall repository-book:2
    1) "_class"
    2) "com.ken.redisrepositorysample.model.CacheBook"
    3) "author"
    4) "lisi"
    5) "name"
    6) "python"
    7) "userId"
    8) "2"

    这个是我们数据存放的地方

    127.0.0.1:6379> smembers repository-book:1:idx
    1) "repository-book:name:apache kafka"
    127.0.0.1:6379> smembers "repository-book:name:apache kafka"
    1) "1"

    另外两个都是set, 存在在它们里面的数据是索引信息。
    由此可以看出通过JPA Repository 的方式,代码很少,而且存储的数据也很通用,个人觉得是比较理想的访问方法。

    Cache

    我们已经看了两种方式,在访问的时候遵循这样的模式:缓存中有就从缓存中返回,没有就从持久化存储中找,然后写入缓存,这部分代码我也不想自己写,那么Cache就是你的救星。
    我们先看代码
    我们这次使用内存数据库H2作为持久化存储, 放一个schema.sql在resouces下面

    drop table t_book if exists;
    create table t_book (
        id bigint auto_increment,
        create_time timestamp,
        update_time timestamp,
        name varchar(255),
        author varchar(200),
        primary key (id)
    );
    insert into t_book (name, author, create_time, update_time) values ('python', 'zhangsan', now(), now());
    insert into t_book (name, author, create_time, update_time) values ('hadoop', 'lisi', now(), now());
    insert into t_book (name, author, create_time, update_time) values ('java', 'wangwu', now(), now());
    

    然后定义POJO

    @Entity
    @Table(name = "T_BOOK")
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class CacheBook implements Serializable {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String name;
    
        private String author;
    
        @Column(updatable = false)
        @CreationTimestamp
        private Date createTime;
        @UpdateTimestamp
        private Date updateTime;
    }

    完全是和数据库绑定的代码,和缓存没有任何关系
    一个Repository来访问数据库

    public interface BookRepository extends JpaRepository<CacheBook, Long> {
    }

    定义一个service来调用它

    @Slf4j
    @Service
    @CacheConfig(cacheNames = "cache-book")
    public class BookService {
    
        @Autowired
        private BookRepository bookRepository;
    
        @Cacheable
        public List<CacheBook> findAllCoffee() {
            return bookRepository.findAll();
        }
    
        @CacheEvict
        public void reloadCoffee() {
        }
    }

    这里就比较关键了,在类上加上了注解
    @CacheConfig(cacheNames = "cache-book")
    在方法上面加上了Cacheable和CacheEvict, Cacheable这个方法就是用来实现逻辑,有就从缓存中拿,没有就从数据库拿的,CacheEvict是调用这个方法的时候清除缓存。
    然后再启动入口程序的地方加上注解
    @EnableJpaRepositories
    @EnableCaching(proxyTargetClass = true)
    在配置文件application.properties中加上

    spring.jpa.hibernate.ddl-auto=none
    spring.jpa.properties.hibernate.show_sql=true
    spring.jpa.properties.hibernate.format_sql=true
    
    management.endpoints.web.exposure.include=*
    
    spring.cache.type=redis
    spring.cache.cache-names=cache-book
    spring.cache.redis.time-to-live=600000
    spring.cache.redis.cache-null-values=false
    
    spring.redis.host=localhost

    这样就可以了, 感觉就是通过配置下就把缓存给完成了,非常的简单
    我们来看Redis中是怎么存的

    127.0.0.1:6379> keys *
    1) "cache-book::SimpleKey []"
    127.0.0.1:6379> get "cache-book::SimpleKey []"
    "\xac\xed\x00\x05sr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x03w\x04\x00\x00\x00\x03sr\x00(com.ken.rediscachesample.model.CacheBook\xec\xcbR=\xe1U\x9b\xf7\x02\x00\x05L\x00\x06authort\x00\x12Ljava/lang/String;L\x00\ncreateTimet\x00\x10Ljava/util/Date;L\x00\x02idt\x00\x10Ljava/lang/Long;L\x00\x04nameq\x00~\x00\x03L\x00\nupdateTimeq\x00~\x00\x04xpt\x00\bzhangsansr\x00\x12java.sql.Timestamp&\x18\xd5\xc8\x01S\xbfe\x02\x00\x01I\x00\x05nanosxr\x00\x0ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\b\x00\x00\x01\x84\xff]\x85\xb0x\b-\x81\x80sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x00\x00\x00\x00\x01t\x00\x06pythonsq\x00~\x00\bw\b\x00\x00\x01\x84\xff]\x85\xb0x\b-\x81\x80sq\x00~\x00\x02t\x00\x04lisisq\x00~\x00\bw\b\x00\x00\x01\x84\xff]\x85\xb0x\b<\xbf\xd8sq\x00~\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x02t\x00\x06hadoopsq\x00~\x00\bw\b\x00\x00\x01\x84\xff]\x85\xb0x\b<\xbf\xd8sq\x00~\x00\x02t\x00\x06wangwusq\x00~\x00\bw\b\x00\x00\x01\x84\xff]\x85\xb0x\b<\xbf\xd8sq\x00~\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x03t\x00\x04javasq\x00~\x00\bw\b\x00\x00\x01\x84\xff]\x85\xb0x\b<\xbf\xd8x"
    

    看到没有,就是当成Redis里面的String来存的, 如果数据量比较小,那是非常的方便,如果数据量大,这种方式就有些问题了。

    总结

    我们看了这三种方式,这里仅仅是做了个入门,每个里面都有很多细节的地方需要去研究和使用,整体的感觉是要想使用的简单,那么存储在Redis中的数据就要量少,量大后,就需要自己来定制了,那基本上要用RedisTemplate来做一些工作。 这三个程序比较简单,我也把它放在github上面了, https://github.com/dengkun39/redisdemo.git

    到此这篇关于详解Spring Boot 访问Redis的三种方式的文章就介绍到这了,更多相关Spring Boot 访问Redis内容请搜索北冥有鱼以前的文章或继续浏览下面的相关文章希望大家以后多多支持北冥有鱼!