Redis數據庫
靈魂拷問:不是學了MySQL嗎,存數據也能存了啊,又學一個數據庫干嘛?
在前面我們學習了MySQL數據庫,它是一種傳統的關系型數據庫,我們可以使用MySQL來更好地管理和組織我們的數據,雖然在小型Web應用下,只需要一個MySQL+Mybatis自帶的緩存系統就可以勝任大部分的數據存儲工作。
但是MySQL的缺點也很明顯,它的數據始終是存儲在硬盤上的,對於我們的用戶信息這種不需要經常發生修改的內容,使用MySQL存儲確實可以,但是如果是快速更新或是頻繁使用的數據,比如微博熱搜、雙十一秒殺,這些數據不僅要求服務器需要提供更高的響應速度,而且還需要面對短時間內上百萬甚至上千萬次訪問,而MySQL的磁盤IO讀寫性能完全不能滿足上面的需求,能夠滿足上述需求的只有內存,因為速度遠高於磁盤IO。
因此,我們需要尋找一種更好的解決方案,來存儲上述這類特殊數據,彌補MySQL的不足,以應對大數據時代的重重考驗。
NoSQL概論
NoSQL全稱是Not Only SQL(不僅僅是SQL)它是一種非關系型數據庫,相比傳統SQL關系型數據庫,它:
- 不保證關系數據的ACID特性
- 並不遵循SQL標准
- 消除數據之間關聯性
乍一看,這玩意不比MySQL垃圾?我們再來看看它的優勢:
- 遠超傳統關系型數據庫的性能
- 非常易於擴展
- 數據模型更加靈活
- 高可用
這樣,NoSQL的優勢一下就出來了,這不就是我們正要尋找的高並發海量數據的解決方案嗎!
NoSQL數據庫分為以下幾種:
- 鍵值存儲數據庫:所有的數據都是以鍵值方式存儲的,類似於我們之前學過的HashMap,使用起來非常簡單方便,性能也非常高。
- 列存儲數據庫:這部分數據庫通常是用來應對分布式存儲的海量數據。鍵仍然存在,但是它們的特點是指向了多個列。
- 文檔型數據庫:它是以一種特定的文檔格式存儲數據,比如JSON格式,在處理網頁等復雜數據時,文檔型數據庫比傳統鍵值數據庫的查詢效率更高。
- 圖形數據庫:利用類似於圖的數據結構存儲數據,結合圖相關算法實現高速訪問。
其中我們要學習的Redis數據庫,就是一個開源的鍵值存儲數據庫,所有的數據全部存放在內存中,它的性能大大高於磁盤IO,並且它也可以支持數據持久化,他還支持橫向擴展、主從復制等。
實際生產中,我們一般會配合使用Redis和MySQL以發揮它們各自的優勢,取長補短。
Redis安裝和部署
我們這里還是使用Windows安裝Redis服務器,但是官方指定是安裝到Linux服務器上,我們后面學習了Linux之后,再來安裝到Linux服務器上。由於官方並沒有提供Windows版本的安裝包,我們需要另外尋找:
- 官網地址:https://redis.io
- GitHub Windows版本維護地址:https://github.com/tporadowski/redis/releases
基本操作
在我們之前使用MySQL時,我們需要先在數據庫中創建一張表,並定義好表的每個字段內容,最后再通過insert
語句向表中添加數據,而Redis並不具有MySQL那樣的嚴格的表結構,Redis是一個鍵值數據庫。
因此,可以像Map一樣的操作方式,通過鍵值對向Redis數據庫中添加數據(操作起來類似於向一個HashMap中存放數據)
在Redis下,數據庫是由一個整數索引標識,而不是由一個數據庫名稱。 默認情況下,我們連接Redis數據庫之后,會使用0號數據庫,我們可以通過Redis配置文件中的參數來修改數據庫總數,默認為16個。
我們可以通過select
語句進行切換:
select 序號;
數據操作
我們來看看,如何向Redis數據庫中添加數據:
set <key> <value>
-- 一次性多個
mset [<key> <value>]...
所有存入的數據默認會以字符串的形式保存,鍵值具有一定的命名規范,以方便我們可以快速定位我們的數據屬於哪一個部分,比如用戶的數據:
-- 使用冒號來進行板塊分割,比如下面表示用戶XXX的信息中的name屬性,值為lbw
set user:info:用戶ID:name lbw
我們可以通過鍵值獲取存入的值:
get <key>
你以為Redis就僅僅只是存取個數據嗎?它還支持數據的過期時間設定:
set <key> <value> EX 秒
set <key> <value> PX 毫秒
當數據到達指定時間時,會被自動刪除。我們也可以單獨為其他的鍵值對設置過期時間:
expire <key> 秒
通過下面的命令來查詢某個鍵值對的過期時間還剩多少:
ttl <key>
-- 毫秒顯示
pttl <key>
-- 轉換為永久
persist <key>
那么當我們想直接刪除這個數據時呢?直接使用:
del <key>...
刪除命令可以同時拼接多個鍵值一起刪除。
當我們想要查看數據庫中所有的鍵值時:
keys *
也可以查詢某個鍵是否存在:
exists <key>...
還可以隨機拿一個鍵:
randomkey
我們可以將一個數據庫中的內容移動到另一個數據庫中:
move <key> 數據庫序號
修改一個鍵為另一個鍵:
rename <key> <新的名稱>
-- 下面這個會檢查新的名稱是否已經存在
renamex <key> <新的名稱>
如果存放的數據是一個數字,我們還可以對其進行自增自減操作:
-- 等價於a = a + 1
incr <key>
-- 等價於a = a + b
incrby <key> b
-- 等價於a = a - 1
decr <key>
最后就是查看值的數據類型:
type <key>
Redis數據庫也支持多種數據類型,但是它更偏向於我們在Java中認識的那些數據類型。
數據類型介紹
一個鍵值對除了存儲一個String類型的值以外,還支持多種常用的數據類型。
Hash
這種類型本質上就是一個HashMap,也就是嵌套了一個HashMap罷了,在Java中就像這樣:
#Redis默認存String類似於這樣:
Map<String, String> hash = new HashMap<>();
#Redis存Hash類型的數據類似於這樣:
Map<String, Map<String, String>> hash = new HashMap<>();
它比較適合存儲類這樣的數據,由於值本身又是一個Map,因此我們可以在此Map中放入類的各種屬性和值,以實現一個Hash數據類型存儲一個類的數據。
我們可以像這樣來添加一個Hash類型的數據:
hset <key> [<字段> <值>]...
我們可以直接獲取:
hget <key> <字段>
-- 如果想要一次性獲取所有的字段和值
hgetall <key>
同樣的,我們也可以判斷某個字段是否存在:
hexists <key> <字段>
刪除Hash中的某個字段:
hdel <key>
我們發現,在操作一個Hash時,實際上就是我們普通操作命令前面添加一個h
,這樣就能以同樣的方式去操作Hash里面存放的鍵值對了,這里就不一一列出所有的操作了。我們來看看幾個比較特殊的。
我們現在想要知道Hash中一共存了多少個鍵值對:
hlen <key>
我們也可以一次性獲取所有字段的值:
hvals <key>
唯一需要注意的是,Hash中只能存放字符串值,不允許出現嵌套的的情況。
List
我們接着來看List類型,實際上這個猜都知道,它就是一個列表,而列表中存放一系列的字符串,它支持隨機訪問,支持雙端操作,就像我們使用Java中的LinkedList一樣。
我們可以直接向一個已存在或是不存在的List中添加數據,如果不存在,會自動創建:
-- 向列表頭部添加元素
lpush <key> <element>...
-- 向列表尾部添加元素
rpush <key> <element>...
-- 在指定元素前面/后面插入元素
linsert <key> before/after <指定元素> <element>
同樣的,獲取元素也非常簡單:
-- 根據下標獲取元素
lindex <key> <下標>
-- 獲取並移除頭部元素
lpop <key>
-- 獲取並移除尾部元素
rpop <key>
-- 獲取指定范圍內的
lrange <key> start stop
注意下標可以使用負數來表示從后到前數的數字(Python:擱這兒抄呢是吧):
-- 獲取列表a中的全部元素
lrange a 0 -1
沒想到吧,push和pop還能連着用呢:
-- 從前一個數組的最后取一個數出來放到另一個數組的頭部,並返回元素
rpoplpush 當前數組 目標數組
它還支持阻塞操作,類似於生產者和消費者,比如我們想要等待列表中有了數據后再進行pop操作:
-- 如果列表中沒有元素,那么就等待,如果指定時間(秒)內被添加了數據,那么就執行pop操作,如果超時就作廢,支持同時等待多個列表,只要其中一個列表有元素了,那么就能執行
blpop <key>... timeout
Set和SortedSet
Set集合其實就像Java中的HashSet一樣(我們在JavaSE中已經講解過了,HashSet本質上就是利用了一個HashMap,但是Value都是固定對象,僅僅是Key不同)它不允許出現重復元素,不支持隨機訪問,但是能夠利用Hash表提供極高的查找效率。
向Set中添加一個或多個值:
sadd <key> <value>...
查看Set集合中有多少個值:
scard <key>
判斷集合中是否包含:
-- 是否包含指定值
sismember <key> <value>
-- 列出所有值
smembers <key>
集合之間的運算:
-- 集合之間的差集
sdiff <key1> <key2>
-- 集合之間的交集
sinter <key1> <key2>
-- 求並集
sunion <key1> <key2>
-- 將集合之間的差集存到目標集合中
sdiffstore 目標 <key1> <key2>
-- 同上
sinterstore 目標 <key1> <key2>
-- 同上
sunionstore 目標 <key1> <key2>
移動指定值到另一個集合中:
smove <key> 目標 value
移除操作:
-- 隨機移除一個幸運兒
spop <key>
-- 移除指定
srem <key> <value>...
那么如果我們要求Set集合中的數據按照我們指定的順序進行排列怎么辦呢?這時就可以使用SortedSet,它支持我們為每個值設定一個分數,分數的大小決定了值的位置,所以它是有序的。
我們可以添加一個帶分數的值:
zadd <key> [<value> <score>]...
同樣的:
-- 查詢有多少個值
zcard <key>
-- 移除
zrem <key> <value>...
-- 獲取區間內的所有
zrange <key> start stop
由於所有的值都有一個分數,我們也可以根據分數段來獲取:
-- 通過分數段查看
zrangebyscore <key> start stop [withscores] [limit]
-- 統計分數段內的數量
zcount <key> start stop
-- 根據分數獲取指定值的排名
zrank <key> <value>
https://www.jianshu.com/p/32b9fe8c20e1
有關Bitmap、HyperLogLog和Geospatial等數據類型,這里暫時不做介紹,感興趣可以自行了解。
持久化
我們知道,Redis數據庫中的數據都是存放在內存中,雖然很高效,但是這樣存在一個非常嚴重的問題,如果突然停電,那我們的數據不就全部丟失了嗎?
它不像硬盤上的數據,斷電依然能夠保存。
這個時候我們就需要持久化,我們需要將我們的數據備份到硬盤上,防止斷電或是機器故障導致的數據丟失。
持久化的實現方式有兩種方案:一種是直接保存當前已經存儲的數據,相當於復制內存中的數據到硬盤上,需要恢復數據時直接讀取即可;還有一種就是保存我們存放數據的所有過程,需要恢復數據時,只需要將整個過程完整地重演一遍就能保證與之前數據庫中的內容一致。
RDB
RDB就是我們所說的第一種解決方案,那么如何將數據保存到本地呢?我們可以使用命令:
save
-- 注意上面這個命令是直接保存,會占用一定的時間,也可以單獨開一個子進程后台執行保存
bgsave
執行后,會在服務端目錄下生成一個dump.rdb文件,而這個文件中就保存了內存中存放的數據,當服務器重啟后,會自動加載里面的內容到對應數據庫中。保存后我們可以關閉服務器:
shutdown
重啟后可以看到數據依然存在。
雖然這種方式非常方便,但是由於會完整復制所有的數據,如果數據庫中的數據量比較大,那么復制一次可能就需要花費大量的時間,所以我們可以每隔一段時間自動進行保存;還有就是,如果我們基本上都是在進行讀操作,而沒有進行寫操作,實際上只需要偶爾保存一次即可,因為數據幾乎沒有怎么變化,可能兩次保存的都是一樣的數據。
我們可以在配置文件中設置自動保存,並設定在一段時間內寫入多少數據時,執行一次保存操作:
save 300 10 # 300秒(5分鍾)內有10個寫入
save 60 10000 # 60秒(1分鍾)內有10000個寫入
配置的save使用的都是bgsave后台執行。
AOF
雖然RDB能夠很好地解決數據持久化問題,但是它的缺點也很明顯:每次都需要去完整地保存整個數據庫中的數據,同時后台保存過程中也會產生額外的內存開銷,最嚴重的是它並不是實時保存的,如果在自動保存觸發之前服務器崩潰,那么依然會導致少量數據的丟失。
而AOF就是另一種方式,它會以日志的形式將我們每次執行的命令都進行保存,服務器重啟時會將所有命令依次執行,通過這種重演的方式將數據恢復,這樣就能很好解決實時性存儲問題。
但是,我們多久寫一次日志呢?我們可以自己配置保存策略,有三種策略:
- always:每次執行寫操作都會保存一次
- everysec:每秒保存一次(默認配置),這樣就算丟失數據也只會丟一秒以內的數據
- no:看系統心情保存
可以在配置文件中配置:
# 注意得改成也是
appendonly yes
# appendfsync always
appendfsync everysec
# appendfsync no
重啟服務器后,可以看到服務器目錄下多了一個appendonly.aof
文件,存儲的就是我們執行的命令。
AOF的缺點也很明顯,每次服務器啟動都需要進行過程重演,相比RDB更加耗費時間,並且隨着我們的操作變多,不斷累計,可能到最后我們的aof文件會變得無比巨大,我們需要一個改進方案來優化這些問題。
Redis有一個AOF重寫機制進行優化,比如我們執行了這樣的語句:
lpush test 666
lpush test 777
lpush test 888
實際上用一條語句也可以實現:
lpush test 666 777 888
正是如此,只要我們能夠保證最終的重演結果和原有語句的結果一致,無論語句如何修改都可以,所以我們可以通過這種方式將多條語句進行壓縮。
我們可以輸入命令來手動執行重寫操作:
bgrewriteaof
或是在配置文件中配置自動重寫:
# 百分比計算,這里不多介紹
auto-aof-rewrite-percentage 100
# 當達到這個大小時,觸發自動重寫
auto-aof-rewrite-min-size 64mb
至此,我們就完成了兩種持久化方案的介紹,最后我們再來進行一下總結:
- AOF:
- 優點:存儲速度快、消耗資源少、支持實時存儲
- 缺點:加載速度慢、數據體積大
- RDB:
- 優點:加載速度快、數據體積小
- 缺點:存儲速度慢大量消耗資源、會發生數據丟失
事務和鎖機制
和MySQL一樣,在Redis中也有事務機制,當我們需要保證多條命令一次性完整執行而中途不受到其他命令干擾時,就可以使用事務機制。
我們可以使用命令來直接開啟事務:
multi
當我們輸入完所有要執行的命令時,可以使用命令來立即執行事務:
exec
我們也可以中途取消事務:
discard
實際上整個事務是創建了一個命令隊列,它不像MySQL那種在事務中也能單獨得到結果,而是我們提前將所有的命令裝在隊列中,但是並不會執行,而是等我們提交事務的時候再統一執行。
鎖
又提到鎖了,實際上這個概念對我們來說已經不算是陌生了。
實際上在Redis中也會出現多個命令同時競爭同一個數據的情況,比如現在有兩條命令同時執行,他們都要去修改a的值,那么這個時候就只能動用鎖機制來保證同一時間只能有一個命令操作。
雖然Redis中也有鎖機制,但是它是一種樂觀鎖,不同於MySQL,我們在MySQL中認識的鎖是悲觀鎖,那么什么是樂觀鎖什么是悲觀鎖呢?
- 悲觀鎖:時刻認為別人會來搶占資源,禁止一切外來訪問,直到釋放鎖,具有強烈的排他性質。
- 樂觀鎖:並不認為會有人來搶占資源,所以會直接對數據進行操作,在操作時再去驗證是否有其他人搶占資源。
Redis中可以使用watch來監視一個目標,如果執行事務之前被監視目標發生了修改,則取消本次事務:
watch
我們可以開兩個客戶端進行測試。
取消監視可以使用:
unwatch
至此,Redis的基礎內容就講解完畢了,在之后的SpringCloud階段,我們還會去講解集群相關的知識,包括主從復制、哨兵模式等。
使用Java與Redis交互
既然了解了如何通過命令窗口操作Redis數據庫,那么我們如何使用Java來操作呢?
這里我們需要使用到Jedis框架,它能夠實現Java與Redis數據庫的交互,依賴:
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
基本操作
我們來看看如何連接Redis數據庫,非常簡單,只需要創建一個對象即可:
public static void main(String[] args) {
//創建Jedis對象
Jedis jedis = new Jedis("localhost", 6379);
//使用之后關閉連接
jedis.close();
}
通過Jedis對象,我們就可以直接調用命令的同名方法來執行Redis命令了,比如:
public static void main(String[] args) {
//直接使用try-with-resouse,省去close
try(Jedis jedis = new Jedis("192.168.10.3", 6379)){
jedis.set("test", "lbwnb"); //等同於 set test lbwnb 命令
System.out.println(jedis.get("test")); //等同於 get test 命令
}
}
Hash類型的數據也是這樣:
public static void main(String[] args) {
try(Jedis jedis = new Jedis("192.168.10.3", 6379)){
jedis.hset("hhh", "name", "sxc"); //等同於 hset hhh name sxc
jedis.hset("hhh", "sex", "19"); //等同於 hset hhh age 19
jedis.hgetAll("hhh").forEach((k, v) -> System.out.println(k+": "+v));
}
}
我們接着來看看列表操作:
public static void main(String[] args) {
try(Jedis jedis = new Jedis("192.168.10.3", 6379)){
jedis.lpush("mylist", "111", "222", "333"); //等同於 lpush mylist 111 222 333 命令
jedis.lrange("mylist", 0, -1)
.forEach(System.out::println); //等同於 lrange mylist 0 -1
}
}
實際上我們只需要按照對應的操作去調用同名方法即可,所有的類型封裝Jedis已經幫助我們完成了。
SpringBoot整合Redis
我們接着來看如何在SpringBoot項目中整合Redis操作框架,只需要一個starter即可,但是它底層沒有用Jedis,而是Lettuce:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
starter提供的默認配置會去連接本地的Redis服務器,並使用0號數據庫,當然你也可以手動進行修改:
spring:
redis:
#Redis服務器地址
host: 192.168.10.3
#端口
port: 6379
#使用幾號數據庫
database: 0
starter已經給我們提供了兩個默認的模板類:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
那么如何去使用這兩個模板類呢?
我們可以直接注入StringRedisTemplate
來使用模板:
@SpringBootTest
class SpringBootTestApplicationTests {
@Autowired
StringRedisTemplate template;
@Test
void contextLoads() {
ValueOperations<String, String> operations = template.opsForValue();
operations.set("c", "xxxxx"); //設置值
System.out.println(operations.get("c")); //獲取值
template.delete("c"); //刪除鍵
System.out.println(template.hasKey("c")); //判斷是否包含鍵
}
}
實際上所有的值的操作都被封裝到了ValueOperations
對象中,而普通的鍵操作直接通過模板對象就可以使用了,大致使用方式其實和Jedis一致。
我們接着來看看事務操作,由於Spring沒有專門的Redis事務管理器,所以只能借用JDBC提供的,只不過無所謂,正常情況下反正我們也要用到這玩意:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
@Service
public class RedisService {
@Resource
StringRedisTemplate template;
@PostConstruct
public void init(){
template.setEnableTransactionSupport(true); //需要開啟事務
}
@Transactional //需要添加此注解
public void test(){
template.multi();
template.opsForValue().set("d", "xxxxx");
template.exec();
}
}
我們還可以為RedisTemplate對象配置一個Serializer來實現對象的JSON存儲:
@Test
void contextLoad2() {
//注意Student需要實現序列化接口才能存入Redis
template.opsForValue().set("student", new Student());
System.out.println(template.opsForValue().get("student"));
}
使用Redis做緩存
我們可以輕松地使用Redis來實現一些框架的緩存和其他存儲。
Mybatis二級緩存
還記得我們在學習Mybatis講解的緩存機制嗎,我們當時介紹了二級緩存,它是Mapper級別的緩存,能夠作用與所有會話。
但是當時我們提出了一個問題,由於Mybatis的默認二級緩存只能是單機的,如果存在多台服務器訪問同一個數據庫,實際上二級緩存只會在各自的服務器上生效,但是我們希望的是多台服務器都能使用同一個二級緩存,這樣就不會造成過多的資源浪費。
我們可以將Redis作為Mybatis的二級緩存,這樣就能實現多台服務器使用同一個二級緩存,
因為它們只需要連接同一個Redis服務器即可,所有的緩存數據全部存儲在Redis服務器上。
我們需要手動實現Mybatis提供的Cache接口,這里我們簡單編寫一下:
//實現Mybatis的Cache接口
public class RedisMybatisCache implements Cache {
private final String id;
private static RedisTemplate<Object, Object> template;
//注意構造方法必須帶一個String類型的參數接收id
public RedisMybatisCache(String id){
this.id = id;
}
//初始化時通過配置類將RedisTemplate給過來
public static void setTemplate(RedisTemplate<Object, Object> template) {
RedisMybatisCache.template = template;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object o, Object o1) {
//這里直接向Redis數據庫中丟數據即可,o就是Key,o1就是Value,60秒為過期時間
template.opsForValue().set(o, o1, 60, TimeUnit.SECONDS);
}
@Override
public Object getObject(Object o) {
//這里根據Key直接從Redis數據庫中獲取值即可
return template.opsForValue().get(o);
}
@Override
public Object removeObject(Object o) {
//根據Key刪除
return template.delete(o);
}
@Override
public void clear() {
//由於template中沒封裝清除操作,只能通過connection來執行
template.execute((RedisCallback<Void>) connection -> {
//通過connection對象執行清空操作
connection.flushDb();
return null;
});
}
@Override
public int getSize() {
//這里也是使用connection對象來獲取當前的Key數量
return template.execute(RedisServerCommands::dbSize).intValue();
}
}
緩存類編寫完成后,我們接着來編寫配置類:
@Configuration
public class MainConfiguration {
@Resource
RedisTemplate<Object, Object> template;
@PostConstruct
public void init(){
//把RedisTemplate給到RedisMybatisCache
RedisMybatisCache.setTemplate(template);
}
}
最后我們在Mapper上啟用此緩存即可:
//只需要修改緩存實現類implementation為我們的RedisMybatisCache即可
@CacheNamespace(implementation = RedisMybatisCache.class)
@Mapper
public interface MainMapper {
@Select("select name from student where sid = 1")
String getSid();
}
最后我們提供一個測試用例來查看當前的二級緩存是否生效:
@SpringBootTest
class SpringBootTestApplicationTests {
@Resource
MainMapper mapper;
@Test
void contextLoads() {
System.out.println(mapper.getSid());
System.out.println(mapper.getSid());
System.out.println(mapper.getSid());
}
}
手動使用客戶端查看Redis數據庫,可以看到已經有一條Mybatis生成的緩存數據了。
Token持久化存儲
我們之前使用SpringSecurity時,remember-me的Token是支持持久化存儲的,而我們當時是存儲在數據庫中,那么Token信息能否存儲在緩存中呢,當然也是可以的,我們可以手動實現一個:
//實現PersistentTokenRepository接口
@Component
public class RedisTokenRepository implements PersistentTokenRepository {
//Key名稱前綴,用於區分
private final static String REMEMBER_ME_KEY = "spring:security:rememberMe:";
@Resource
RedisTemplate<Object, Object> template;
@Override
public void createNewToken(PersistentRememberMeToken token) {
//這里要放兩個,一個存seriesId->Token,一個存username->seriesId,因為刪除時是通過username刪除
template.opsForValue().set(REMEMBER_ME_KEY+"username:"+token.getUsername(), token.getSeries());
template.expire(REMEMBER_ME_KEY+"username:"+token.getUsername(), 1, TimeUnit.DAYS);
this.setToken(token);
}
//先獲取,然后修改創建一個新的,再放入
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentRememberMeToken token = this.getToken(series);
if(token != null)
this.setToken(new PersistentRememberMeToken(token.getUsername(), series, tokenValue, lastUsed));
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
return this.getToken(seriesId);
}
//通過username找seriesId直接刪除這兩個
@Override
public void removeUserTokens(String username) {
String series = (String) template.opsForValue().get(REMEMBER_ME_KEY+"username:"+username);
template.delete(REMEMBER_ME_KEY+series);
template.delete(REMEMBER_ME_KEY+"username:"+username);
}
//由於PersistentRememberMeToken沒實現序列化接口,這里只能用Hash來存儲了,所以單獨編寫一個set和get操作
private PersistentRememberMeToken getToken(String series){
Map<Object, Object> map = template.opsForHash().entries(REMEMBER_ME_KEY+series);
if(map.isEmpty()) return null;
return new PersistentRememberMeToken(
(String) map.get("username"),
(String) map.get("series"),
(String) map.get("tokenValue"),
new Date(Long.parseLong((String) map.get("date"))));
}
private void setToken(PersistentRememberMeToken token){
Map<String, String> map = new HashMap<>();
map.put("username", token.getUsername());
map.put("series", token.getSeries());
map.put("tokenValue", token.getTokenValue());
map.put("date", ""+token.getDate().getTime());
template.opsForHash().putAll(REMEMBER_ME_KEY+token.getSeries(), map);
template.expire(REMEMBER_ME_KEY+token.getSeries(), 1, TimeUnit.DAYS);
}
}
接着把驗證Service實現了:
@Service
public class AuthService implements UserDetailsService {
@Resource
UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = mapper.getAccountByUsername(username);
if(account == null) throw new UsernameNotFoundException("");
return User
.withUsername(username)
.password(account.getPassword())
.roles(account.getRole())
.build();
}
}
Mapper也安排上:
@Data
public class Account implements Serializable {
int id;
String username;
String password;
String role;
}
@CacheNamespace(implementation = MybatisRedisCache.class)
@Mapper
public interface UserMapper {
@Select("select * from users where username = #{username}")
Account getAccountByUsername(String username);
}
最后配置文件配一波:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.tokenRepository(repository);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(service)
.passwordEncoder(new BCryptPasswordEncoder());
}
OK,啟動服務器驗證一下吧。
三大緩存問題
推薦閱讀:緩存穿透,擊穿,雪崩詳解
雖然我們可以利用緩存來大幅度提升我們程序的數據獲取效率,但是使用緩存也存在着一些潛在的問題。
緩存穿透
當我們去查詢一個一定不存在的數據,比如Mybatis在緩存是未命中的情況下需要從數據庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,造成緩存穿透。
這顯然是很浪費資源的,我們希望的是,如果這個數據不存在,為什么緩存這一層不直接返回空呢,這時就不必再去查數據庫了,但是也有一個問題,緩存不去查數據庫怎么知道數據庫里面到底有沒有這個數據呢?
這時我們就可以使用布隆過濾器來進行判斷。什么是布隆過濾器?(當然不是打輔助的那個布隆,只不過也挺像,輔助布隆也是擋子彈的)
使用布隆過濾器,能夠告訴你某樣東西一定不存在或是某樣東西可能存在。
布隆過濾器本質是一個存放二進制位的bit數組,如果我們要添加一個值到布隆過濾器中,我們需要使用N個不同的哈希函數來生成N個哈希值,並對每個生成的哈希值指向的bit位置1,如上圖所示,一共添加了三個值abc。
接着我們給一個d,那么這時就可以進行判斷,如果說d計算的N個哈希值的位置上都是1,那么就說明d可能存在;這時候又來了個e,計算后我們發現有一個位置上的值是0,這時就可以直接斷定e一定不存在。
緩存擊穿
某個 Key 屬於熱點數據,訪問非常頻繁,同一時間很多人都在訪問,在這個Key失效的瞬間,大量的請求到來,這時發現緩存中沒有數據,就全都直接請求數據庫,相當於擊穿了緩存屏障,直接攻擊整個系統核心。
這種情況下,最好的解決辦法就是不讓Key那么快過期,如果一個Key處於高頻訪問,那么可以適當地延長過期時間。
緩存雪崩
當你的Redis服務器炸了或是大量的Key在同一時間過期,這時相當於緩存直接GG了,那么如果這時又有很多的請求來訪問不同的數據,同一時間內緩存服務器就得向數據庫大量發起請求來重新建立緩存,很容易把數據庫也搞GG。
解決這種問題最好的辦法就是設置高可用,也就是搭建Redis集群,當然也可以采取一些服務熔斷降級機制。