偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

面對(duì)緩存,有哪些問題需要思考?

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

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

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

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

面對(duì)緩存,有哪些問題需要思考?

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時(shí),緩存就沒有意義了 
  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.     } 

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

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

因?yàn)楦唏詈蠋淼膯栴}還很多,就不一一列舉了。接下來介紹筆者開源的一個(gè)緩存管理框架:AutoLoadCache是如何幫助我們來解決上述問題的。

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

 

  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攔截到請(qǐng)求后,大概的流程如下:

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

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

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

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

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

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

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

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

 

  1. public class CacheWrapper<T> implements Serializable, Cloneable { 
  2.  
  3.     private T cacheObject; // 緩存數(shù)據(jù) 
  4.  
  5.     private long lastLoadTime; // 加載時(shí)間 
  6.  
  7.     private int expire; // 緩存時(shí)長(zhǎng) 
  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í)間和緩存時(shí)長(zhǎng),通過這兩項(xiàng)數(shù)據(jù),很容易判斷緩存是否即將過期或者已經(jīng)過期。

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

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

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

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

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

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

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

為了解決這個(gè)問題AutoLoadCache,增加了namespace。如果設(shè)置了namespace就會(huì)在每個(gè)緩存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è)置此項(xiàng),則用哈希表進(jìn)行存儲(chǔ) 
  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)存占用,以及減輕帶寬壓力;同時(shí)也要考慮序列化與反序列化的性能。

AutoLoadCache為了滿足不同用戶的需要,已經(jīng)實(shí)現(xiàn)了基于JDK、Hessian、JacksonJson、Fastjson、JacksonMsgpack等技術(shù)序列化及反序列工具。也可以通過實(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是其一些屬性是會(huì)被忽略,存在這種情況時(shí),也不能使用Json;

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

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

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

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

拿來主義機(jī)制

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

自動(dòng)加載機(jī)制

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

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

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

7. 異步刷新

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

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

8. 支持多種緩存操作

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

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

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

9. 批量刪除緩存

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

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

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

我們舉個(gè)商品評(píng)論的場(chǎng)景(代碼五):

 

  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 時(shí)相當(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時(shí),相當(dāng)于Redis命令: DEL prouduct_comment_list_1 
  8.     public void addComment(ProuductComment comment) ; 
  9.      

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

 

  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í), 同時(shí)會(huì)主動(dòng)刪除緩存中的數(shù)據(jù)。 如果在事務(wù)還沒提交之前又有一個(gè)請(qǐng)求去加載用戶數(shù)據(jù),這時(shí)就會(huì)把數(shù)據(jù)庫中舊數(shù)據(jù)緩存起來,在下次主動(dòng)刪除緩存或緩存過期之前的這一段時(shí)間內(nèi),緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)是不一致的。AutoloadCache框架為了解決這個(gè)問題,引入了一個(gè)新的注解:@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 會(huì)先使用ThreadLocal緩存要?jiǎng)h除緩存KEY,等事務(wù)提交后再去執(zhí)行緩存刪除操作。其實(shí)不能說是“解決不一致問題”,而是緩解而已。

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

11. 與Spring Cache的比較

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

2018-03-13 09:20:05

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

2009-09-16 13:29:30

BSM

2010-04-21 10:04:33

Oracle移植

2019-06-05 15:23:09

Redis緩存存儲(chǔ)

2013-04-03 15:42:46

2019-10-23 06:09:18

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

2018-09-18 14:03:57

OpenStack知識(shí)難點(diǎn)

2020-09-17 06:47:23

緩存類型算法

2024-03-14 09:07:05

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

2021-12-30 06:59:28

方法重寫面試

2019-05-07 18:17:26

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

2020-07-01 11:29:52

云計(jì)算邊緣計(jì)算疫情

2020-12-28 11:11:26

前端開發(fā)語言

2011-07-05 09:28:57

vmware

2022-09-26 10:03:02

低代碼開發(fā)

2017-02-09 11:10:51

2019-04-11 08:17:36

2019-03-28 09:14:22

人工智能AI

2012-07-19 15:16:45

移動(dòng)營(yíng)銷

2019-07-10 12:19:31

網(wǎng)絡(luò)安全信息安全網(wǎng)絡(luò)威脅
點(diǎn)贊
收藏

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