還在只用RedisTemplate訪問Redis嗎

語言: CN / TW / HK

theme: nico

開啟掘金成長之旅!這是我參與「掘金日新計劃 · 12 月更文挑戰」的第28天,點選檢視活動詳情

開始準備

開始之前我們需要有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 getBookString(String name){        HashOperations 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 = 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來做一些工作。