- 資訊首頁(yè) > 開(kāi)發(fā)技術(shù) >
- springboot中如何使用自定義兩級緩存
工作中用到了springboot的緩存,使用起來(lái)挺方便的,直接引入redis或者ehcache這些緩存依賴(lài)包和相關(guān)緩存的starter依賴(lài)包,然后在啟動(dòng)類(lèi)中加入@EnableCaching注解,然后在需要的地方就可以使用@Cacheable和@CacheEvict使用和刪除緩存了。這個(gè)使用很簡(jiǎn)單,相信用過(guò)springboot緩存的都會(huì )玩,這里就不再多說(shuō)了。美中不足的是,springboot使用了插件式的集成方式,雖然用起來(lái)很方便,但是當你集成ehcache的時(shí)候就是用ehcache,集成redis的時(shí)候就是用redis。如果想兩者一起用,ehcache作為本地一級緩存,redis作為集成式的二級緩存,使用默認的方式據我所知是沒(méi)法實(shí)現的(如果有高人可以實(shí)現,麻煩指點(diǎn)下我)。畢竟很多服務(wù)需要多點(diǎn)部署,如果單獨選擇ehcache可以很好地實(shí)現本地緩存,但是如果在多機之間共享緩存又需要比較費時(shí)的折騰,如果選用集中式的redis緩存,因為每次取數據都要走網(wǎng)絡(luò ),總感覺(jué)性能不會(huì )太好。
為了不要侵入springboot原本使用緩存的方式,這里自己定義了兩個(gè)緩存相關(guān)的注解,如下
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Cacheable { String value() default ""; String key() default ""; //泛型的Class類(lèi)型 Class<?> type() default Exception.class; } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface CacheEvict { String value() default ""; String key() default ""; }
如上兩個(gè)注解和spring中緩存的注解基本一致,只是去掉了一些不常用的屬性。說(shuō)到這里,不知道有沒(méi)有朋友注意過(guò),當你在springboot中單獨使用redis緩存的時(shí)候,Cacheable和CacheEvict注解的value屬性,實(shí)際上在redis中變成了一個(gè)zset類(lèi)型的值的key,而且這個(gè)zset里面還是空的,比如@Cacheable(value="cache1",key="key1"),正常情況下redis中應該是出現cache1 -> map(key1,value1)這種形式,其中cache1作為緩存名稱(chēng),map作為緩存的值,key作為map里的鍵,可以有效的隔離不同的緩存名稱(chēng)下的緩存。但是實(shí)際上redis里確是cache1 -> 空(zset)和key1 -> value1,兩個(gè)獨立的鍵值對,試驗得知不同的緩存名稱(chēng)下的緩存完全是共用的,如果有感興趣的朋友可以去試驗下,也就是說(shuō)這個(gè)value屬性實(shí)際上是個(gè)擺設,鍵的唯一性只由key屬性保證。我只能認為這是spring的緩存實(shí)現的bug,或者是特意這么設計的,(如果有知道啥原因的歡迎指點(diǎn))。
回到正題,有了注解還需要有個(gè)注解處理類(lèi),這里我使用aop的切面來(lái)進(jìn)行攔截處理,原生的實(shí)現其實(shí)也大同小異。切面處理類(lèi)如下:
import com.xuanwu.apaas.core.multicache.annotation.CacheEvict; import com.xuanwu.apaas.core.multicache.annotation.Cacheable; import com.xuanwu.apaas.core.utils.JsonUtil; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 多級緩存切面 * @author rongdi */ @Aspect @Component public class MultiCacheAspect { private static final Logger logger = LoggerFactory.getLogger(MultiCacheAspect.class); @Autowired private CacheFactory cacheFactory; //這里通過(guò)一個(gè)容器初始化監聽(tīng)器,根據外部配置的@EnableCaching注解控制緩存開(kāi)關(guān) private boolean cacheEnable; @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)") public void cacheableAspect() { } @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)") public void cacheEvict() { } @Around("cacheableAspect()") public Object cache(ProceedingJoinPoint joinPoint) { //得到被切面修飾的方法的參數列表 Object[] args = joinPoint.getArgs(); // result是方法的最終返回結果 Object result = null; //如果沒(méi)有開(kāi)啟緩存,直接調用處理方法返回 if(!cacheEnable){ try { result = joinPoint.proceed(args); } catch (Throwable e) { logger.error("",e); } return result; } // 得到被代理方法的返回值類(lèi)型 Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType(); // 得到被代理的方法 Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); // 得到被代理的方法上的注解 Cacheable ca = method.getAnnotation(Cacheable.class); //獲得經(jīng)過(guò)el解析后的key值 String key = parseKey(ca.key(),method,args); Class<?> elementClass = ca.type(); //從注解中獲取緩存名稱(chēng) String name = ca.value(); try { //先從ehcache中取數據 String cacheValue = cacheFactory.ehGet(name,key); if(StringUtils.isEmpty(cacheValue)) { //如果ehcache中沒(méi)數據,從redis中取數據 cacheValue = cacheFactory.redisGet(name,key); if(StringUtils.isEmpty(cacheValue)) { //如果redis中沒(méi)有數據 // 調用業(yè)務(wù)方法得到結果 result = joinPoint.proceed(args); //將結果序列化后放入redis cacheFactory.redisPut(name,key,serialize(result)); } else { //如果redis中可以取到數據 //將緩存中獲取到的數據反序列化后返回 if(elementClass == Exception.class) { result = deserialize(cacheValue, returnType); } else { result = deserialize(cacheValue, returnType,elementClass); } } //將結果序列化后放入ehcache cacheFactory.ehPut(name,key,serialize(result)); } else { //將緩存中獲取到的數據反序列化后返回 if(elementClass == Exception.class) { result = deserialize(cacheValue, returnType); } else { result = deserialize(cacheValue, returnType,elementClass); } } } catch (Throwable throwable) { logger.error("",throwable); } return result; } /** * 在方法調用前清除緩存,然后調用業(yè)務(wù)方法 * @param joinPoint * @return * @throws Throwable * */ @Around("cacheEvict()") public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable { // 得到被代理的方法 Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); //得到被切面修飾的方法的參數列表 Object[] args = joinPoint.getArgs(); // 得到被代理的方法上的注解 CacheEvict ce = method.getAnnotation(CacheEvict.class); //獲得經(jīng)過(guò)el解析后的key值 String key = parseKey(ce.key(),method,args); //從注解中獲取緩存名稱(chēng) String name = ce.value(); // 清除對應緩存 cacheFactory.cacheDel(name,key); return joinPoint.proceed(args); } /** * 獲取緩存的key * key 定義在注解上,支持SPEL表達式 * @return */ private String parseKey(String key,Method method,Object [] args){ if(StringUtils.isEmpty(key)) return null; //獲取被攔截方法參數名列表(使用Spring支持類(lèi)庫) LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paraNameArr = u.getParameterNames(method); //使用SPEL進(jìn)行key的解析 ExpressionParser parser = new SpelExpressionParser(); //SPEL上下文 StandardEvaluationContext context = new StandardEvaluationContext(); //把方法參數放入SPEL上下文中 for(int i=0;i<paraNameArr.length;i++){ context.setVariable(paraNameArr[i], args[i]); } return parser.parseExpression(key).getValue(context,String.class); } //序列化 private String serialize(Object obj) { String result = null; try { result = JsonUtil.serialize(obj); } catch(Exception e) { result = obj.toString(); } return result; } //反序列化 private Object deserialize(String str,Class clazz) { Object result = null; try { if(clazz == JSONObject.class) { result = new JSONObject(str); } else if(clazz == JSONArray.class) { result = new JSONArray(str); } else { result = JsonUtil.deserialize(str,clazz); } } catch(Exception e) { } return result; } //反序列化,支持List<xxx> private Object deserialize(String str,Class clazz,Class elementClass) { Object result = null; try { if(clazz == JSONObject.class) { result = new JSONObject(str); } else if(clazz == JSONArray.class) { result = new JSONArray(str); } else { result = JsonUtil.deserialize(str,clazz,elementClass); } } catch(Exception e) { } return result; } public void setCacheEnable(boolean cacheEnable) { this.cacheEnable = cacheEnable; } }
上面這個(gè)界面使用了一個(gè)cacheEnable變量控制是否使用緩存,為了實(shí)現無(wú)縫的接入springboot,必然需要受到原生@EnableCaching注解的控制,這里我使用一個(gè)spring容器加載完成的監聽(tīng)器,然后在監聽(tīng)器里找到是否有被@EnableCaching注解修飾的類(lèi),如果有就從spring容器拿到MultiCacheAspect對象,然后將cacheEnable設置成true。這樣就可以實(shí)現無(wú)縫接入springboot,不知道朋友們還有沒(méi)有更加優(yōu)雅的方法呢?歡迎交流!監聽(tīng)器類(lèi)如下
import com.xuanwu.apaas.core.multicache.CacheFactory; import com.xuanwu.apaas.core.multicache.MultiCacheAspect; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; import java.util.Map; /** * 用于spring加載完成后,找到項目中是否有開(kāi)啟緩存的注解@EnableCaching * @author rongdi */ @Component public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { // 判斷根容器為Spring容器,防止出現調用兩次的情況(mvc加載也會(huì )觸發(fā)一次) if(event.getApplicationContext().getParent()==null){ //得到所有被@EnableCaching注解修飾的類(lèi) Map<String,Object> beans = event.getApplicationContext().getBeansWithAnnotation(EnableCaching.class); if(beans != null && !beans.isEmpty()) { MultiCacheAspect multiCache = (MultiCacheAspect)event.getApplicationContext().getBean("multiCacheAspect"); multiCache.setCacheEnable(true); } } } }
實(shí)現了無(wú)縫接入,還需要考慮多點(diǎn)部署的時(shí)候,多點(diǎn)的ehcache怎么和redis緩存保持一致的問(wèn)題。在正常應用中,一般redis適合長(cháng)時(shí)間的集中式緩存,ehcache適合短時(shí)間的本地緩存,假設現在有A,B和C服務(wù)器,A和B部署了業(yè)務(wù)服務(wù),C部署了redis服務(wù)。當請求進(jìn)來(lái),前端入口不管是用LVS或者nginx等負載軟件,請求都會(huì )轉發(fā)到某一個(gè)具體服務(wù)器,假設轉發(fā)到了A服務(wù)器,修改了某個(gè)內容,而這個(gè)內容在redis和ehcache中都有,這時(shí)候,A服務(wù)器的ehcache緩存,和C服務(wù)器的redis不管控制緩存失效也好,刪除也好,都比較容易,但是這時(shí)候B服務(wù)器的ehcache怎么控制失效或者刪除呢?一般比較常用的方式就是使用發(fā)布訂閱模式,當需要刪除緩存的時(shí)候在一個(gè)固定的通道發(fā)布一個(gè)消息,然后每個(gè)業(yè)務(wù)服務(wù)器訂閱這個(gè)通道,收到消息后刪除或者過(guò)期本地的ehcache緩存(最好是使用過(guò)期,但是redis目前只支持對key的過(guò)期操作,沒(méi)辦法操作key下的map里的成員的過(guò)期,如果非要強求用過(guò)期,可以自己加時(shí)間戳自己實(shí)現,不過(guò)用刪除出問(wèn)題的幾率也很小,畢竟加緩存的都是讀多寫(xiě)少的應用,這里為了方便都是直接刪除緩存)??偨Y起來(lái)流程就是更新某條數據,先刪除redis中對應的緩存,然后發(fā)布一個(gè)緩存失效的消息在redis的某個(gè)通道中,本地的業(yè)務(wù)服務(wù)去訂閱這個(gè)通道的消息,當業(yè)務(wù)服務(wù)收到這個(gè)消息后去刪除本地對應的ehcache緩存,redis的各種配置如下
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.xuanwu.apaas.core.multicache.subscriber.MessageSubscriber; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.HashMap; import java.util.Map; @Configuration public class MultiCacheConfig { @Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisCacheManager rcm = new RedisCacheManager(redisTemplate); //設置緩存過(guò)期時(shí)間(秒) Map<String, Long> expires = new HashMap<>(); expires.put("ExpOpState",0L); expires.put("ImpOpState",0L); rcm.setExpires(expires); rcm.setDefaultExpiration(600); return rcm; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { StringRedisTemplate template = new StringRedisTemplate(factory); StringRedisSerializer redisSerializer = new StringRedisSerializer(); template.setValueSerializer(redisSerializer); template.afterPropertiesSet(); return template; } /** * redis消息監聽(tīng)器容器 * 可以添加多個(gè)監聽(tīng)不同話(huà)題的redis監聽(tīng)器,只需要把消息監聽(tīng)器和相應的消息訂閱處理器綁定,該消息監聽(tīng)器 * 通過(guò)反射技術(shù)調用消息訂閱處理器的相關(guān)方法進(jìn)行一些業(yè)務(wù)處理 * @param connectionFactory * @param listenerAdapter * @return */ @Bean public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); //訂閱了一個(gè)叫redis.uncache的通道 container.addMessageListener(listenerAdapter, new PatternTopic("redis.uncache")); //這個(gè)container 可以添加多個(gè) messageListener return container; } /** * 消息監聽(tīng)器適配器,綁定消息處理器,利用反射技術(shù)調用消息處理器的業(yè)務(wù)方法 * @param receiver * @return */ @Bean MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) { //這個(gè)地方 是給messageListenerAdapter 傳入一個(gè)消息接受的處理器,利用反射的方法調用“handle” return new MessageListenerAdapter(receiver, "handle"); } }
消息發(fā)布類(lèi)如下:
import com.xuanwu.apaas.core.multicache.CacheFactory; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MessageSubscriber { private static final Logger logger = LoggerFactory.getLogger(MessageSubscriber.class); @Autowired private CacheFactory cacheFactory; /** * 接收到redis訂閱的消息后,將ehcache的緩存失效 * @param message 格式為name_key */ public void handle(String message){ logger.debug("redis.ehcache:"+message); if(StringUtils.isEmpty(message)) { return; } String[] strs = message.split("#"); String name = strs[0]; String key = null; if(strs.length == 2) { key = strs[1]; } cacheFactory.ehDel(name,key); } }
具體操作緩存的類(lèi)如下:
import com.xuanwu.apaas.core.multicache.publisher.MessagePublisher; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.io.InputStream; /** * 多級緩存切面 * @author rongdi */ @Component public class CacheFactory { private static final Logger logger = LoggerFactory.getLogger(CacheFactory.class); @Autowired private RedisTemplate redisTemplate; @Autowired private MessagePublisher messagePublisher; private CacheManager cacheManager; public CacheFactory() { InputStream is = this.getClass().getResourceAsStream("/ehcache.xml"); if(is != null) { cacheManager = CacheManager.create(is); } } public void cacheDel(String name,String key) { //刪除redis對應的緩存 redisDel(name,key); //刪除本地的ehcache緩存,可以不需要,訂閱器那里會(huì )刪除 // ehDel(name,key); if(cacheManager != null) { //發(fā)布一個(gè)消息,告訴訂閱的服務(wù)該緩存失效 messagePublisher.publish(name, key); } } public String ehGet(String name,String key) { if(cacheManager == null) return null; Cache cache=cacheManager.getCache(name); if(cache == null) return null; cache.acquireReadLockOnKey(key); try { Element ele = cache.get(key); if(ele == null) return null; return (String)ele.getObjectValue(); } finally { cache.releaseReadLockOnKey(key); } } public String redisGet(String name,String key) { HashOperations<String,String,String> oper = redisTemplate.opsForHash(); try { return oper.get(name, key); } catch(RedisConnectionFailureException e) { //連接失敗,不拋錯,直接不用redis緩存了 logger.error("connect redis error ",e); return null; } } public void ehPut(String name,String key,String value) { if(cacheManager == null) return; if(!cacheManager.cacheExists(name)) { cacheManager.addCache(name); } Cache cache=cacheManager.getCache(name); //獲得key上的寫(xiě)鎖,不同key互相不影響,類(lèi)似于synchronized(key.intern()){} cache.acquireWriteLockOnKey(key); try { cache.put(new Element(key, value)); } finally { //釋放寫(xiě)鎖 cache.releaseWriteLockOnKey(key); } } public void redisPut(String name,String key,String value) { HashOperations<String,String,String> oper = redisTemplate.opsForHash(); try { oper.put(name, key, value); } catch (RedisConnectionFailureException e) { //連接失敗,不拋錯,直接不用redis緩存了 logger.error("connect redis error ",e); } } public void ehDel(String name,String key) { if(cacheManager == null) return; Cache cache = cacheManager.getCache(name); if(cache != null) { //如果key為空,直接根據緩存名刪除 if(StringUtils.isEmpty(key)) { cacheManager.removeCache(name); } else { cache.remove(key); } } } public void redisDel(String name,String key) { HashOperations<String,String,String> oper = redisTemplate.opsForHash(); try { //如果key為空,直接根據緩存名刪除 if(StringUtils.isEmpty(key)) { redisTemplate.delete(name); } else { oper.delete(name,key); } } catch (RedisConnectionFailureException e) { //連接失敗,不拋錯,直接不用redis緩存了 logger.error("connect redis error ",e); } } }
工具類(lèi)如下
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.json.JSONArray; import org.json.JSONObject; import java.util.*; public class JsonUtil { private static ObjectMapper mapper; static { mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } /** * 將對象序列化成json * * @param obj 待序列化的對象 * @return * @throws Exception */ public static String serialize(Object obj) throws Exception { if (obj == null) { throw new IllegalArgumentException("obj should not be null"); } return mapper.writeValueAsString(obj); } /** 帶泛型的反序列化,比如一個(gè)JSONArray反序列化成List<User> */ public static <T> T deserialize(String jsonStr, Class<?> collectionClass, Class<?>... elementClasses) throws Exception { JavaType javaType = mapper.getTypeFactory().constructParametrizedType( collectionClass, collectionClass, elementClasses); return mapper.readValue(jsonStr, javaType); } /** * 將json字符串反序列化成對象 * @param src 待反序列化的json字符串 * @param t 反序列化成為的對象的class類(lèi)型 * @return * @throws Exception */ public static <T> T deserialize(String src, Class<T> t) throws Exception { if (src == null) { throw new IllegalArgumentException("src should not be null"); } if("{}".equals(src.trim())) { return null; } return mapper.readValue(src, t); } }
具體使用緩存,和之前一樣只需要關(guān)注@Cacheable和@CacheEvict注解,同樣也支持spring的el表達式。而且這里的value屬性表示的緩存名稱(chēng)也沒(méi)有上面說(shuō)的那個(gè)問(wèn)題,完全可以用value隔離不同的緩存,例子如下
@Cacheable(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode") @CacheEvict(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
附上主要的依賴(lài)包
"org.springframework.boot:spring-boot-starter-redis:1.4.2.RELEASE", 'net.sf.ehcache:ehcache:2.10.4', "org.json:json:20160810"
以上就是springboot中如何使用自定義兩級緩存的詳細內容,更多關(guān)于springboot 使用自定義兩級緩存的資料請關(guān)注腳本之家其它相關(guān)文章!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自互聯(lián)網(wǎng)轉載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權請聯(lián)系QQ:712375056 進(jìn)行舉報,并提供相關(guān)證據,一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容。
Copyright ? 2009-2021 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 珠海市特網(wǎng)科技有限公司 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站