自动加载缓存框架
2019獨角獸企業重金招聘Python工程師標準>>>
自動加載緩存框架
代碼,請訪問github 獲取更詳情,更新的內容 QQ交流群:429274886,版本更新會在群里通知,能了解最新動態
0.5版本已經是穩定版本了,大家可以放心使用了。
現在使用的緩存技術很多,比如Redis、 Memcache 、 EhCache等,甚至還有使用ConcurrentHashMap 或 HashTable 來實現緩存。但在緩存的使用上,每個人都有自己的實現方式,大部分是直接與業務代碼綁定,隨著業務的變化,要更換緩存方案時,非常麻煩。接下來我們就使用AOP?+?Annotation 來解決這個問題,同時使用自動加載機制 來實現數據“常駐內存”。
Spring AOP這幾年非常熱門,使用也越來越多,但個人建議AOP只用于處理一些輔助的功能(比如:接下來我們要說的緩存),而不能把業務邏輯使用AOP中實現,尤其是在需要“事務”的環境中。
如下圖所示:
AOP攔截到請求后:
AutoLoadHandler(自動加載處理器)主要做的事情:當緩存即將過期時,去執行DAO的方法,獲取數據,并將數據放到緩存中。為了防止自動加載隊列過大,設置了容量限制;同時會將超過一定時間沒有用戶請求的也會從自動加載隊列中移除,把服務器資源釋放出來,給真正需要的請求。
使用自加載的目的:
分布式自動加載
如果將應用部署在多臺服務器上,理論上可以認為自動加載隊列是由這幾臺服務器共同完成自動加載任務。比如應用部署在A,B兩臺服務器上,A服務器自動加載了數據D,(因為兩臺服務器的自動加載隊列是獨立的,所以加載的順序也是一樣的),接著有用戶從B服務器請求數據D,這時會把數據D的最后加載時間更新給B服務器,這樣B服務器就不會重復加載數據D。
##使用方法 ###1. 實現com.jarvis.cache.CacheGeterSeter 下面舉個使用Redis做緩存服務器的例子:
package com.jarvis.example.cache; import ... ... /*** 緩存切面,用于攔截數據并調用Redis進行緩存操作*/ @Aspect public class CachePointCut implements CacheGeterSeter<Serializable> {private static final Logger logger=Logger.getLogger(CachePointCut.class);private AutoLoadHandler<Serializable> autoLoadHandler;private static List<RedisTemplate<String, Serializable>> redisTemplateList;public CachePointCut() {autoLoadHandler=new AutoLoadHandler<Serializable>(10, this, 20000);}@Pointcut(value="execution(public !void com.jarvis.example.dao..*.*(..)) && @annotation(cache)", argNames="cache")public void daoCachePointcut(Cache cache) {logger.info("----------------------init daoCachePointcut()--------------------");}@Around(value="daoCachePointcut(cache)", argNames="pjp, cache")public Object controllerPointCut(ProceedingJoinPoint pjp, Cache cache) throws Exception {return CacheUtil.proceed(pjp, cache, autoLoadHandler, this);}public static RedisTemplate<String, Serializable> getRedisTemplate(String key) {if(null == redisTemplateList || redisTemplateList.isEmpty()) {return null;}int hash=Math.abs(key.hashCode());Integer clientKey=hash % redisTemplateList.size();RedisTemplate<String, Serializable> redisTemplate=redisTemplateList.get(clientKey);return redisTemplate;}@Overridepublic void setCache(final String cacheKey, final CacheWrapper<Serializable> result, final int expire) {try {final RedisTemplate<String, Serializable> redisTemplate=getRedisTemplate(cacheKey);redisTemplate.execute(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);JdkSerializationRedisSerializer serializer=(JdkSerializationRedisSerializer)redisTemplate.getValueSerializer();byte[] val=serializer.serialize(result);connection.set(key, val);connection.expire(key, expire);return null;}});} catch(Exception ex) {logger.error(ex.getMessage(), ex);}}@Overridepublic CacheWrapper<Serializable> get(final String cacheKey) {CacheWrapper<Serializable> res=null;try {final RedisTemplate<String, Serializable> redisTemplate=getRedisTemplate(cacheKey);res=redisTemplate.execute(new RedisCallback<CacheWrapper<Serializable>>() {@Overridepublic CacheWrapper<Serializable> doInRedis(RedisConnection connection) throws DataAccessException {byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);byte[] value=connection.get(key);if(null != value && value.length > 0) {JdkSerializationRedisSerializer serializer=(JdkSerializationRedisSerializer)redisTemplate.getValueSerializer();@SuppressWarnings("unchecked")CacheWrapper<Serializable> res=(CacheWrapper<Serializable>)serializer.deserialize(value);return res;}return null;}});} catch(Exception ex) {logger.error(ex.getMessage(), ex);}return res;}/*** 刪除緩存* @param cs Class* @param method* @param arguments* @param subKeySpEL* @param deleteByPrefixKey 是否批量刪除*/public static void delete(@SuppressWarnings("rawtypes") Class cs, String method, Object[] arguments, String subKeySpEL,boolean deleteByPrefixKey) {try {if(deleteByPrefixKey) {final String cacheKey=CacheUtil.getDefaultCacheKeyPrefix(cs.getName(), method, arguments, subKeySpEL) + "*";for(final RedisTemplate<String, Serializable> redisTemplate : redisTemplateList){redisTemplate.execute(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);Set<byte[]> keys=connection.keys(key);if(null != keys && keys.size() > 0) {byte[][] keys2=new byte[keys.size()][];keys.toArray(keys2);connection.del(keys2);}return null;}});}} else {final String cacheKey=CacheUtil.getDefaultCacheKey(cs.getName(), method, arguments, subKeySpEL);final RedisTemplate<String, Serializable> redisTemplate=getRedisTemplate(cacheKey);redisTemplate.execute(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {byte[] key=redisTemplate.getStringSerializer().serialize(cacheKey);connection.del(key);return null;}});}} catch(Exception ex) {logger.error(ex.getMessage(), ex);}}public AutoLoadHandler<Serializable> getAutoLoadHandler() {return autoLoadHandler;}public void destroy() {autoLoadHandler.shutdown();autoLoadHandler=null;}public List<RedisTemplate<String, Serializable>> getRedisTemplateList() {return redisTemplateList;}public void setRedisTemplateList(List<RedisTemplate<String, Serializable>> redisTemplateList) {CachePointCut.redisTemplateList=redisTemplateList;}}從上面的代碼可以看出,對緩存的操作,還是由業務系統自己來實現的,我們只是對AOP攔截到的ProceedingJoinPoint,進行做一些處理。
java代碼實現后,接下來要在spring中進行相關的配置:
<aop:aspectj-autoproxy proxy-target-class="true"/> <bean id="cachePointCut" class="com.jarvis.example.cache.CachePointCut" destroy-method="destroy"><property name="redisTemplateList"><list><ref bean="redisTemplate1"/><ref bean="redisTemplate2"/></list></property> </bean>從0.4版本開始增加了Redis的PointCut 的實現,直接在Spring 中用aop:config就可以使用:
<bean id="autoLoadConfig" class="com.jarvis.cache.to.AutoLoadConfig"><property name="threadCnt" value="10" /><property name="maxElement" value="20000" /><property name="printSlowLog" value="true" /><property name="slowLoadTime" value="1000" /> </bean> <bean id="cachePointCut" class="com.jarvis.cache.redis.CachePointCut" destroy-method="destroy"><constructor-arg ref="autoLoadConfig" /><property name="redisTemplateList"><list><ref bean="redisTemplate100" /><ref bean="redisTemplate2" /></list></property> </bean><aop:config><aop:aspect id="aa" ref="cachePointCut"><aop:pointcut id="daoCachePointcut" expression="execution(public !void com.jarvis.cache_example.dao..*.*(..)) && @annotation(cache)" /><aop:around pointcut-ref="daoCachePointcut" method="controllerPointCut" /></aop:aspect> </aop:config>通過Spring配置,能更好地支持,不同的數據使用不同的緩存服務器的情況。
實例代碼
Memcache例子:
<bean id="memcachedClient" class="net.spy.memcached.spring.MemcachedClientFactoryBean"><property name="servers" value="192.138.11.165:11211,192.138.11.166:11211" /><property name="protocol" value="BINARY" /><property name="transcoder"><bean class="net.spy.memcached.transcoders.SerializingTranscoder"><property name="compressionThreshold" value="1024" /></bean></property><property name="opTimeout" value="2000" /><property name="timeoutExceptionThreshold" value="1998" /><property name="hashAlg"><value type="net.spy.memcached.DefaultHashAlgorithm">KETAMA_HASH</value></property><property name="locatorType" value="CONSISTENT" /><property name="failureMode" value="Redistribute" /><property name="useNagleAlgorithm" value="false" /> </bean><bean id="cachePointCut" class="com.jarvis.cache.memcache.CachePointCut" destroy-method="destroy"><constructor-arg value="10" /><!-- 線程數量 --><constructor-arg value="20000" /><!-- 自動加載隊列容量 --><property name="memcachedClient", ref="memcachedClient" /> </bean>###2. 將需要使用緩存的方法前增加@Cache注解
package com.jarvis.example.dao; import ... ... public class UserDAO {@Cache(expire=600, autoload=true, requestTimeout=72000)public List<UserTO> getUserList(... ...) {... ...} }##緩存Key的生成
使用Spring EL 表達式自定義緩存Key:CacheUtil.getDefinedCacheKey(String keySpEL, Object[] arguments)
例如: @Cache(expire=600, key="'goods'+#args[0]")
默認生成緩存Key的方法:CacheUtil.getDefaultCacheKey(String className, String method, Object[] arguments, String subKeySpEL)
-
className 類名稱
-
method 方法名稱
-
arguments 參數
-
subKeySpEL SpringEL表達式
生成的Key格式為:{類名稱}.{方法名稱}{.SpringEL表達式運算結果}:{參數值的Hash字符串}。
當@Cache中不設置key值時,使用默認方式生成緩存Key
建議使用默認生成緩存Key的方法,能減少一些維護工作。
###subKeySpEL 使用說明
根據業務的需要,將緩存Key進行分組。舉個例子,商品的評論列表:
package com.jarvis.example.dao; import ... ... public class GoodsCommentDAO{@Cache(expire=600, subKeySpEL="#args[0]", autoload=true, requestTimeout=18000)public List<CommentTO> getCommentListByGoodsId(Long goodsId, int pageNo, int pageSize) {... ...} }如果商品Id為:100,那么生成緩存Key格式為:com.jarvis.example.dao.GoodsCommentDAO.getCommentListByGoodsId.100:xxxx 在Redis中,能精確刪除商品Id為100的評論列表,執行命令即可: del com.jarvis.example.dao.GoodsCommentDAO.getCommentListByGoodsId.100:*
SpringEL表達式使用起來確實非常方便,如果需要,@Cache中的expire,requestTimeout以及autoload參數都可以用SpringEL表達式來動態設置,但使用起來就變得復雜,所以我們沒有這樣做。
###數據實時性
上面商品評論的例子中,如果用戶發表了評論,要立即顯示該如何來處理?
比較簡單的方法就是,在發表評論成功后,立即把緩存中的數據也清除,這樣就可以了。
package com.jarvis.example.dao; import ... ... public class GoodsCommentDAO{@Cache(expire=600, subKeySpEL="#args[0]", autoload=true, requestTimeout=18000)public List<CommentTO> getCommentListByGoodsId(Long goodsId, int pageNo, int pageSize) {... ...}public void addComment(Long goodsId, String comment) {... ...// 省略添加評論代碼deleteCache(goodsId);}private void deleteCache(Long goodsId) {Object arguments[]=new Object[]{goodsId};CachePointCut.delete(this.getClass(), "getCommentListByGoodsId", arguments, "#args[0]", true);} }###@Cache
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Cache {/*** 緩存的過期時間,單位:秒*/int expire();/*** 自定義緩存Key,如果不設置使用系統默認生成緩存Key的方法* @return*/String key() default "";/*** 是否啟用自動加載緩存* @return*/boolean autoload() default false;/*** 當autoload為true時,緩存數據在 requestTimeout 秒之內沒有使用了,就不進行自動加載數據,如果requestTimeout為0時,會一直自動加載* @return*/long requestTimeout() default 36000L;/*** 使用SpEL,將緩存key,根據業務需要進行二次分組* @return*/String subKeySpEL() default "";/*** 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩存,例如:"#args[0]==1",當第一個參數值為1時,才進緩存。* @return*/String condition() default ""; }##注意事項
###1. 當@Cache中 autoload 設置為 ture 時,對應方法的參數必須都是Serializable的。 AutoLoadHandler中需要緩存通過深度復制后的參數。
###2. 參數中只設置必要的屬性值,在DAO中用不到的屬性值盡量不要設置,這樣能避免生成不同的緩存Key,降低緩存的使用率。 例如:
public CollectionTO<AccountTO> getAccountByCriteria(AccountCriteriaTO criteria) {List<AccountTO> list=null;PaginationTO paging=criteria.getPaging();if(null != paging && paging.getPageNo() > 0 && paging.getPageSize() > 0) {// 如果需要分頁查詢,先查詢總數criteria.setPaging(null);// 減少緩存KEY的變化,在查詢記錄總數據時,不用設置分頁相關的屬性值Integer recordCnt=accountDAO.getAccountCntByCriteria(criteria);if(recordCnt > 0) {criteria.setPaging(paging);paging.setRecordCnt(recordCnt);list=accountDAO.getAccountByCriteria(criteria);}return new CollectionTO<AccountTO>(list, recordCnt, criteria.getPaging().getPageSize());} else {list=accountDAO.getAccountByCriteria(criteria);return new CollectionTO<AccountTO>(list, null != list ? list.size() : 0, 0);}}###3. 注意AOP失效的情況; 例如:
TempDAO {public Object a() {return b().get(0);}@Cache(expire=600)public List<Object> b(){return ... ...;}}通過 new TempDAO().a() 調用b方法時,AOP失效,也無法進行緩存相關操作。
###4. 自動加載緩存時,不能在緩存方法內疊加查詢參數值; 例如:
@Cache(expire=600, autoload=true)public List<AccountTO> getDistinctAccountByPlayerGet(AccountCriteriaTO criteria) {List<AccountTO> list;int count=criteria.getPaging().getThreshold() ;// 查預設查詢數量的10倍criteria.getPaging().setThreshold(count * 10);… …}因為自動加載時,AutoLoadHandler 緩存了查詢參數,執行自動加載時,每次執行時 threshold 都會乘以10,這樣threshold的值就會越來越大。
###5. 當方法返回值類型改變了怎么辦?
在代碼重構時,可能會出現改方法返回值類型的情況,而參數不變的情況,那上線部署時,可能會從緩存中取到舊數據類型的數據,可以通過以下方法處理:
- 上線后,快速清理緩存中的數據;
- 在CacheGeterSeter的實現類中統一加個version;
- 在@Cache中加version(未實現)。
###6. 對于一些比較耗時的方法盡量使用自動加載。
###7. 對于查詢條件變化比較劇烈的,不要使用自動加載機制。 比如,根據用戶輸入的關鍵字進行搜索數據的方法,不建議使用自動加載。
##在事務環境中,如何減少“臟讀”
不要從緩存中取數據,然后應用到修改數據的SQL語句中
在事務完成后,再刪除相關的緩存
在事務開始時,用一個ThreadLocal記錄一個HashSet,在更新數據方法執行完時,把要刪除緩存的相關參數封裝成在一個Bean中,放到這個HashSet中,在事務完成時,遍歷這個HashSet,然后刪除相關緩存。
大部分情況,只要做到第1點就可以了,因為保證數據庫中的數據準確才是最重要的。因為這種“臟讀”的情況只能減少出現的概率,不能完成解決。一般只有在非常高并發的情況才有可能發生。就像12306,在查詢時告訴你還有車票,但最后支付時不一定會有。
##使用規范
##為什么要使用自動加載機制?
首先我們想一下系統的瓶頸在哪里?
在高并發的情況下數據庫性能極差,即使查詢語句的性能很高;如果沒有自動加載機制的話,在當緩存過期時,訪問洪峰到來時,很容易就使數據壓力大增。
往緩存寫數據與從緩存讀數據相比,效率也差很多,因為寫緩存時需要分配內存等操作。使用自動加載,可以減少同時往緩存寫數據的情況,同時也能提升緩存服務器的吞吐量。
還有一些比較耗時的業務。
##如何減少DAO層并發
##可擴展性及維護性
轉載于:https://my.oschina.net/u/1469495/blog/380865
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: 梦到水井是什么意思
- 下一篇: 做梦梦到去拉萨是啥意思