首頁 > 軟體

詳解Spring Boot 存取Redis的三種方式

2022-12-15 14:03:56

前言

最近在極客時間上面學習丁雪豐老師的《玩轉 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) "xacxedx00x05tx00x04book"
127.0.0.1:6379> type "xacxedx00x05tx00x04book"
hash
127.0.0.1:6379> hgetall "xacxedx00x05tx00x04book"
1) "xacxedx00x05tx00x06python"
2) "xacxedx00x05srx00&com.ken.redistemplatesample.model.Book=x19x96xfbx7fx7fxdaxbex02x00x03Lx00x06authortx00x12Ljava/lang/String;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x01xptx00x04lisisrx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x02tx00x06python"
3) "xacxedx00x05tx00x0capache kafka"
4) "xacxedx00x05srx00&com.ken.redistemplatesample.model.Book=x19x96xfbx7fx7fxdaxbex02x00x03Lx00x06authortx00x12Ljava/lang/String;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x01xptx00bzhangsansrx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x01tx00x0capache kafka"

我們可以看到資料被存在了key是“xacxedx00x05tx00x04book”的一個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) "xacxedx00x05tx00x04book"
2) "string_book"
127.0.0.1:6379> memory usage "string_book"
(integer) 104
127.0.0.1:6379> memory usage "xacxedx00x05tx00x04book"
(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 []"
"xacxedx00x05srx00x13java.util.ArrayListxx81xd2x1dx99xc7ax9dx03x00x01Ix00x04sizexpx00x00x00x03wx04x00x00x00x03srx00(com.ken.rediscachesample.model.CacheBookxecxcbR=xe1Ux9bxf7x02x00x05Lx00x06authortx00x12Ljava/lang/String;Lx00ncreateTimetx00x10Ljava/util/Date;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x03Lx00nupdateTimeqx00~x00x04xptx00bzhangsansrx00x12java.sql.Timestamp&x18xd5xc8x01Sxbfex02x00x01Ix00x05nanosxrx00x0ejava.util.Datehjx81x01KYtx19x03x00x00xpwbx00x00x01x84xff]x85xb0xb-x81x80srx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x01tx00x06pythonsqx00~x00bwbx00x00x01x84xff]x85xb0xb-x81x80sqx00~x00x02tx00x04lisisqx00~x00bwbx00x00x01x84xff]x85xb0xb<xbfxd8sqx00~x00x0bx00x00x00x00x00x00x00x02tx00x06hadoopsqx00~x00bwbx00x00x01x84xff]x85xb0xb<xbfxd8sqx00~x00x02tx00x06wangwusqx00~x00bwbx00x00x01x84xff]x85xb0xb<xbfxd8sqx00~x00x0bx00x00x00x00x00x00x00x03tx00x04javasqx00~x00bwbx00x00x01x84xff]x85xb0xb<xbfxd8x"

看到沒有,就是當成Redis裡面的String來存的, 如果資料量比較小,那是非常的方便,如果資料量大,這種方式就有些問題了。

總結

我們看了這三種方式,這裡僅僅是做了個入門,每個裡面都有很多細節的地方需要去研究和使用,整體的感覺是要想使用的簡單,那麼儲存在Redis中的資料就要量少,量大後,就需要自己來客製化了,那基本上要用RedisTemplate來做一些工作。 這三個程式比較簡單,我也把它放在github上面了, https://github.com/dengkun39/redisdemo.git

到此這篇關於詳解Spring Boot 存取Redis的三種方式的文章就介紹到這了,更多相關Spring Boot 存取Redis內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com