Java开发 - Redis初体验
前言
es我們已經在前文中有所了解,和es有相似功能的是Redis,他們都不是純粹的數據庫。兩者使用場景也是存在一定的差異的,本文目的并不重點說明他們之間的差異,但會簡要說明,重點還是在對Redis的了解和學習上。學完本篇,你將了解Redis的特點和作用,掌握Redis的基礎用法,這將有助于你在后續的項目中更好的使用Redis。建議大家都動手和博主一起實操,莫要養成眼高手低的毛病,下面,讓我們提起精神,一起開始這場Redis盛宴吧。
Redis
什么是Redis
Redis全名Remote Dictionary Server,即遠程字典服務,是一個開源的使用ANSI?C語言編寫、支持網絡、可基于內存亦可持久化的日志型、Key-Value數據庫,并提供多種語言的API。
其官網:Redis
雖然Redis也是數據庫,但又有別于我們所知的mysql等關系型數據庫,Redis是一款基于內存的NoSQL數據存儲服務,也就是非關系型的數據庫,這點要搞搞清楚。
為什么使用Redis
相信大家在學習SQL的時候都有過這樣的經歷,往數據庫插入50w條數據,然后去條件檢索某些數據,效率如何?大家心里多少還是有點x數的,試想,這種暴力型操作要是出現在我們常用的一些網站和應用上,且每次請求到這么做,那該是何等的瘋狂?
說到這里,其實還沒有說出Redis的主要應用場景,每每想到這個,總是會跳出另一個東西:ES。算了,咱們看下面的對比吧,有對比才有傷害啊!
Redis和ES的區別
Redis使用場景
因為Redis是基于內存運行的,說起內存,你應該知道,其運行效率遠高于和硬盤的交互。這也就導致了Redis運行效率非常高。
Redis同樣支持將數據存儲在硬盤上,支持主從和分布式使用,但在事務上有著嚴重不足,所以在關系比較復雜的地方就不適合使用Redis,但卻可以配合關系型數據庫做緩存,也就是通過復雜SQL查找到數據緩存在Redis。
此類緩存數據,我準備用一個絕對一些的詞,必須是穩定型,通用型,高頻次數據,比如類別,商品信息,資訊信息等。如果每次請求都會變,每個人又都不一樣的數據,不太建議存儲在Redis,不是不行,只是不建議,因為會占用大量的內存,造成數據的冗余,還不利于做數據同步。
ES使用場景
ES是非關系型數據庫,我們通常說他是一個引擎,實時搜索引擎,他是把數據按照一定的規律存儲起來,達到比關系型數據庫查詢效率更高的目的。
ES擴展容易,前文中曾使用了ik插件,ES同樣支持主從,由于其存儲的數據結構特點,所以其查詢效率非常高,在微服務這種大數據形態下的表現尤為優秀,可以快速實現數據的整合,對于日志和數據分析非常友好,對于實時狀態的高也并發有著極強的適應能力,且延遲也很低。
想了解ES的童鞋可以點擊下面鏈接前往查看:Java開發 - Elasticsearch初體驗
Redis安裝
由于博主是Mac電腦,這里就以Mac為例,Windows沒試過,Windows的童鞋可自行百度安裝。
- 打開終端,輸入:brew install redis
- 測試安裝成功輸入:redis-server,看到Redis 的啟動日志則說明安裝成功,通過Ctrl-C可停止此redis
- 使用 launchd 啟動Redis:brew services start redis 暫停Redis:brew services stop redis?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 查看Redis信息:brew services info redis
其實博主覺得這么做挺麻煩的,有個東西叫:Docker Desktop
直接安裝這個,Mac電腦省去了安裝虛擬機的麻煩,直接在此軟件內安裝各種服務,不要太爽:
數據庫,nacos,senta等都可以在這里安裝并且一鍵啟動,推薦大家去裝一個,就不用每一個東西都要自己裝,太麻煩了。
還有個Redis的客戶端也推薦大家裝一下 :
此軟件在Mac上App Store是收費的,推薦安裝方式如下:
brew install --cask another-redis-desktop-manager安裝后就可以找到啟動圖標了,Windows可在網上自行搜索,其工作頁面如下:
可看到目前我們是啟動狀態,客戶端連接數是1。
最好弄好了這些東西后再來跟著學下去,這種可視化工具可以在我們調用接口時看到存儲在Redis中的數據,非常方便。
Redis緩存
緩存淘汰
Redis幫助我們解決了一些三高的問題,但在訪問量非常大的時候,Redis要在同一時間保存大量的數據,但Redis內存并不是無限的,一旦內存占滿,可能會發生什么?降速?阻塞?宕機?都有可能,所以在連續不斷的存入新數據的同時,還要將不使用的老數據及時的從內存中刪除,這就需要一個淘汰策略。
好在Redis提供了這種機制,我們看看有哪些淘汰策略:
noeviction:返回錯誤**(默認)**
allkeys-random:所有數據中隨機刪除數據
volatile-random:所有過期時間的數據庫中隨機刪除數據
volatile-ttl:刪除剩余有效時間最少的數據
allkeys-lru:所有數據中刪除上次使用時間最久的數據
volatile-lru:所有過期時間的數據中刪除上次使用時間最久的數據
allkeys-lfu:所有數據中刪除使用頻率最少的
volatile-lfu:所有過期時間的數據中刪除使用頻率最少的
通過合理的選擇以上參數配置Redis,可以有效解決這個問題,但也需要時刻監測Redis內存情況,現在的云服務做得都很好,可以提前預警通知。
緩存穿透
我們在使用Redis時,將數據庫中查詢出來的數據保存在Redis中,但Redis有自己的淘汰策略,所以這些數據并不會無限期保存。
正常來說,訪問的請求會先去Redis中拿數據,拿不到才會去數據庫中查找,再將查到的數據存儲到Redis中,一旦這樣的請求數量非常多的時候,數據庫的壓力就會變大,我們可以認為,其表現的現象即為Redis失效,沒有工作,當然算是失效了,而這種情況,我們稱之為緩存穿透。
開發中當然要避免緩存穿透,簡單點,可以將查詢回來為空的數據在Redis中存為null,防止Redis被反復穿透,但這也有缺點,比如反復更換查詢關鍵字,反復穿透依然存在,當然,這只是特例,雖然實際中發生的概率不會太高,但還是要防范利用此情況攻擊服務器的可能。
最好的做法是通過增加布隆過濾器來解決此問題,在業務進入時,提前判斷用戶查詢的信息是否存在于數據庫中,如果沒有,直接返回,不再走完整的路徑。
緩存擊穿
緩存穿透和緩存擊穿很類似,我們正常的流程是先訪問Redis,Redis沒有就去數據庫查詢,這種情況,數據庫是可以查到數據的,此種現象就叫擊穿,而少量的擊穿并不是問題。
緩存雪崩
上面的擊穿在同一時間大量發生,就變成了雪崩,數據庫短時間內出現很多新的查詢請求,就會發生性能問題。
這是由于Redis緩存淘汰策略把過期的數據大批量清空導致的,它本身不算異常,只是我們要避免同一時間大量的過期情況出現,所以在設置過期時間時,在基礎時間上增加10分鐘或30分鐘以內的隨機時間來解決這個問題,時間你可以自己定。
Redis持久化
存儲特點
Redis是在內存中運行的,這和我們所有的軟件都是一樣的,內存可以保存,但Redis保存的數據卻并不是在內存上,試想我們的電腦手機,關機后再打開還能恢復打開時的樣子嗎?這自然是不能的。
所以,為了解決斷電重啟等問題,Redis支持了持久化,將需要保存的數據保存在服務器硬盤上。
針對以上硬盤保存數據的特點,Redis在重新啟動后恢復數據的方式有兩種,我們來看看是哪兩種。
RDB
RDB全稱Redis Database Backup,中文名叫數據庫快照,它可以將Redis數據庫數據轉化為二進制數據保存在硬盤上,生成一個dump.rdb的文件,想使用此恢復模式需要提前在Redis安裝程序的配置文件中進行配置才能生效。
基于此模式,由于是整體Redis數據的二進制格式,所以數據恢復是整體恢復的,非常方便。但也因此存在了一個大文件的通病:讀寫效率不高。快照的備份不能實時進行,所以斷電重啟恢復只能恢復最后一次生成的rdb文件數據。可能會造成短時間的數據丟失。
AOF
AOF全稱Append Only File,它的策略不是緩存數據,而是將所有命令日志備份下來,在數據丟失后,可以根據運行過的日志恢復為斷電前的狀態,注意一點:這種保存日志的策略也不是實時的,數據量比較大時會分批分次進行緩存。
實際中,我們一般設置1s發送一次日志,斷電最多丟失1s數據。為了降低日志對內存的占用,AOF支持AOF rewrite,也就是說,如果你是刪除數據,那完全沒有留日志的必要,但默認時有日志的,所以,可以將這些刪除操作的日志刪除。
存儲原理
存儲原理博主簡單給大家說說,想要深入了解的推薦這篇博客:Redis存儲原理深入剖析 - 墨天輪
也可以自行查找。
Redis將內存劃分為16384個槽,類似哈希槽,將要存儲的數據的key通過CRC16算法處理,得到一個0~16383之間的值,然后將這條數據存儲到對應的槽中,下次查找的時候也是通過CRC16算法處理過的數字去對應槽中查找當前key是否存在,因為有可能直接一次就找到對應key,所以這種存儲查找方式效率非常高。這也是一種散列算法,和數據庫主鍵查找的原理很類似。推薦讀一下博主這篇博客:Java開發 - 數據庫索引的數據結構
Redis集群
Redis我們一般說起來都會說Redis服務器,所以,Redis本質上也是一臺服務器,Redis即服務器,服務器即Redis。服務器宕機,Redis肯定也好不到哪里去。如果只有一臺Redis服務器,那將會面臨一定的風險,比如系統崩潰。
主從
為了解決單Redis服務器可能存在的問題,我們一般會使用一臺備用機,這就叫做主從。主從狀態下,備用機Redis會實時同步主機Redis的數據,如果主機掉線,備用機就可以起到預備隊的效果。但這也存在一定的問題,主機正常工作時,從機就在那里歇著,主機累的喘不過來氣,肯定不愿意,從機的錢不是也白花了嗎?如下圖:
讀寫分離
為了解決從機不干活的問題,我們一般會將讀寫分離,主機可讀可寫,從機也可以讀取,這樣不僅讓從機干活,還減輕了主機的壓力,提高了項目運行的流暢度。如下圖:
哨兵
此時,還存在一個問題,主機宕機后,需要人手動切換到從機,要是及時發現還好,要是不及時,將會造成嚴重的后果。這時候,我們需要一個可以自動切換到從機的機制:哨兵模式。如下圖:
哨兵每隔固定時間向主從節點發送請求,如果節點正常相應,則說明節點正常工作,否則,將視為節點異常,啟將自動切換備用機。
有時候,因為網絡問題或者其他因素會導致哨兵接受請求返回異常而切換備用機,這不僅沒起到保護的作用,還降低了Redis的工作效率,這時,有兩種方式來解決:一是多次請求后都返回異常再切換備用機,二是采用多個哨兵的形式,當多臺哨兵都認為某臺機器存在異常,再切換到備用機。但是切記一點,哨兵不能和Redis在同一臺服務器上,否則服務器異常哨兵也將離線。
?哨兵的配置推薦看看這篇博客:Redis中的哨兵模式 - 簡書?
Redis基本使用
下面,我們就來看看Redis有哪些API,具體該怎么使用。在原來微服務項目中,上一篇Quartz是在stock子項目下運行的,Redis我們也在stock子項目下添加吧,你也可以選擇其他模塊,或者獨立建一個項目也是可以的。
添加依賴
<!-- Spring Boot Data Redis:緩存 --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>請注意,博主依賴中沒有添加version信息是因為主工程中對版本進行了管理,如果你單獨創建的項目是需要加上版本的,其他依賴在需要時按需添加。
添加配置
spring:redis:database: 0host: localhostport: 6379password:jedis:pool:#最大連接數max-active: 8#最大阻塞等待時間(負數表示沒限制)max-wait: -1#最大空閑max-idle: 8#最小空閑min-idle: 0#連接超時時間timeout: 10000cache:redis:time-to-live: 360000000推薦這篇博客:SpringDataRedis知識概括,可以去了解下Redis配置相關的詳細信息,博主覺得寫得還是很全面的。
創建配置類
操作Redis需要使用RedisTemplate對象,我們在config包下創建RedisConfiguration類:
package com.codingfire.cloud.stock.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer;import java.io.Serializable;@Configuration public class RedisConfiguration {@Beanpublic RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);redisTemplate.setKeySerializer(RedisSerializer.string());redisTemplate.setValueSerializer(RedisSerializer.json());return redisTemplate;} }固定模式,也沒啥好說的,只是需要使用這樣一個實例來操作Redis。
編寫測試方法?
由于我們之前刪除了用于測試的文件夾,還是需要新建的,選擇src,新建,file:
選擇test/java,新建測試類:
package com.codingfire.cloud.stock;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest public class CloudStockApplicationTests {}?包名,路徑和格式注意下,不要錯了。下面我們來添加Redis的測試方法。
存儲普通字符串:
package com.codingfire.cloud.stock;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import org.junit.jupiter.api.Test;import java.io.Serializable; import java.util.concurrent.TimeUnit;@SpringBootTest class CloudStockApplicationTests {@AutowiredRedisTemplate<String, Serializable> redisTemplate;@Testvoid testSetValue() {redisTemplate.opsForValue().set("name", "codingfire");}}運行測試方法,成功后我們去Redis客戶端看看有沒有什么變化:
這個就是我們測試方法中存入的數據,表示我們已經將此字符串存入Redis中。有問題的童鞋看看自己的Redis有沒有啟動連接。
有存就有取,我們去把剛才存進去的字符串取出來:
@Testvoid testGetValue() {// 當key存在時,可獲取到有效值// 當key不存在時,獲取到的結果將是nullSerializable name = redisTemplate.opsForValue().get("name");System.out.println("get value --> " + name);}運行測試方法,查看控制臺輸出:
get value --> codingfire成功從Redis取到了我們存入的數據。
不知道你注意到沒,我們前面的配置類里面對Redis的存取類型做了限制:
redis存儲類型很有限,不過好在string和json幾乎能滿足所有的需要,下面我們來存取對象類型試試:
@Testvoid testSetAdminValue() {AdminLoginDTO adminLoginDTO = new AdminLoginDTO();adminLoginDTO.setUsername("codingfire");adminLoginDTO.setPassword("123456");redisTemplate.opsForValue().set("user", adminLoginDTO);}@Testvoid testGetAdminValue() {// 當key存在時,可獲取到有效值// 當key不存在時,獲取到的結果將是nullSerializable user = redisTemplate.opsForValue().get("user");System.out.println("get value --> " + user);if (user != null) {AdminLoginDTO adminLoginDTO = (AdminLoginDTO) user;System.out.println("get value --> " + adminLoginDTO);}}分別運行以上存取對象類型方法,看看Redis客戶端和控制臺會輸出什么。
存儲對象后Redis客戶端:
獲取對象后控制臺輸出:
兩次輸出一樣為什么還要強轉呢?不強轉你就不知道獲取的對象類型,也無法直接使用序列化后對象進行數據的操作。
有添加就有刪除,那么刪除Redis中數據該怎么做呢:
@Testvoid testDeleteKey() {// 刪除key時,將返回“是否成功刪除”// 當key存在時,將返回true// 當key不存在時,將返回falseBoolean result = redisTemplate.delete("name");System.out.println("result --> " + result);}?運行此測試方法查看結果:
由于Redis中存在名為name的參數,所以result為true,對象類型的刪除也是一樣的操作。
接下來我們設置Redis的過期時間,時間到了就自動刪除,我們通過查看源碼得知,set有三個參數,第二個為過期時間,第三個為時間單位:
下面我來寫代碼:
@Testvoid testSetValueTimeout() {redisTemplate.opsForValue().set("name", "codingfire",20, TimeUnit.SECONDS);}?運行測試方法,在Redis中能看到存入的數據,20s后,數據自動刪除:
在客戶端中能看到TTL,就是倒計時的意思,打開自動刷新功能,你能看見數字是在變化的。
在存儲數據時還有一個特殊情況,比如分頁數據,是一個數組,這該怎么存呢?Redis中ops是操作器,opsForValue可以操作普通字符串或對象類型,既然是對象,為什么不能操作數組?數組也是對象啊,博主也不死心,我們來試試看:
?好像成功了?為了對比這兩種方式存儲數組的能力我們來試試ospForList存數組后是否一樣:
@Testvoid testRightPushList() {// 存入List時,需要redisTemplate.opsForList()得到針對List的操作器// 通過rightPush()可以向Redis中的List追加數據// 每次調用rightPush()時使用的key必須是同一個,才能把多個數據放到同一個List中List<AdminLoginDTO> list = new ArrayList<>();for (int i = 1; i <= 5; i++) {AdminLoginDTO adminLoginDTO = new AdminLoginDTO();adminLoginDTO.setUsername("name" + i);list.add(adminLoginDTO);}String key = "UserList";for (AdminLoginDTO adminLoginDTO : list) {redisTemplate.opsForList().rightPush(key, adminLoginDTO);}}運行測試代碼后查看Redis客戶端數據:
呀!數據展示的形式不一樣啊,我們來獲取并輸出一下這兩組數據,看看輸出是否一樣:
@Testvoid testGetListValue() {// 調用opsForList()后再調用range(String key, long start, long end)方法取出List中的若干個數據,將得到List// long start:起始下標(結果中將包含)// long end:結束下標(結果中將包含),如果需要取至最后一個元素,可使用-1作為此參數值List<Serializable> rangeList = redisTemplate.opsForList().range("UserList", 0, -1);Serializable listSer = redisTemplate.opsForValue().get("list");List<Serializable> list = (List<Serializable>) listSer;System.out.println(rangeList);System.out.println(list);for (Serializable serializable : rangeList) {System.out.println(serializable);}for (Serializable serializable : list) {System.out.println(serializable);}}?運行測試方法查看控制臺輸出:
從數據庫,其實并沒有什么太大的差別 ,但是請仔細看博主獲取兩個list的代碼,秘密就藏在里面,也就是說:怎么存,怎么取,不通過數組專用方法的需要強轉。這就是最終結論。大家不用自己試了,博主都一一試過了,opsForxxxxx用的不對就報錯了。
最后再補充兩個方法,一個是獲取數組長度,一個是獲取Redis中所有key的方法,注意:獲取數組長度的方法必須是通過opsForList方法存進去的,否則此方法無效,且報錯,看代碼:
@Testvoid testListSize() {// 獲取List的長度,即List中的元素數量String key = "UserList";Long size = redisTemplate.opsForList().size(key);System.out.println("size --> " + size);}運行結果:
獲取所有Redis的key:
@Testvoid testKeys() {// 調用keys()方法可以找出匹配模式的所有key// 在模式中,可以使用星號作為通配符Set<String> keys = redisTemplate.keys("*");for (String key : keys) {System.out.println(key);}}?此方法對ops無影響,只獲取key,查看運行結果:
對比Redis客戶端中所有key值:
完全一致,測試成功。
最后,關于Key的使用,通常建議使用冒號區分多層次,類似URL的設計方式,例如:
- 用戶列表的Key:users:list或users
- 某個用戶id(001)對應用戶信息的Key:users:userId:001
但也不是絕對,可根據自己需要選擇合適的組合方式,目的是使key不重復,且好理解。
到這里,Redis基礎方法使用就講解完了,但這畢竟只是基礎方法,在實戰中該怎么用還是個問題。下面,我們將在真實項目中去使用Redis。
Redis實戰
開始前的思考
使用Redis可以提高查詢效率,降低數據庫的壓力。基本上是用在高頻查詢的數據或是幾乎不太會改變的數據上。所以,有些比較精密的經常需要變動的數據就不能使用Redis,比如購物類應用的創建訂單,庫存都屬于此類數據。
所以在開始前確定哪些數據使用Redis,Redis中的數據從哪來是很重要的。關于Redis的調用,是寫在業務邏輯層還是做一個單獨的組件獨立出來,這也是一個問題。
如果直接將訪問Redis的代碼寫在Service中,首次開發時會很省事,但卻不利于后期的維護。
如果將訪問Redis的代碼寫的新的組件中,首次開發時會更麻煩,但有利于后期的維護。
所以你會怎么選呢?我們首先要知道,訪問Redis的API都很簡單,上面的基礎使用大家基本應該是掌握了,也沒什么難的,自定義組件雖然有利于后期維護,但代碼量可能會很少,這個需要我們去衡量總體的工作量和后期的維護情況。
每一個項目都不一樣,甚至有些小的項目根本不使用Redis都有可能,這并不是危言聳聽,這個就教給大家自己選擇了,今天,博主的目的是教會大家在項目中使用Redis。
創建Redis模塊
博主準備以passport為基礎,在其上使用Redis,雖然實際中不會在這么簡單的模塊用,不過該有的功能,博主是一步都不會省略的,照葫蘆畫瓢,其他的模塊參照此模塊就可以移植。passport是做單點登錄的模塊:Java開發 - 單點登錄初體驗(Spring Security + JWT)
沒有此模塊的童鞋可以先學此篇,也可以先看看博主代碼,新建一個模塊,照著往別的模塊上面搬。
創建調用Redis接口
在passport包下創建repository包,repository下創建IPassportRedisRepository接口:
package com.codingfire.cloud.passport.repository;import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;public interface IPassportRedisRepository {String KEY_ADMIN_ITEM_PREFIX = "admins:item:";// 將用戶信息存入到Redis中void save(AdminLoginVO adminLoginVO);// 根據用戶id獲取用戶信息AdminLoginVO getAdminDetailsById(Long id); }創建Redis實現類
在這一步之前,請大家添加Redis的依賴,并將上面代碼中Redis的配置類RedisConfiguration復制到passport的config包下,在repository包下新建impl包,包下建PassportRedisRepositoryImpl實現類:
package com.codingfire.cloud.passport.repository.impl;import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO; import com.codingfire.cloud.passport.repository.IPassportRedisRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; import java.io.Serializable;@Repository public class PassportRedisRepositoryImpl implements IPassportRedisRepository {@Autowiredprivate RedisTemplate<String, Serializable> redisTemplate;@Overridepublic void save(AdminLoginVO adminLoginVO) {String key = KEY_ADMIN_ITEM_PREFIX + adminLoginVO();redisTemplate.opsForValue().set(key, adminLoginVO);}@Overridepublic AdminLoginVO getAdminDetailsById(Long id) {String key = KEY_ADMIN_ITEM_PREFIX + id;Serializable result = redisTemplate.opsForValue().get(key);if (result == null) {return null;} else {AdminLoginVO adminLoginVO = (AdminLoginVO) result;return adminLoginVO;}} }測試以上代碼
下面在test文件夾下depassport包下新建一個測試類PassportRedisRepositoryTests:
package com.codingfire.cloud.passport;import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO; import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO; import com.codingfire.cloud.passport.repository.IPassportRedisRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest class PassportRedisRepositoryTests {@AutowiredIPassportRedisRepository repository;@Testvoid testGetAdminDetailsByIdSuccessfully() {testSave();Long id = 2L;AdminLoginDTO admin = repository.getAdminDetailsById(id);System.out.println(admin);}@Testvoid testGetAdminDetailsByIdReturnNull() {Long id = -1L;AdminLoginVO adminLoginVO = repository.getAdminDetailsById(id);Assertions.assertNull(adminLoginVO);}private void testSave() {AdminLoginVO adminLoginVO = new AdminLoginVO();adminLoginVO.setId(2L);adminLoginVO.setUsername("codeliu");adminLoginVO.setPassword("123456");repository.save(adminLoginVO);} }?運行第一個方法,先存儲,后查找:
控制器輸出我們存入的數據,看看Redis客戶端有沒有數據:
可以看到室友層級的key,這種key的命名方式我們在上面已經講過,有利于數據分層,便于觀察。
讓Redis在業務中體現調用邏輯
業務邏輯思考
下面,我們結合接口的調用來做個修改,在接口的調用過程中,我們去使用Redis。我們打開IAdminService類,在里面添加一個新的方法:
AdminLoginVO getAdminDetailsById(Long id);接著打開AdminServiceImpl類,在里面實現接口方法:
@Overridepublic AdminLoginVO getAdminDetailsById(Long id) {// ===== 以下是原有代碼,只從數據庫中獲取數據 ===== // AdminLoginVO adminLoginVO = adminMapper.getLoginInfoByUserId(id); // if (adminLoginVO == null) { // throw new CloudServiceException(ResponseCode.ERR_INTERNAL_SERVER_ERROR, // "獲取用戶信息失敗,嘗試訪問的數據不存在!"); // } // return adminLoginVO;// ===== 以下是新的業務,將從Redis中獲取數據 =====// 從repsotiroy中調用方法,根據id獲取緩存的數據// 判斷緩存中是否存在與此id對應的key// 有:表示明確的存入過某數據,此數據可能是有效數據,也可能是null// -- 判斷此key對應的數據是否為null// -- 是:表示明確的存入了null值,則此id對應的數據確實不存在,則拋出異常// -- 否:表示明確的存入了有效數據,則返回此數據即可// 無:表示從未向緩存中寫入此id對應的數據,在數據庫中,此id可能存在數據,也可能不存在// 從mapper中調用方法,根據id獲取數據庫的數據// 判斷從數據庫中獲取的結果是否為null// 是:數據庫也沒有此數據,先向緩存中寫入錯誤數據(null),再拋出異常// 將從數據庫中查詢到的結果存入到緩存中// 返回查詢結果return null;}大家看看實現的邏輯,這個很重要。
看完后,為了保證項目能運行,我們還有兩步需要做。
添加調用SQL的方法
AdminMapper添加如下方法:
AdminLoginVO getLoginInfoByUserId(Long id);添加SQL
AdminMapper.xml添加如下SQL:
<select id="getLoginInfoByUserId" resultMap="LoginInfoResultMap">select<include refid="LoginInfoQueryFields" />from adminleft join admin_roleon admin.id = admin_role.admin_idleft join role_permissionon admin_role.role_id = role_permission.role_idleft join permissionon role_permission.permission_id = permission.idwhere id=#{id} </select>?避免緩存穿透
在IPassportRedisRepository接口中添加如下方法:
/*** 判斷是否存在id對應的緩存數據** @param id 類別id* @return 存在則返回true,否則返回false*/boolean exists(Long id);/*** 向緩存中寫入某id對應的空數據(null),此方法主要用于解決緩存穿透問題** @param id 類別id*/void saveEmptyValue(Long id);在PassportRedisRepositoryImpl類中添加實現方法如下:
@Overridepublic boolean exists(Long id) {String key = KEY_ADMIN_ITEM_PREFIX + id;return redisTemplate.hasKey(key);}@Overridepublic void saveEmptyValue(Long id) {String key = KEY_ADMIN_ITEM_PREFIX + id;redisTemplate.opsForValue().set(key, null);}其實我們在上面說過,這種設置null的方法能一定程度上防止緩存反復穿透,但卻并不是最好的解決辦法,常規做法應該是通過布隆過濾器來做。
業務實現
@Overridepublic AdminLoginVO getAdminDetailsById(Long id) { // ===== 以下是原有代碼,只從數據庫中獲取數據 ===== // AdminLoginVO adminLoginVO = adminMapper.getLoginInfoByUserId(id); // if (adminLoginVO == null) { // throw new CloudServiceException(ResponseCode.ERR_INTERNAL_SERVER_ERROR, // "獲取用戶信息失敗,嘗試訪問的數據不存在!"); // } // return adminLoginVO;// ===== 以下是新的業務,將從Redis中獲取數據 =====log.debug("根據id({})獲取用戶詳情……", id);// 從repository中調用方法,根據id獲取緩存的數據// 判斷緩存中是否存在與此id對應的keyboolean exists = redisRepository.exists(id);if (exists) {// 有:表示明確的存入過某數據,此數據可能是有效數據,也可能是null// -- 判斷此key對應的數據是否為nullAdminLoginVO cacheResult = redisRepository.getAdminDetailsById(id);if (cacheResult == null) {// -- 是:表示明確的存入了null值,則此id對應的數據確實不存在,則拋出異常log.warn("在緩存中存在此id()對應的Key,卻是null值,則拋出異常", id);throw new CloudServiceException(ResponseCode.ERR_INTERNAL_SERVER_ERROR,"獲取用戶詳情失敗,嘗試訪問的數據不存在!");} else {// -- 否:表示明確的存入了有效數據,則返回此數據即可return cacheResult;}}// 緩存中沒有此id匹配的數據// 從mapper中調用方法,根據id獲取數據庫的數據log.debug("沒有命中緩存,則從數據庫查詢數據……");AdminLoginVO dbResult = adminMapper.getAdminInfoByUserId(id);// 判斷從數據庫中獲取的結果是否為nullif (dbResult == null) {// 是:數據庫也沒有此數據,先向緩存中寫入錯誤數據,再拋出異常log.warn("數據庫中也無此數據(id={}),先向緩存中寫入錯誤數據", id);redisRepository.saveEmptyValue(id);log.warn("拋出異常");throw new CloudServiceException(ResponseCode.ERR_INTERNAL_SERVER_ERROR,"獲取用戶信息失敗,嘗試訪問的數據不存在!");}// 將從數據庫中查詢到的結果存入到緩存中log.debug("已經從數據庫查詢到匹配的數據,將數據存入緩存……");redisRepository.save(dbResult);// 返回查詢結果log.debug("返回查詢到數據:{}", dbResult);return dbResult;}到這里,基于業務調用的Redis業務調用流程代碼就結束了,你可以在controller中添加新的方法來調用此接口完成測試,博主不再寫了。
但此時還有一個問題,我們不能讓每次數據查詢的時候再去存Redis,否則第一次查詢的時候Redis是空的。基于此,我們需要在系統啟動時就把數據存入Redis中,此法叫做緩存預熱。
緩存預熱
創建預熱類
緩存預熱需要確定哪些數據在系統啟動時就存入數據,我們在Spring Boot內自定義一個組件,他需要實現實現ApplicationRunner,我們和啟動類平級建一個這樣的類,名字叫CachePreLoad:
package com.codingfire.cloud.passport;import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner;public class CachePreLoad implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {System.out.println("CachePreLoad.run()");} }此類用于啟動項目時提前預熱資源,run方法是重寫的啟動類方法,啟動時此方法會被調用,可在這里寫存儲Redis相關的內容。
但卻不可將業務實現直接寫在這個類中,為了項目代碼整體的一致性,我們還將預熱的業務以接口和實現的形式寫在Redis單獨的Repository組件內。
在寫之前,為了防止童鞋們迷惑,博主需要解釋一個問題,我個人也感覺使用用戶模塊做Redis不太恰當,應該是使用店鋪列表,商品列表,品牌列表這樣不太會變的數據做Redis,考慮到再加入新的表增加大家學習難度,所以才在用戶表上面做文章,好在這個用戶表也很簡單,大家在真實場景中可以根據這里的代碼轉嫁到其他的業務下即可,萬不可鉆牛角尖。此處數據只做案例講解使用,并非真實使用場景,但代碼絕對是業務級別的。下面,我們來在Redis組件中增加預熱的代碼。
添加Redis操作接口
在IPassportRedisRepository接口中增加以下接口:
String KEY_ADMIN_LIST = "admins:list";/*** 將用戶的列表存入到Redis中** @param admins 用戶列表*/void save(List<AdminLoginVO> admins);/*** 刪除Redis中各獨立存儲的用戶數據*/void deleteAllItem();/*** 刪除Redis中的用戶列表* @return 如果成功刪除,則返回true,否則返回false*/Boolean deleteList();實現Redis操作接口
接口增加完了,實現類報錯,需要實現新增加的方法:
@Overridepublic void save(List<AdminLoginVO> admins) {for (AdminLoginVO admin : admins) {redisTemplate.opsForList().rightPush(KEY_ADMIN_LIST, admin);}}@Overridepublic void deleteAllItem() {Set<String> keys = redisTemplate.keys(KEY_ADMIN_ITEM_PREFIX + "*");redisTemplate.delete(keys);}@Overridepublic Boolean deleteList() {return redisTemplate.delete(KEY_ADMIN_LIST);}添加調用SQL方法
在IAdminMapper接口中增加方法:
@Select("select * from admin")List<AdminLoginVO> list();由于比較簡單,就把SQL直接寫在注解里來。
添加預熱調用接口
在IAdminService接口中增加預熱方法:
void preloadCache();實現預熱方法
在AdminServiceImpl實現類中實現上面的接口方法:
@Overridepublic void preloadCache() {log.debug("刪除緩存中的用戶列表……");redisRepository.deleteList();log.debug("刪除緩存中的各獨立的用戶數據……");redisRepository.deleteAllItem();log.debug("從數據庫查詢用戶列表……");List<AdminLoginVO> list = adminMapper.list();for (AdminLoginVO admin : list) {log.debug("查詢結果:{}", admin);log.debug("將當前用戶存入到Redis:{}", admin);redisRepository.save(admin);}log.debug("將用戶列表寫入到Redis……");redisRepository.save(list);log.debug("將用戶列表寫入到Redis完成!");}啟動時預熱類調用預熱方法?
最后一步,在預熱緩存類中調用此預熱方法:
package com.codingfire.cloud.passport;import com.codingfire.cloud.passport.service.IAdminService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component;@Component @Slf4j public class CachePreLoad implements ApplicationRunner {@Autowiredprivate IAdminService adminService;@Overridepublic void run(ApplicationArguments args) throws Exception {System.out.println("CachePreLoad.run()");log.debug("準備執行緩存預熱……");adminService.preloadCache();log.debug("緩存預熱完成!");} }測試緩存預熱
其實這一步測試是最簡單的,我們什么都不需要做,直接啟動項目就可以,項目啟動后,我們在控制臺會看到預熱方法運行的輸出:
?下面是啟動前,Redis客戶端內的參數列表:
?下面是項目啟動后,Redis客戶端內的參數列表:
?到這一步,若果你的測試結果和博主一樣,那么恭喜你,你已經完成了Redis的基本學習,趕快到自己的項目中使用吧。
結語
Redis學習到這里就結束了,美中不足是少了布隆過濾器對Redis做的一個防止緩存穿透的操作,建議大家可以自己寫寫,后期博主會做個補充,建議大家不要等,自己動動手,也不算難。總體上,Redis整體上還算是比較簡單的,用過幾次其實就熟練了,很多東西都是調用API,再配合我們的業務邏輯來寫。行吧,結語也不知道該寫點啥,就是跟大家訴訴苦,太累了,坐的腰酸背疼,熬夜熬的眼疼,覺得寫的不錯,三連支持一下。
總結
以上是生活随笔為你收集整理的Java开发 - Redis初体验的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 帧差法得到运动背景图像
- 下一篇: 如何在Java中以编程方式阅读,添加或删