自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

面對緩存,有哪些問題需要思考?

移動開發(fā)
緩存是用于解決高并發(fā)場景下系統(tǒng)的性能及穩(wěn)定性問題的銀彈。本文主要是討論我們經(jīng)常使用的分布式緩存Redis在開發(fā)過程中需要考慮的問題。

緩存可以說是無處不在,比如:PC電腦中的內(nèi)存、CPU中有二級緩存、http協(xié)議中的緩存控制、CDN加速技術(shù) 無不都是使用了緩存的思想來解決性能問題。

緩存是用于解決高并發(fā)場景下系統(tǒng)的性能及穩(wěn)定性問題的銀彈。

本文主要是討論我們經(jīng)常使用的分布式緩存Redis在開發(fā)過程中需要考慮的問題。

面對緩存,有哪些問題需要思考?

1. 如何將業(yè)務(wù)邏輯與緩存之間進(jìn)行解耦?

大部分情況,大家都是把緩存操作和業(yè)務(wù)邏輯之間的代碼交織在一起的,比如(代碼一):

 

  1. public UserServiceImpl implements UserService { 
  2.     @Autowired 
  3.     private RedisTemplate<String, User> redisTemplate; 
  4.      
  5.     @Autowired 
  6.     private UserMapper userMapper; 
  7.      
  8.     public User getUserById(Long userId) { 
  9.         String cacheKey = "user_" + userId; 
  10.         User user = redisTemplate.opsForValue().get(cacheKey); 
  11.         if(null != user) { 
  12.             return user
  13.         } 
  14.         user = userMapper.getUserById(userId); 
  15.         redisTemplate.opsForValue().set(cacheKey, user); // 如果user 為null時,緩存就沒有意義了 
  16.         return user
  17.     } 
  18.      
  19.     public void deleteUserById(Long userId) { 
  20.         userMapper.deleteUserById(userId); 
  21.         String cacheKey = "user_" + userId; 
  22.         redisTemplate.opsForValue().del(cacheKey); 
  23.     } 

從上面的代碼可以看出以下幾個問題:

  1. 緩存操作非常繁瑣,產(chǎn)生非常多的重復(fù)代碼;
  2. 緩存操作與業(yè)務(wù)邏輯耦合度非常高,不利于后期的維護(hù);
  3. 當(dāng)業(yè)務(wù)數(shù)據(jù)為null時,無法確定是否已經(jīng)緩存,會造成緩存無法***;
  4. 開發(fā)階段,為了排查問題,經(jīng)常需要來回開關(guān)緩存功能,使用上面的代碼是無法做到很方便地開關(guān)緩存功能;
  5. 當(dāng)業(yè)務(wù)越來越復(fù)雜時,使用緩存的地方越來越多時,很難定位哪些數(shù)據(jù)要進(jìn)行主動刪除;
  6. 如果不想用Redis,換用別的緩存技術(shù)的話,那是多么痛苦的一件事。

因為高耦合帶來的問題還很多,就不一一列舉了。接下來介紹筆者開源的一個緩存管理框架:AutoLoadCache是如何幫助我們來解決上述問題的。

借鑒于Spring cache的思想使用AOP + Annotation 等技術(shù)實現(xiàn)緩存與業(yè)務(wù)邏輯的解耦。我們再用AutoLoadCache 來重構(gòu)上面的代碼,進(jìn)行對比(代碼二):

 

  1. public interface UserMapper { 
  2.     @Cache(expire = 120, key = "'user_' + #args[0]"
  3.     User getUserById(Long userId); 
  4.      
  5.     @CacheDelete({ @CacheDeleteKey(value = "'user' + #args[0].id") }) 
  6.     void updateUser(User user); 
  7.  
  8. public UserServiceImpl implements UserService { 
  9.      
  10.     @Autowired 
  11.     private UserMapper userMapper; 
  12.      
  13.     public User getUserById(Long userId) { 
  14.         return userMapper.getUserById(userId); 
  15.     } 
  16.     @Transactional(rollbackFor=Throwable.class) 
  17.     public void updateUser(User user) { 
  18.         userMapper.updateUser(user); 
  19.     } 
  20. }

AutoloadCache 在AOP攔截到請求后,大概的流程如下:

  1. 獲取到攔截方法的@Cache注解,并生成緩存key;
  2. 通過緩存key,去緩存中獲取數(shù)據(jù);

如果緩存***,執(zhí)行如下流程:

  1. 如果需要自動加載,則把相關(guān)信息保存到自動加載隊列中;
  2. 否則判斷緩存是否即將過期,如果即將過期,則會發(fā)起異步刷新;
  3. ***把數(shù)據(jù)返回給用戶;

如果緩存沒有***,執(zhí)行如下流程:

  1. 選舉出一個leader回到數(shù)據(jù)源中去加載數(shù)據(jù),加載到數(shù)據(jù)后通知其它請求從內(nèi)存中獲取數(shù)據(jù)(拿來主義機(jī)制);
  2. leader負(fù)責(zé)把數(shù)據(jù)寫入緩存;如果需要自動加載,則把相關(guān)信息保存到自動加載隊列中;
  3. ***把數(shù)據(jù)返回給用戶;

這里提到的異步刷新、自動加載、拿來主義機(jī)制,我們會在后面再說明。

2. 對緩存進(jìn)行“包裝”

上面代碼一的例子中,當(dāng)從數(shù)據(jù)源獲取的數(shù)據(jù)為null時,緩存就沒有意義了,所獲取這個數(shù)據(jù)的請求,都會回到數(shù)據(jù)源去獲取數(shù)據(jù)。當(dāng)請求量非常大的話,會造成數(shù)據(jù)源負(fù)載過高而宕機(jī)。所以對于null的數(shù)據(jù),需要做特殊處理,比如使用特殊字符串進(jìn)行替換。而在AutoloadCache中使用了一個包裝器對所有緩存數(shù)據(jù)進(jìn)行包裝(代碼三):

 

  1. public class CacheWrapper<T> implements Serializable, Cloneable { 
  2.  
  3.     private T cacheObject; // 緩存數(shù)據(jù) 
  4.  
  5.     private long lastLoadTime; // 加載時間 
  6.  
  7.     private int expire; // 緩存時長 
  8.      
  9.     /** 
  10.      * 判斷緩存是否已經(jīng)過期 
  11.      * @return boolean 
  12.      */ 
  13.     public boolean isExpired() { 
  14.         if(expire > 0) { 
  15.             return (System.currentTimeMillis() - lastLoadTime) > expire * 1000; 
  16.         } 
  17.         return false
  18.     } 

在這上面的代碼中,除了封裝了緩存數(shù)據(jù)外,還封裝了數(shù)據(jù)加載時間和緩存時長,通過這兩項數(shù)據(jù),很容易判斷緩存是否即將過期或者已經(jīng)過期。

3. 如何提升緩存key生成表達(dá)式性能?

使用Annotation解決緩存與業(yè)務(wù)之間的耦合后,我們最主要的工作就是如何來設(shè)計緩存KEY了,緩存KEY設(shè)計的粒度越小,緩存的復(fù)用性也就越好。

上面例子中我們是使用Spring EL表達(dá)式來生成緩存KEY,有些人估計會擔(dān)心Spring EL表達(dá)式的性能不好,或者不想用Spring的情況該怎么辦?

框架中為了滿足這些需求,支持?jǐn)U展表達(dá)式解析器:繼承com.jarvis.cache.script. AbstractScriptParser后就可以任你擴(kuò)展。

框架現(xiàn)在除了支持Spring EL表達(dá)式外,還支持Ognl,javascript表達(dá)式。對于性能要求非常高的人,可以使用Ognl,它的性能非常接近原生代碼。

4. 如何解決緩存Key沖突問題?

在實際情況中,可能有多個模塊共用一個Redis服務(wù)器或是一個Redis集群的情況,那么有可能造成緩存key沖突了。

為了解決這個問題AutoLoadCache,增加了namespace。如果設(shè)置了namespace就會在每個緩存Key最前面增加namespace(代碼四):

 

  1. public final class CacheKeyTO implements Serializable { 
  2.  
  3.     private final String namespace; 
  4.  
  5.     private final String key;// 緩存Key 
  6.  
  7.     private final String hfield;// 設(shè)置哈希表中的字段,如果設(shè)置此項,則用哈希表進(jìn)行存儲 
  8.  
  9.     public String getCacheKey() { // 生成緩存Key方法 
  10.         if(null != this.namespace && this.namespace.length() > 0) { 
  11.             return new StringBuilder(this.namespace).append(":").append(this.key).toString(); 
  12.         } 
  13.         return this.key
  14.     } 

5. 壓縮緩存數(shù)據(jù)及提升序列化與反序列化性能

我們希望緩存數(shù)據(jù)包越小越好,能減少內(nèi)存占用,以及減輕帶寬壓力;同時也要考慮序列化與反序列化的性能。

AutoLoadCache為了滿足不同用戶的需要,已經(jīng)實現(xiàn)了基于JDK、Hessian、JacksonJson、Fastjson、JacksonMsgpack等技術(shù)序列化及反序列工具。也可以通過實現(xiàn)com.jarvis.cache.serializer.ISerializer 接口自行擴(kuò)展。

JDK自帶的序列化與反序列化工具產(chǎn)生的數(shù)據(jù)包非常大,而且性能也非常差,不建議大家使用;JacksonJson 和 Fastjson 是基于JSON的,所有用到緩存的函數(shù)的參數(shù)及返回值都必須是具體類型的,不能是不確定類型的(不能是Object, List等),另外有些數(shù)據(jù)轉(zhuǎn)成Json是其一些屬性是會被忽略,存在這種情況時,也不能使用Json;

而Hessian 則是非常不錯的選擇,非常成熟和穩(wěn)定性。阿里的dubbo和HSF兩個RPC框架都是使用了Hessian進(jìn)行序列化和返序列化。

6. 如何減少回源并發(fā)數(shù)?

當(dāng)緩存未***時,都需要回到數(shù)據(jù)源去取數(shù)據(jù),如果這時有多個并發(fā)來請求相同一個數(shù)據(jù)(即相同緩存key請求),都回到數(shù)據(jù)源加載數(shù)據(jù),并寫緩存,造成資源極大的浪費(fèi),也可能造成數(shù)據(jù)源負(fù)載過高而無法服務(wù)。

AutoLoadCache 使用拿來主義機(jī)制和自動加載機(jī)制來解決這個問題:

拿來主義機(jī)制

拿來主交機(jī)制,指的是當(dāng)有多個用戶請求同一個數(shù)據(jù)時,會選舉出一個leader去數(shù)據(jù)源加載數(shù)據(jù),其它用戶則等待其拿到的數(shù)據(jù)。并由leader將數(shù)據(jù)寫入緩存。

自動加載機(jī)制

自動加載機(jī)制,將用戶請求及緩存時間等信息放到一個隊列中,后臺使用線程池定期掃這個隊列,發(fā)現(xiàn)緩存即將過期,則去數(shù)據(jù)源加載***的數(shù)據(jù)放到緩存中。達(dá)到將數(shù)據(jù)長駐內(nèi)存的效果。從而將這些數(shù)據(jù)的請求,全部引向了緩存,而不會回到數(shù)據(jù)源去獲取數(shù)據(jù)。非常適合用于緩存使用非常頻繁的數(shù)據(jù),以及非常耗時的數(shù)據(jù)。

為了防止自動加載隊列過大,設(shè)置了容量限制;同時會將超過一定時間沒有用戶請求的也會從自動加載隊列中移除,把服務(wù)器資源釋放出來,給真正需要的請求。

往緩存里寫數(shù)據(jù)的性能相比讀的性能差非常多,通過上面兩種機(jī)制,可以減少寫緩存的并發(fā),提升緩存服務(wù)能力。

7. 異步刷新

AutoLoadCache 從緩存中獲取到數(shù)據(jù)后,借助于上面提到的CacheWrapper,能很方便判斷緩存是否即將過期, 如果即將過期,則會把發(fā)起異步刷新請求。

使用異步刷新的目的,提前將數(shù)據(jù)緩存起來,避免緩存失效后,大量請求穿透到數(shù)據(jù)源。

8. 支持多種緩存操作

大部分情況下,我們都是對緩存進(jìn)行讀與寫操作,可有時,我們只需要從緩存中讀取數(shù)據(jù),或者只寫數(shù)據(jù),那么可以通過 @Cache 的 opType 指定緩存操作類型。現(xiàn)支持以下幾種操作類型:

  1. READ_WRITE:讀寫緩存操:如果緩存中有數(shù)據(jù),則使用緩存中的數(shù)據(jù),如果緩存中沒有數(shù)據(jù),則加載數(shù)據(jù),并寫入緩存。默認(rèn)是READ_WRITE;
  2. WRITE:從數(shù)據(jù)源中加載***的數(shù)據(jù),并寫入緩存。對數(shù)據(jù)源和緩存數(shù)據(jù)進(jìn)行同步;
  3. READ_ONLY: 只從緩存中讀取,并不會去數(shù)據(jù)源加載數(shù)據(jù)。用于異地讀寫緩存的場景;
  4. LOAD :只從數(shù)據(jù)源加載數(shù)據(jù),不讀取緩存中的數(shù)據(jù),也不寫入緩存。

另外在@Cache中只能靜態(tài)指寫緩存操作類型,如果想在運(yùn)行時調(diào)整操作類型,需要通過CacheHelper.setCacheOpType()方法來進(jìn)行調(diào)整。

9. 批量刪除緩存

在很多時候,數(shù)據(jù)查詢條件是比較復(fù)雜,我們無法獲取或還原要刪除的緩存key。

AutoLoadCache 為了解決這個問題,使用Redis的hash表來管理這部分的緩存。把需要批量刪除的緩存放在同一個hash表中,如果需要需要批量刪除這些緩存時,直接把這個hash表刪除即可。這時只要設(shè)計合理粒度的緩存key即可。

通過@Cache的hfield設(shè)置hash表的key。

我們舉個商品評論的場景(代碼五):

 

  1. public interface ProuductCommentMapper { 
  2.     @Cache(expire=600, key="'prouduct_comment_list_'+#args[0]", hfield = "#args[1]+'_'+#args[2]"
  3.     // 例如:prouductId=1, pageNo=2, pageSize=3 時相當(dāng)于Redis命令:HSET prouduct_comment_list_1 2_3  List<Long> 
  4.     public List<Long> getCommentListByProuductId(Long prouductId, int pageNo, int pageSize); 
  5.          
  6.     @CacheDelete({@CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId")})  
  7.     // 例如:#args[0].prouductId = 1時,相當(dāng)于Redis命令: DEL prouduct_comment_list_1 
  8.     public void addComment(ProuductComment comment) ; 
  9.      

如果添加評論時,我們只需要主動刪除前3頁的評論(代碼六):

 

  1. public interface ProuductCommentMapper { 
  2.     @Cache(expire=600, key="'prouduct_comment_list_'+#args[0]+'_'+#args[1]", hfield = "#args[2]"
  3.     public List<Long> getCommentListByProuductId(Long prouductId, int pageNo, int pageSize); 
  4.          
  5.     @CacheDelete({ 
  6.         @CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId+'_1'"), 
  7.         @CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId+'_2'"), 
  8.         @CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId+'_3'"
  9.     })  
  10.     public void addComment(ProuductComment comment) ; 
  11.      

10. 雙寫不一致問題

在“代碼二”中使用updateUser方法更新用戶信息時, 同時會主動刪除緩存中的數(shù)據(jù)。 如果在事務(wù)還沒提交之前又有一個請求去加載用戶數(shù)據(jù),這時就會把數(shù)據(jù)庫中舊數(shù)據(jù)緩存起來,在下次主動刪除緩存或緩存過期之前的這一段時間內(nèi),緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)是不一致的。AutoloadCache框架為了解決這個問題,引入了一個新的注解:@CacheDeleteTransactional (代碼七):

 

  1. public UserServiceImpl implements UserService { 
  2.      
  3.     @Autowired 
  4.     private UserMapper userMapper; 
  5.      
  6.     public User getUserById(Long userId) { 
  7.         return userMapper.getUserById(userId); 
  8.     } 
  9.     @Transactional(rollbackFor=Throwable.class) 
  10.     @CacheDeleteTransactional 
  11.     public void updateUser(User user) { 
  12.         userMapper.updateUser(user);  
  13.     } 
  14.  

使用@CacheDeleteTransactional注解后,AutoloadCache 會先使用ThreadLocal緩存要刪除緩存KEY,等事務(wù)提交后再去執(zhí)行緩存刪除操作。其實不能說是“解決不一致問題”,而是緩解而已。

緩存數(shù)據(jù)雙寫不一致的問題是很難解決的,即使我們只用數(shù)據(jù)庫(單寫的情況)也會存在數(shù)據(jù)不一致的情況(當(dāng)從數(shù)據(jù)庫中取數(shù)據(jù)時,同時又被更新了),我們只能是減少不一致情況的發(fā)生。對于一些比較重要的數(shù)據(jù),我們不能直接使用緩存中的數(shù)據(jù)進(jìn)行計算并回寫的數(shù)據(jù)庫中,比如扣庫存,需要對數(shù)據(jù)增加版本信息,并通過樂觀鎖等技術(shù)來避免數(shù)據(jù)不一致問題。

11. 與Spring Cache的比較

AutoLoadCache 的思想其實是源自 Spring Cache,都是使用 AOP + Annotation ,將緩存與業(yè)務(wù)邏輯進(jìn)行解耦。區(qū)別在于:

  1. AutoLoadCache 的AOP不限于Spring 中的AOP技術(shù),即可以脫離Spring 生態(tài)使用,比如成功案例nutz;
  2. Spring Cache不支持命名空間;
  3. Spring Cache沒有自動加載、異步刷新、拿來主義機(jī)制;
  4. Spring Cache使用name 和 key的來管理緩存(即通過name和key就可以操作具體緩存了),而AutoLoadCache 使用的是namespace + key + hfield 來管理緩存,同時每個緩存都可以指定緩存時間(expire)。也就是說Spring Cache 比較適合用來管理Ehcache的緩存,而AutoLoadCache 更加適合管理Redis, Memcache,尤其是Redis,hfield 相關(guān)的功能都是針對它們進(jìn)行開發(fā)的(因為Memcache不支持hash表,所以沒辦法使用hfield相關(guān)的功能)。
  5. Spring Cache不能針對每個緩存Key,進(jìn)行設(shè)置緩存過期時間。而在緩存管理應(yīng)用中,不同的緩存其緩存時間要盡量設(shè)置為不同的。如果都相同的,那緩存同時失效的可能性會比較大些,這樣穿透到數(shù)據(jù)庫的可能性也就更大了,對系統(tǒng)的穩(wěn)定性是沒有好處的;
  6. Spring Cache ***的缺點(diǎn)就是無法使用Spring EL表達(dá)式來動態(tài)生成Cache name,而且Cache name是的必須在Spring 配置時指定幾個,非常不方便使用。尤其想在Redis中想精確清除一批緩存,是無法實現(xiàn)的,可能會誤刪除我們不希望被刪除的緩存;
  7. Spring Cache只能基于Spring 中的AOP及Spring EL表達(dá)式來使用,而AutoloadCache 可以根據(jù)使用者的實際情況進(jìn)行擴(kuò)展;
  8. AutoLoadCache中使用@CacheDeleteTransactional 來減少雙寫不一致問題,而Spring Cache沒有相應(yīng)的解決方案;作者:家榆_77cd鏈接:http://www.jianshu.com/p/4f52d046c3d2來源:簡書著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
責(zé)任編輯:未麗燕 來源: 簡書
相關(guān)推薦

2018-03-13 09:20:05

數(shù)字化轉(zhuǎn)型

2010-04-21 10:04:33

Oracle移植

2009-09-16 13:29:30

BSM

2019-06-05 15:23:09

Redis緩存存儲

2019-10-23 06:09:18

DDos攻擊清洗服務(wù)網(wǎng)絡(luò)攻擊

2020-09-17 06:47:23

緩存類型算法

2013-04-03 15:42:46

2018-09-18 14:03:57

OpenStack知識難點(diǎn)

2024-03-14 09:07:05

刷數(shù)任務(wù)維度后端

2019-05-07 18:17:26

Redis服務(wù)器數(shù)據(jù)

2021-12-30 06:59:28

方法重寫面試

2020-07-01 11:29:52

云計算邊緣計算疫情

2022-09-26 10:03:02

低代碼開發(fā)

2020-12-28 11:11:26

前端開發(fā)語言

2011-07-05 09:28:57

vmware

2019-04-11 08:17:36

2017-02-09 11:10:51

2021-04-22 22:26:13

Java語言IT

2019-07-10 12:19:31

網(wǎng)絡(luò)安全信息安全網(wǎng)絡(luò)威脅

2012-07-19 15:16:45

移動營銷
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號