文章開始之前先做個找工作的介紹吧,由于這次疫情影響,我一個UI朋友的公司破產之后他現在處于找工作階段,一直沒有找到自己合適的,三年工作經驗左右,坐標深圳,如果有招UI的朋友可以聯系我。
作品: http://yiming.zcool.com.cn/
目錄 什么是緩存 人在地上走,鍋從天上來 緩存穿透 緩存擊穿 緩存雪崩 總結
緩存是互聯網開發中必不可少的一部分,它能降低我們數據庫的并發數,提高我們系統的性能,比如我們經常使用的redis、emCached等等,其中redis應該是大部分的人選,為什么?因為速度快,易上手,是很多開發者的首選,但是緩存同樣存在這問題,如果使用的不恰當,也可能會造成非常嚴重的后果,這時候你可能就會有疑問,緩存只是存儲一些數據而已,怎么會造成嚴重的后果呢?下面我就帶大家一起來分析分析。
什么是緩存 緩存(cache),原始意義是指訪問速度比一般隨機存取存儲器(RAM)快的一種高速存儲器,通常它不像系統主存那樣使用DRAM技術,而使用昂貴但較快速的SRAM技術。緩存的設置是所有現代計算機系統發揮高性能的重要因素之一。
比如我們的redis、他就是緩存中比較常見的一種,他的并發讀寫能力能達到10w/s左右的速度,這個速度是相當不錯的,相對于傳統的數據存儲來說,比如數據庫,快了不知道多少倍,傳統的數據庫(mysql)操作的都是磁盤,而redis操作的是內存(ram),所以他們的速度肯定是沒法比較的,由于傳統數據庫的讀寫較慢,所以并發較高的時候就會造成性能瓶頸問題,這也是為什么需要引入緩存的原因之一。
人在地上走,鍋從天上來 我是一個快樂的程序狗,每天最快樂的事情就是codding,最大的愿望就是能準時6點下班,然后回家,但是今天肯定是走不了了,現在是17:30,我們的測試小哥哥給我提了一個很詭異的bug,難受啊,我的準時下班夢,但是作為一個程序狗,肯定都有著一顆和bug戰斗到底的決心,究竟是什么bug呢?
bug是這樣的:并發請求訂單信息,沒過幾秒就拋出系統錯誤。這個bug看著沒幾個字,但是一看就知道不好解決,尤其是像這種并發bug,能讓人瞬間白了頭,隨后我找到了測試,讓他們復現了這個神秘的bug,而我也找到了產生這個bug的來源,并且快速的修復了他,到底是什么問題呢?是因為小明(同事)在編寫代碼的時候考慮的不是很周全導致的,所以開發一定要想仔細了再動手,否則吃虧的就是自己啊。
緩存穿透 什么是緩存穿透 緩存穿透指的是:同一時刻,大量的并發請求數據庫中不存在的信息,他既不會命中緩存,也不會命中數據庫,但是他會查找數據庫。
上面的bug也是因為它產生的,測試的小哥哥查詢的訂單都是數據庫不存在的,所以這個時候這些并發請求都不會命中緩存(redis),將直達數據庫(mysql),由于大量的并發請求到達數據庫,而數據庫承受不住這么高的并發,從而導致數據庫奔潰,這就是緩存穿透。
重現bug
1.新建數據表:訂單表,結構如下: 2.編寫測試代碼
OrderBo.java
package com
. ymy
. bo
;
import lombok. Data;
import java. io. Serializable; import java. math. BigDecimal;
@Data public class OrderBo implements Serializable {
<span class="token comment">/*** 自增id*/</span>
<span class="token keyword">private</span> Long id<span class="token punctuation">;</span><span class="token comment">/***訂單編號*/</span>
<span class="token keyword">private</span> Long orderCode<span class="token punctuation">;</span><span class="token comment">/***訂單價格*/</span>
<span class="token keyword">private</span> BigDecimal orderPrice<span class="token punctuation">;</span><span class="token comment">/***商品名稱*/</span>
<span class="token keyword">private</span> String peoductName<span class="token punctuation">;</span><span class="token comment">/***創建時間*/</span>
<span class="token keyword">private</span> String createTime<span class="token punctuation">;</span>
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
OrderController.java
package com
. ymy
. controller
;
import com. ymy. bo. OrderBo; import com. ymy. service. OrderService; import org. springframework. web. bind. annotation. RequestMapping; import org. springframework. web. bind. annotation. RequestMethod; import org. springframework. web. bind. annotation. RequestParam; import org. springframework. web. bind. annotation. RestController;
@RestController public class OrderController {
<span class="token keyword">private</span> OrderService orderService<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token function">OrderController</span><span class="token punctuation">(</span>OrderService orderService<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>orderService <span class="token operator">=</span> orderService<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token annotation punctuation">@RequestMapping</span><span class="token punctuation">(</span>value <span class="token operator">=</span> <span class="token string">"/detail"</span><span class="token punctuation">,</span>method <span class="token operator">=</span> RequestMethod<span class="token punctuation">.</span>GET<span class="token punctuation">)</span>
<span class="token keyword">public</span> OrderBo <span class="token function">getDetail</span><span class="token punctuation">(</span><span class="token annotation punctuation">@RequestParam</span><span class="token punctuation">(</span><span class="token string">"id"</span><span class="token punctuation">)</span> Long id<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">return</span> orderService<span class="token punctuation">.</span><span class="token function">getDetail</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
OrderService.java
package com
. ymy
. service
;
import com. ymy. bo. OrderBo; import com. ymy. mapper. OrderMapper; import lombok. extern. slf4j. Slf4j; import org. springframework. data. redis. core. RedisTemplate; import org. springframework. stereotype. Service;
@Service @Slf4j public class OrderService {
<span class="token keyword">private</span> RedisTemplate redisTemplate<span class="token punctuation">;</span><span class="token keyword">private</span> OrderMapper orderMapper<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token function">OrderService</span><span class="token punctuation">(</span>RedisTemplate redisTemplate<span class="token punctuation">,</span>OrderMapper orderMapper<span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>redisTemplate <span class="token operator">=</span> redisTemplate<span class="token punctuation">;</span><span class="token keyword">this</span><span class="token punctuation">.</span>orderMapper <span class="token operator">=</span> orderMapper<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">/*** 通過id查詢訂單詳情* @param id* @return*/</span>
<span class="token keyword">public</span> OrderBo <span class="token function">getDetail</span><span class="token punctuation">(</span>Long id<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token comment">//緩存中查詢詞詞訂單</span>OrderBo orderBo <span class="token operator">=</span> <span class="token punctuation">(</span>OrderBo<span class="token punctuation">)</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span>orderBo <span class="token operator">!=</span> null <span class="token punctuation">)</span><span class="token punctuation">{</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"緩存中查詢到了信息,直接返回:{}"</span><span class="token punctuation">,</span>orderBo<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">return</span> orderBo<span class="token punctuation">;</span><span class="token punctuation">}</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"前往數據庫查詢"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>orderBo <span class="token operator">=</span> orderMapper<span class="token punctuation">.</span><span class="token function">getDetail</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span><span class="token punctuation">(</span>orderBo <span class="token operator">!=</span> null <span class="token punctuation">)</span><span class="token punctuation">{</span><span class="token comment">//將數據保存到數據庫,有效時間一小時</span>redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">,</span>orderBo<span class="token punctuation">,</span><span class="token number">3600</span><span class="token punctuation">,</span>TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"數據已經存入緩存"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span> orderBo<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
OrderMapper.java
package com
. ymy
. mapper
;
import com. ymy. bo. OrderBo; import org. apache. ibatis. annotations. Mapper; import org. apache. ibatis. annotations. Select;
@Mapper public interface OrderMapper { @Select ( " select id,order_code as orderCode,order_price as orderPrice,peoduct_name as peoductName,create_time as createTime from orders where id = #{id} " ) OrderBo getDetail ( Long id) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
RedisConfig.java
package com
. ymy
. config
;
import com. fasterxml. jackson. annotation. JsonAutoDetect; import com. fasterxml. jackson. annotation. PropertyAccessor; import com. fasterxml. jackson. databind. ObjectMapper; 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. Jackson2JsonRedisSerializer; import org. springframework. data. redis. serializer. StringRedisSerializer;
@Configuration public class RedisConfig {
<span class="token annotation punctuation">@Bean</span>
<span class="token keyword">public</span> RedisTemplate<span class="token generics function"><span class="token punctuation"><</span>Object<span class="token punctuation">,</span> Object<span class="token punctuation">></span></span> <span class="token function">redisTemplate</span><span class="token punctuation">(</span>RedisConnectionFactory redisConnectionFactory<span class="token punctuation">)</span> <span class="token punctuation">{</span>RedisTemplate<span class="token generics function"><span class="token punctuation"><</span>Object<span class="token punctuation">,</span> Object<span class="token punctuation">></span></span> redisTemplate <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">RedisTemplate</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>redisTemplate<span class="token punctuation">.</span><span class="token function">setConnectionFactory</span><span class="token punctuation">(</span>redisConnectionFactory<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// 使用Jackson2JsonRedisSerialize 替換默認序列化</span>Jackson2JsonRedisSerializer jackson2JsonRedisSerializer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Jackson2JsonRedisSerializer</span><span class="token punctuation">(</span>Object<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span>ObjectMapper objectMapper <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ObjectMapper</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>objectMapper<span class="token punctuation">.</span><span class="token function">setVisibility</span><span class="token punctuation">(</span>PropertyAccessor<span class="token punctuation">.</span>ALL<span class="token punctuation">,</span> JsonAutoDetect<span class="token punctuation">.</span>Visibility<span class="token punctuation">.</span>ANY<span class="token punctuation">)</span><span class="token punctuation">;</span>objectMapper<span class="token punctuation">.</span><span class="token function">enableDefaultTyping</span><span class="token punctuation">(</span>ObjectMapper<span class="token punctuation">.</span>DefaultTyping<span class="token punctuation">.</span>NON_FINAL<span class="token punctuation">)</span><span class="token punctuation">;</span>jackson2JsonRedisSerializer<span class="token punctuation">.</span><span class="token function">setObjectMapper</span><span class="token punctuation">(</span>objectMapper<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// 設置value的序列化規則和 key的序列化規則</span>redisTemplate<span class="token punctuation">.</span><span class="token function">setKeySerializer</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">StringRedisSerializer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>redisTemplate<span class="token punctuation">.</span><span class="token function">setValueSerializer</span><span class="token punctuation">(</span>jackson2JsonRedisSerializer<span class="token punctuation">)</span><span class="token punctuation">;</span>redisTemplate<span class="token punctuation">.</span><span class="token function">setHashKeySerializer</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">StringRedisSerializer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>redisTemplate<span class="token punctuation">.</span><span class="token function">setHashValueSerializer</span><span class="token punctuation">(</span>jackson2JsonRedisSerializer<span class="token punctuation">)</span><span class="token punctuation">;</span>redisTemplate<span class="token punctuation">.</span><span class="token function">afterPropertiesSet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">return</span> redisTemplate<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
上面的代碼實現的功能很簡單,通過訂單id查詢訂單詳情,不過查詢的循序是先查緩存,如果緩存沒有數據,在查詢數據庫,大致流程圖如下: 這個過程很簡單,看上去沒有什么問題,如果你仔細觀察的話就會發現一個致命的問題,就是剛剛說的緩存穿透問題,我們來做個實驗。
我在數據庫提前添加了一條數據,信息如下: 正常情況:查詢id等于1的訂單信息。
第一次:
2020 - 04 - 19 15 : 55 : 35.564 INFO
20188 -- - [ nio
- 9900 - exec
- 1 ] com
. ymy
. service
. OrderService
: 前往數據庫查詢
2020 - 04 - 19 15 : 55 : 35.675 INFO
20188 -- - [ nio
- 9900 - exec
- 1 ] com
. ymy
. service
. OrderService
: 數據已經存入緩存
由于是第一次查詢,所以緩存中不會存在數據,請求直接到達了數據庫,并且獲取到了id為1的數據,并且將數據添加到了緩存。
第二次查詢id等于1的數據
2020 - 04 - 19 15 : 57 : 47.879 INFO
20188 -- - [ nio
- 9900 - exec
- 5 ] com
. ymy
. service
. OrderService
: 緩存中查詢到了信息,直接返回:
OrderBo ( id
= 1 , orderCode
= 202004191416 , orderPrice
= 3299.00 , peoductName
= iphone se2
, createTime
= 2020 - 04 - 19 14 : 17 : 07 )
我們發現他直接命中了緩存,直接返回,這是正常情況,那如果非正常情況呢?比如查詢的訂單id=-1呢?這個時候會發生什么事情?
http://localhost:9900/detail?id=-1 看到沒有,請求全都進入數據庫了,這種情況是肯定不被允許的,如果你的程序中存在這種情況,一定要趕緊修改,否則有可能會讓一些心懷不軌的人直接將數據庫的服務搞宕機,那這種問題如何解決呢?
解決方案
將空數據存入緩存
什么意思呢?簡單點來說,不管數據庫中有沒有查詢到數據,都往緩存中添加一條數據,這樣下次請求的時候就會直接在緩存中返回,這種方式比較簡單粗暴,我們一起看看如何實現。
代碼改造:
OrderService.java
public OrderBo
getDetail ( Long id
) { Object obj
= redisTemplate
. opsForValue ( ) . get ( "order:" + id
) ; if ( obj
!= null
) { String data
= obj
. toString ( ) ; log
. info ( "緩存中查詢到了信息,直接返回:{}" , data
) ; return "" . equals ( data
) ? null
: ( OrderBo
) obj
; } log
. info ( "前往數據庫查詢" ) ; OrderBo orderBo
= orderMapper
. getDetail ( id
) ; if ( orderBo
!= null
) { redisTemplate
. opsForValue ( ) . set ( "order:" + id
, orderBo
, 3600 , TimeUnit
. SECONDS
) ; log
. info ( "數據已經存入緩存" ) ; } else { redisTemplate
. opsForValue ( ) . set ( "order:" + id
, "" , 300 , TimeUnit
. SECONDS
) ; log
. info ( "數據庫中不存在此數據,但是為了防止緩存穿透,存入一條空數據到緩存中" ) ; } return orderBo
; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
往緩存中添加數據的時候一定要注意值的問題,請看我這里,我添加的是一個空字符串,并不是null,是因為我判斷的條件是緩存中!=null就直接返回,如果你往緩存中添加一條null的數據,這個時候就會和你的判斷起沖突,又會進入到數據庫了,所以這點需要特別注意,我們來看測試:
第一次請求:http://localhost:9900/detail?id=-1
2020 - 04 - 19 16 : 23 : 21.520 INFO
16596 -- - [ nio
- 9900 - exec
- 6 ] com
. ymy
. service
. OrderService
: 前往數據庫查詢
2020 - 04 - 19 16 : 23 : 21.577 INFO
16596 -- - [ nio
- 9900 - exec
- 6 ] com
. ymy
. service
. OrderService
: 數據庫中不存在此數據,但是為了防止緩存穿透,存入一條空數據到緩存中
第二次請求:http://localhost:9900/detail?id=-1
2020 - 04 - 19 16 : 24 : 25.855 INFO
16596 -- - [ nio
- 9900 - exec
- 9 ] com
. ymy
. service
. OrderService
: 緩存中查詢到了信息,直接返回:
這個時候請求命中了緩存,就不會前往數據庫中了,但是這個需要注意一點:空值的過期時間不能設置的太長,什么意思呢?設想一下,我們現在數據庫中只有id=1的數據,我們查詢id=2也會往緩存中插入一條數據,但是這個時候數據庫中新增了一條訂單id=2,用戶下次查詢的時候看到你存儲在緩存中中的數據,接直接回了空,但是數據庫中明明已經添加了這條數據,這就是為什么過期時間不要設置太久的原因,當然了,我們也需要分情況考慮,比如查詢id<=0的,我們都可以考慮永久存入緩存或者設置很長的過期時間,推薦設置很長的過期時間,為什么呢?因為訂單id不存在會<=0,但是對于>=0,我們可以將過期時間設置為30秒等等,這個看業務需求即可。
布隆過濾器
布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用于檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都比一般的算法要好的多,缺點是有一定的誤識別率和刪除困難。
這個算法實現起來比上面第一種稍微復雜一點,這里就不具體說明了,如果感興趣的話可以百度自行了解一下,不是很難。
緩存擊穿
什么是緩存擊穿
緩存擊穿是指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的并發請求過來,從而大量的請求打到db(數據庫)。
這個也不難理解,和緩存穿透有點像但是性質又不相同,都是緩存中沒有數據,請求命中數據庫,緩存穿透指的是數據庫中不存在的數據,緩存擊穿則是指緩存失效的問題。,這種情況不太好模擬,我們可以直接將緩存中數據清空,替代緩存數據過期。
代碼還是上面的代碼,不做任何修改,不過我們不再使用postman測試,而是采用jemter,首先我們刪除緩存中的數據,模擬key已經過期,我們查詢id=1的訂單詳細信息,但需要注意的是,我并不是發一個請求,而是100個同時請求,會發生什么呢?
線程數: Http請求信息 聚合報告 我們發現100個并發請求全部成功,異常率為0,接下來就是重點了,控制臺會打印什么呢? 這就是緩存擊穿,是不是很恐怖,雖然命中數據庫的次數不是很多,那是因為我們的并發請求不是很大,像雙十一這種并發,如果存在這種問題,數據庫可能撐不過3秒就炸了。
解決方案
自動更新
什么是自動更新呢?這個有點類似與jwt的自動刷新token機制,jwt的自動刷新token實現原理大致為:請求的時候判斷一下token的剩余有效時間,如果有效時間小于設定的時間,那么jwt將生成一個新的token,然后再將次token重新設置過期時間,并將新的token返回給前端使用,這個也可以參考一下,redis是支持查詢某個key剩余有效時間,所以這里我們只需要設定一個時間差,比如3分鐘,請求的時候查詢的有效時間如果小于3分鐘,那么刷新這個key的有效時間,刷新這個操作可以使用異步實現(提高性能)。
可能你想到了,這種方式存在缺陷,沒錯,如果再快失效的3分鐘內沒有請求,那么緩存中的key將不會被刷新,還是會存在緩存擊穿的問題,所以這種方式不是特別推薦。
定時刷新
定時刷新有兩種方案
第一種:定時任務 查詢快要過期的key,更新內容,并刷新有效時間,這種比較消耗服務器性能,也不是特別推薦。
第二種:延遲隊列 如果大家了解它的話可能一下就知道我說的是什么意思了,將數據存入緩存的那一刻同時發送一個延遲隊列(安指定時間消費),時間小于緩存中key的過期時間,到了指定時間,消費者刷新key的有效時間再發送一個延遲隊列,以此循環,這種方式還是不錯的,但是實現方式相對于第一種來說就要復雜一點了,他需要依靠消息中間件來完成,如果消息中間件某個時間宕機,那就gg了,雖然這種方式雖然比較推薦,但是成本偏高,因為為了防止消息中間件宕機,我們有可能需要對消息中間件做集群處理。
程序加鎖
我個人推薦使用這個,為什么呢?因為它不需要額外的服務器開銷,也不需要額外的資源消耗,他僅僅只是讓線程串行而已,但是這個時候你可能就會有疑問了,加鎖不是會嚴重影響程序的效率嗎?為什么你還推薦這種方式呢?
其實并不是所有的鎖都會很大的降低程序的性能,這里我們當然不能使用synchronized,原因很簡單,他的效率比較慢,不太適合這種情況,我要介紹的這種鎖名字為:讀寫鎖。
什么是讀寫鎖?請參考我的另外一篇博客:【并發編程】java并發編程之ReentrantReadWriteLock讀寫鎖。
好了,我們一起來改造一下之前的代碼
OrderService.java
package com
. ymy
. service
;
import com. ymy. bo. OrderBo; import com. ymy. mapper. OrderMapper; import lombok. extern. slf4j. Slf4j; import org. springframework. data. redis. core. RedisTemplate; import org. springframework. stereotype. Service;
import java. util. concurrent. TimeUnit; import java. util. concurrent. atomic. AtomicInteger; import java. util. concurrent. locks. Lock; import java. util. concurrent. locks. ReadWriteLock; import java. util. concurrent. locks. ReentrantReadWriteLock;
@Service @Slf4j public class OrderService {
<span class="token keyword">private</span> RedisTemplate redisTemplate<span class="token punctuation">;</span><span class="token keyword">private</span> OrderMapper orderMapper<span class="token punctuation">;</span><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> AtomicInteger count <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AtomicInteger</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> ReadWriteLock readWriteLock <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReentrantReadWriteLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token function">OrderService</span><span class="token punctuation">(</span>RedisTemplate redisTemplate<span class="token punctuation">,</span> OrderMapper orderMapper<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>redisTemplate <span class="token operator">=</span> redisTemplate<span class="token punctuation">;</span><span class="token keyword">this</span><span class="token punctuation">.</span>orderMapper <span class="token operator">=</span> orderMapper<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token comment">/*** 通過id查詢訂單詳情** @param id* @return*/</span>
<span class="token keyword">public</span> OrderBo <span class="token function">getDetail</span><span class="token punctuation">(</span>Long id<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token keyword">int</span> num <span class="token operator">=</span> count<span class="token punctuation">.</span><span class="token function">incrementAndGet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">//獲取讀鎖</span>Lock readLock <span class="token operator">=</span> readWriteLock<span class="token punctuation">.</span><span class="token function">readLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">try</span> <span class="token punctuation">{</span>readLock<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">//緩存中查詢訂單信息</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"前往緩存中查詢信息,第一次,這是第:{}次請求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>Object obj <span class="token operator">=</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span> <span class="token punctuation">(</span>obj <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>String data <span class="token operator">=</span> obj<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"緩存中查詢到了信息,直接返回:{}"</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">return</span> <span class="token string">""</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">?</span> null <span class="token operator">:</span> <span class="token punctuation">(</span>OrderBo<span class="token punctuation">)</span> obj<span class="token punctuation">;</span><span class="token punctuation">}</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"沒有在緩存中獲取到數據,即將前往數據庫獲取,這是第:{}次請求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span><span class="token comment">//釋放讀鎖</span>readLock<span class="token punctuation">.</span><span class="token function">unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment">//獲取寫鎖</span>Lock writeLock <span class="token operator">=</span> readWriteLock<span class="token punctuation">.</span><span class="token function">writeLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">try</span><span class="token punctuation">{</span>writeLock<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">//緩存中查詢訂單信息</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"第二次前往緩存中查詢信息,這是第:{}次請求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>Object obj <span class="token operator">=</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span> <span class="token punctuation">(</span>obj <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>String data <span class="token operator">=</span> obj<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"緩存中查詢到了信息,直接返回:{}"</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">return</span> <span class="token string">""</span><span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">?</span> null <span class="token operator">:</span> <span class="token punctuation">(</span>OrderBo<span class="token punctuation">)</span> obj<span class="token punctuation">;</span><span class="token punctuation">}</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"前往數據庫查詢,這是第:{}次請求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span>OrderBo orderBo <span class="token operator">=</span> orderMapper<span class="token punctuation">.</span><span class="token function">getDetail</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"數據庫返回的數據:{},這是第:{}次請求"</span><span class="token punctuation">,</span>orderBo<span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span> <span class="token punctuation">(</span>orderBo <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token comment">//將數據保存到數據庫,有效時間一小時</span>redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">,</span> orderBo<span class="token punctuation">,</span> <span class="token number">3600</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"數據已經存入緩存,這是第:{}次請求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>redisTemplate<span class="token punctuation">.</span><span class="token function">opsForValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"order:"</span> <span class="token operator">+</span> id<span class="token punctuation">,</span> <span class="token string">""</span><span class="token punctuation">,</span> <span class="token number">300</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span>log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"數據庫中不存在此數據,但是為了防止緩存穿透,存入一條空數據到緩存中,這是第:{}次請求"</span><span class="token punctuation">,</span>num<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">return</span> orderBo<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">finally</span> <span class="token punctuation">{</span>writeLock<span class="token punctuation">.</span><span class="token function">unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span>
<span class="token punctuation">}</span>
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
加了讀寫鎖之后我們一起來看看控制臺的輸出結果:
這只是其中一部分,由于輸出的內容過長我就不全部展示出來了,我們這里需要關注的只有一個,數據庫查詢了多少次?
我們將控制臺日志拷貝到notepad++中,搜索“數據庫返回的數據”,請看結果: 我們可以看到,查詢數據庫的操作只有一處,但是查詢緩存的確實并發執行的,這就是為什么我推薦使用讀寫鎖的原因,讀寫鎖中讀鎖和寫鎖是互斥的,你覺得這樣速度還是不夠快,能不能讀鎖和寫鎖并行?答案是肯定的,請參考我的另外一篇博客:【并發編程】面試官:有沒有比讀寫鎖更快的鎖?
緩存雪崩
什么是緩存雪崩
緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至宕機。
為什么會出現大批量的key過期呢?是不是我們設置了相同的過期時間導致的?
解決方案
隨機設置過期時間
這個隨機時間并不是真正的隨機時間,而是指在原來過期時間的基礎上生成一個隨機時間,這個隨機時間比較小,然后兩者相加即可。
設置永久有效
將一些常用的數據設置成為永久有效,注意哦,是經常使用的而不是全部,這點需要特別注意。
總結
什么是緩存穿透? 同一時刻,大量的并發請求數據庫中不存在的信息,他既不會命中緩存,也不會命中數據庫,但是他會查找數據庫。
什么是緩存擊穿? 緩存擊穿是指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的并發請求過來,從而大量的請求打到db(數據庫)。
什么是緩存雪崩? 緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至宕機
并不是只有上面幾種解決方案,這里我只是講解了幾種常用的解決方案,在日常開發中我們可以根據實際的業務需求進行選擇,沒有最好的,只有最適合自己的,所以不一定要選擇最牛逼的解決方案,但是一定要選擇最適合項目的解決方案。
總結
以上是生活随笔 為你收集整理的带你搞明白什么是缓存穿透、缓存击穿、缓存雪崩 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。