Memcached 作用與使用 基本介紹
主要從事網(wǎng)頁(yè)設(shè)計(jì)、PC網(wǎng)站建設(shè)(電腦版網(wǎng)站建設(shè))、wap網(wǎng)站建設(shè)(手機(jī)版網(wǎng)站建設(shè))、響應(yīng)式網(wǎng)站開(kāi)發(fā)、程序開(kāi)發(fā)、微網(wǎng)站、微信小程序定制開(kāi)發(fā)等,憑借多年來(lái)在互聯(lián)網(wǎng)的打拼,我們?cè)诨ヂ?lián)網(wǎng)網(wǎng)站建設(shè)行業(yè)積累了豐富的做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)、網(wǎng)絡(luò)營(yíng)銷經(jīng)驗(yàn),集策劃、開(kāi)發(fā)、設(shè)計(jì)、營(yíng)銷、管理等多方位專業(yè)化運(yùn)作于一體,具備承接不同規(guī)模與類型的建設(shè)項(xiàng)目的能力。
1,對(duì)于緩存的存取方式,簡(jiǎn)言之,就是以鍵值對(duì)的形式將數(shù)據(jù)保存在內(nèi)存中。在日常業(yè)務(wù)中涉及的操作無(wú)非就是增刪改查。加入緩存機(jī)制后,查詢的時(shí)候,對(duì)數(shù)據(jù)進(jìn)行緩存,增刪改的時(shí)候,清除緩存即可。這其中對(duì)于緩存的閉合就非常重要,如果緩存沒(méi)有及時(shí)得到更新,那用戶就會(huì)獲取到過(guò)期數(shù)據(jù),就會(huì)產(chǎn)生問(wèn)題。
2,對(duì)于單一業(yè)務(wù)的緩存管理(數(shù)據(jù)庫(kù)中只操作單表),只需生成一個(gè)key,查詢時(shí),使用key,置入緩存;增刪改時(shí),使用key,清除緩存。將key與表綁定,操作相對(duì)簡(jiǎn)單。
3,但是在現(xiàn)實(shí)業(yè)務(wù)中,更多的是對(duì)關(guān)聯(lián)表的增刪改查(數(shù)據(jù)庫(kù)多表操作),業(yè)務(wù)之間互相關(guān)聯(lián),數(shù)據(jù)庫(kù)中的某張表不止一個(gè)業(yè)務(wù)再進(jìn)行操作,將緩存攔截在service層,對(duì)業(yè)務(wù)進(jìn)行緩存,對(duì)多表進(jìn)行緩存。
4,業(yè)務(wù)層緩存實(shí)現(xiàn)策略:
4.1,在緩存中建立一個(gè)key為"union_query",value為“hashmap<prefix,uqversion>(‘簡(jiǎn)稱uqmap’)“的緩存,prefix保存的是當(dāng)前業(yè)務(wù)操作涉及到的數(shù)據(jù)庫(kù)表名的組合(數(shù)據(jù)庫(kù)表名的唯一性),使用‘|’分隔(例 prefix=“A|B”,此次業(yè)務(wù)將操作A表與B表),uqversion是業(yè)務(wù)版本號(hào),從0開(kāi)始遞增。
4.2,調(diào)用一個(gè)查詢業(yè)務(wù)時(shí),對(duì)數(shù)據(jù)進(jìn)行緩存,設(shè)置operation為1,告訴cache對(duì)象,這是一個(gè)緩存操作,例如調(diào)用 queryAB(args[])方法時(shí),cache對(duì)象切入,將prefix(即”A|B“)與uqversion(初始化為0),存入uqmap中進(jìn)行緩存。
4.3,將prefix,uqversion,方法明+參數(shù),進(jìn)行拼接,使用md5進(jìn)行加密后作為一個(gè)key,將方法的結(jié)果集作為value,進(jìn)行緩存。至此緩存成功。
4.4,當(dāng)?shù)诙€(gè)請(qǐng)求來(lái)調(diào)用queryAB時(shí),cache對(duì)象切入,首先,查詢uqmap對(duì)象,使用prefix找到對(duì)應(yīng)的uqversion,然后,通過(guò)拼接加密獲取key,最后取得結(jié)果集進(jìn)行返回。
4.5,當(dāng)有一個(gè)updateA方法被調(diào)用時(shí),設(shè)置operation為4,告訴cache對(duì)象,這是一個(gè)刪除緩存的操作,此時(shí)prefix的值為“A”,cache對(duì)象切入,獲取全局的uqmap,遍歷其中的prefix,是否包含了表A的名稱:如果包含,則更新此prefix的uqversion進(jìn)行自增,uqversion一旦發(fā)生變化,4.3中組合的key將不復(fù)存在,業(yè)務(wù)緩存也就消失了。(對(duì)于復(fù)雜的updateAB方法,遍歷prefix要復(fù)雜一點(diǎn),可以實(shí)現(xiàn))
4.6,當(dāng)?shù)谌齻€(gè)請(qǐng)求來(lái)調(diào)用queryAB時(shí),可以獲取到uqversion,組合成key后,但是沒(méi)有對(duì)應(yīng)的value。此時(shí)確定緩存不存在時(shí),繼續(xù)正常執(zhí)行方法,獲取結(jié)果集,返回給客戶的同時(shí),將結(jié)果集進(jìn)行緩存。
5,對(duì)于緩存的操作,網(wǎng)上有三種api可以選擇(memcached client forjava、spymemcached、xmemcached),具體的好壞,本人在這就不做分析。本人使用的是XMemcached api。
具體實(shí)現(xiàn)細(xì)節(jié):
1,新建 @interface Annotation{ } 定義一個(gè)注解 @Annotation,一個(gè)注解是一個(gè)類。定義緩存策略。
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 用于查找的時(shí)候,放置緩存信息 * @author shufeng */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface XmemCache{ /** * 值為當(dāng)前操作的表名,表名唯一 * 涉及到多表操作,使用|分隔 */ String prefix() default ""; /* * 緩存有效期 設(shè)置,單位為秒 * 指定間隔時(shí)間,默認(rèn)值為3600秒(1小時(shí)) * */ int interval() default 3600; /** * 1 從cache里取值,如果未置入cache,則置入 * 2 replace cache value 未擴(kuò)展 * 3 replace cache value,并返回舊值 未擴(kuò)展 * 4 remove cache key 從cache里刪除對(duì)應(yīng)的緩存 * 5 remove cache key 從cache里刪除對(duì)應(yīng)的緩存,并返回未刪除之前的值 未擴(kuò)展 **/ int operation() default 1; }
2,memcache基礎(chǔ)操作類,對(duì)一些常用方法進(jìn)行封裝,對(duì)memcachedclient進(jìn)行配置
import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import com.node.hlhw.rbac.api.constant.Constant; import net.rubyeye.xmemcached.GetsResponse; import net.rubyeye.xmemcached.MemcachedClient; import net.rubyeye.xmemcached.MemcachedClientBuilder; import net.rubyeye.xmemcached.XMemcachedClientBuilder; import net.rubyeye.xmemcached.command.BinaryCommandFactory; import net.rubyeye.xmemcached.exception.MemcachedException; import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; import net.rubyeye.xmemcached.utils.AddrUtil; /** * @author Melody shufeng * 對(duì)memcachedclient進(jìn)行封裝,添加一下常用方法 */ public class MemcachedOperate implements DisposableBean { /* * timeout - Operation timeout,if the method is not returned in this * time,throw TimeoutException timeout - operation timeout,in milliseconds * exp - An expiration time, in seconds. Can be up to 30 days. After 30 * days, is treated as a unix timestamp of an exact date. value - stored * data */ private static final Logger logger = LoggerFactory.getLogger(MemcachedOperate.class); private static Properties PROPERTIES = new Properties(); private static String MEMCACHED_SETTING = "memcached.properties"; private static MemcachedClient memcachedClient; public static MemcachedClient getClient(){ return memcachedClient; } /** * 靜態(tài)代碼塊,類加載時(shí),初始化緩存客戶端 * 確保只創(chuàng)建一個(gè)client實(shí)例 * author shufeng */ static { InputStream in = Object.class.getResourceAsStream("/" + MEMCACHED_SETTING); try { PROPERTIES.load(in); } catch (IOException e) { e.printStackTrace(); } String servers = PROPERTIES.getProperty("memcached.servers", ""); if (null != servers && !"".equals(servers)) { try { logger.debug("啟動(dòng)memcached連接"); MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(servers)); builder.setConnectionPoolSize(100); builder.setFailureMode(true); builder.setCommandFactory(new BinaryCommandFactory()); builder.setSessionLocator(new KetamaMemcachedSessionLocator()); builder.setTranscoder(new SerializingTranscoder()); memcachedClient = builder.build(); memcachedClient.setEnableHeartBeat(false); // 關(guān)閉心跳 memcachedClient.flushAll(); // 清空緩存 } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (MemcachedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } /** * @param key * @return 獲取value */ public static Object get(String key) { Object object = null; try { object = memcachedClient.get(key); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (MemcachedException e) { e.printStackTrace(); } return object; } public static void setWithNoReply(String key, int exp, Object value) { try { memcachedClient.setWithNoReply(key, exp, value); } catch (InterruptedException e) { e.printStackTrace(); } catch (MemcachedException e) { e.printStackTrace(); } } /** * 查詢聯(lián)表的業(yè)務(wù)版本號(hào) 如果為空,則初始化 * * @param prefix * @return */ @SuppressWarnings("unchecked") public static Long getUnionQueryVersion(String prefix) { try { Map<String, Long> uqmap = null; GetsResponse<Object> getsresponse = memcachedClient.gets(Constant.UNION_QUERY); if (getsresponse == null) { uqmap = new HashMap<String, Long>(); Long uqversion = new Long(1); // 初始化版本號(hào) uqmap.put(prefix, uqversion); if (memcachedClient.cas(Constant.UNION_QUERY, 0, uqmap, 0)) { // 檢測(cè)插入之前是否被修改過(guò) return uqversion; // 插入成功 } else { // 插入失敗,說(shuō)明在代碼運(yùn)行期間,已經(jīng)有其他線程去修改了unionquery的緩存,重新進(jìn)行查詢 return getUnionQueryVersion(prefix); } } else { long cas = getsresponse.getCas(); Object uqobj = getsresponse.getValue(); if (uqobj == null) { // 不存在對(duì)象 uqmap = new HashMap<String, Long>(); Long uqversion = new Long(1); // 初始化版本號(hào) uqmap.put(prefix, uqversion); if (memcachedClient.cas(Constant.UNION_QUERY, 0, uqmap, cas)) { // 檢測(cè)插入之前是否被修改過(guò) return uqversion; // 插入成功 } else { // 插入失敗,說(shuō)明在代碼運(yùn)行期間,已經(jīng)有其他線程去修改了unionquery的緩存,重新進(jìn)行查詢 return getUnionQueryVersion(prefix); } } else { uqmap = (Map<String, Long>) uqobj; Long uqversion = uqmap.get(prefix); if (uqversion == null) { // 不存在此業(yè)務(wù)版本 uqversion = new Long(1); // 初始化版本號(hào) uqmap.put(prefix, uqversion); if (memcachedClient.cas(Constant.UNION_QUERY, 0, uqmap, cas)) { // 檢測(cè)插入之前是否被修改過(guò) return uqversion; // 插入成功 } else { // 插入失敗,說(shuō)明在代碼運(yùn)行期間,已經(jīng)有其他線程去修改了unionquery的緩存,重新進(jìn)行查詢 return getUnionQueryVersion(prefix); } } else { return uqversion; } } } } catch (TimeoutException | InterruptedException | MemcachedException e) { e.printStackTrace(); System.err.println("getUnionQueryVersion---Exception"); } return 1L; } /** * 查詢單表的業(yè)務(wù)版本號(hào) 如果為空,則初始化 * * @return */ public static Long getVersion(String prefix) { try { GetsResponse<Object> getsresponse = memcachedClient.gets(prefix); if (getsresponse == null) { Long pfversion = new Long(1); if (memcachedClient.cas(prefix, 0, pfversion, 0)) { return pfversion; } else { return getVersion(prefix); } } else { Object pfobj = getsresponse.getValue(); long cas = getsresponse.getCas(); if (pfobj == null) { Long pfversion = new Long(1); if (memcachedClient.cas(prefix, 0, pfversion, cas)) { return pfversion; } else { return getVersion(prefix); } } else { return (Long) pfobj; } } } catch (TimeoutException | InterruptedException | MemcachedException e) { e.printStackTrace(); System.err.println("getVersion---Exception"); } return 1L; } /** * shufeng 更新 多表版本號(hào) * 由于存在線程安全問(wèn)題 ,會(huì)覆蓋uqmap,更新unionquery業(yè)務(wù)版本號(hào) * 使用cas方法解決線程安全問(wèn)題 * 更新unionquery中key包含p1或p2或p3的version * @param prefix */ @SuppressWarnings("unchecked") public static void updateUnionQueryVersion(String prefix) { try { Map<String, Long> uqmap = null; GetsResponse<Object> getsresponse = memcachedClient.gets(Constant.UNION_QUERY); if (getsresponse == null) { return; } else { Object uqobj = getsresponse.getValue(); long cas = getsresponse.getCas(); if (uqobj == null) { return; } else { uqmap = (HashMap<String, Long>) uqobj; Set<String> uqset = uqmap.keySet(); // 遍歷unionquery中的key Iterator<String> quit = uqset.iterator(); String uqkey = ""; boolean uqflag = false; while (quit.hasNext()) { uqkey = quit.next(); if (("|" + uqkey + "|").contains("|" + prefix + "|")) { // key中包含prefix uqmap.put(uqkey, uqmap.get(uqkey) + 1); // 更新map uqflag = true; } } if (uqflag) { if (!memcachedClient.cas(Constant.UNION_QUERY, 0, uqmap, cas)) { updateUnionQueryVersion(prefix); } } } } } catch (TimeoutException | InterruptedException | MemcachedException e) { e.printStackTrace(); System.err.println("updateUnionQueryVersion---Exception"); } } /** * 更新單表版本號(hào) * * @param prefix * @return */ public static void updateVersion(String prefix) { try { GetsResponse<Object> getsresponse; getsresponse = memcachedClient.gets(prefix); if (getsresponse == null) { return ; } else { Object pfobj = getsresponse.getValue(); long cas = getsresponse.getCas(); if (pfobj == null) { return ; } else { Long pfversion = (Long) pfobj; pfversion += 1; if (!memcachedClient.cas(prefix, 0, pfversion, cas)) { updateVersion(prefix); } } } } catch (TimeoutException | InterruptedException | MemcachedException e) { e.printStackTrace(); System.err.println("updateVersion---Exception"); } } public void shutdown() { try { memcachedClient.shutdown(); } catch (IOException e) { e.printStackTrace(); } } @Override public void destroy() throws Exception { shutdown(); } }
3,結(jié)合spring aop 配置緩存,使用spring aop來(lái)切入業(yè)務(wù)層加入緩存,與業(yè)務(wù)進(jìn)行解耦。使用注解進(jìn)行方便配置。
import java.lang.reflect.Method; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import com.alibaba.fastjson.JSON; import com.node.hlhw.common.cache.XmemCache; import com.node.hlhw.common.digest.Md5Utils; @Component @Aspect public class MemcachedAop { @Pointcut("execution (* com.node.hlhw.*.service.impl.*.*(..))") public void pointcut() { } // 方法執(zhí)行前調(diào)用 @Before("pointcut()") public void before() { } // 方法執(zhí)行的前后調(diào)用 /** * * 改進(jìn)建議:使用uuid作為版本號(hào),減少版本號(hào)的讀取,直接生成uuid,進(jìn)行緩存 * 線程安全問(wèn)題:存在線程安全問(wèn)題,但是針對(duì)于緩存,問(wèn)題不大。 * 多線程同一時(shí)間重復(fù)覆蓋一個(gè)業(yè)務(wù)id,還是可以更新緩存 * * @param call * @throws Throwable */ @Around("pointcut()") public Object doAround(ProceedingJoinPoint call) throws Throwable { Object result = null; // 檢測(cè)是否存在memcached客戶端實(shí)例 if (MemcachedOperate.getClient() == null) { System.err.println("memcached client not exist"); result = call.proceed(); return result; } Signature signature = call.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if(!method.isAnnotationPresent(XmemCache.class)){ result = call.proceed(); return result; } XmemCache xmemcache = method.getAnnotation(XmemCache.class); // 獲取操作方法 int operation = xmemcache.operation(); // 獲取注解前綴,實(shí)際使用是為各個(gè)業(yè)務(wù)包名稱,一般為表名 String prefix = xmemcache.prefix(); // 無(wú)前綴 if(prefix==null||"".equals(prefix)){ result = call.proceed(); return result; } // 獲取注解配置memcached死亡時(shí)間 秒單位 int interval = xmemcache.interval(); switch (operation) { case 1: // 1 從cache里取值,如果未置入cache,則置入 // 判斷prefix是否涉及多表,查看是否包含| if (prefix.contains("|")) { Long uqversion = MemcachedOperate.getUnionQueryVersion(prefix); String combinedkey = generCombinedKey(prefix, uqversion, method, call.getArgs()); Object resultobj = MemcachedOperate.get(combinedkey); if(resultobj == null){ result = call.proceed(); MemcachedOperate.setWithNoReply(combinedkey, interval, JSON.toJSONString(result));// 緩存數(shù)據(jù) }else{ Class<?> returnType = ((MethodSignature) signature).getReturnType(); result = JSON.parseObject(resultobj.toString(), returnType); } } else { // 單表操作 Long pfversion = MemcachedOperate.getVersion(prefix); String combinedkey = generCombinedKey(prefix, pfversion, method, call.getArgs()); Object resultobj = MemcachedOperate.get(combinedkey); if(resultobj == null){ result = call.proceed(); MemcachedOperate.setWithNoReply(combinedkey, interval, JSON.toJSONString(result));// 緩存數(shù)據(jù) }else{ Class<?> returnType = ((MethodSignature) signature).getReturnType(); result = JSON.parseObject(resultobj.toString(), returnType); } } break; case 2: // 2 replace cache value break; case 3: break; case 4: // 4 remove cache key 從cache里刪除對(duì)應(yīng) 業(yè)務(wù)版本的緩存 /* * 更新unionquery業(yè)務(wù)版本號(hào) * 0,切割 prefix為p1、p2、p3 * 1,更新prefix為p1或p2或p3的version * 2,更新unionquery中key包含p1或p2或p3的version */ if (prefix.contains("|")) { // 表示涉及到多表,需要清除 單表的緩存,與聯(lián)表中 包含 當(dāng)前部分的 緩存 String[] prefixs = prefix.split("\\|"); // 0.切割 prefix為p1、p2、p3 for(String pf : prefixs){ MemcachedOperate.updateVersion(pf); // 1,更新prefix為p1或p2或p3的version MemcachedOperate.updateUnionQueryVersion(pf); } }else{ // 沒(méi)有涉及到多表的時(shí)候 MemcachedOperate.updateVersion(prefix); MemcachedOperate.updateUnionQueryVersion(prefix); } result = call.proceed(); break; default: result = call.proceed(); break; } return result; } /** * 組裝key值 * @param key * @param version * @param method * @param args * @return */ private String generCombinedKey(String key, Long version, Method method, Object[] args) { StringBuffer sb = new StringBuffer(); // 獲取方法名 String methodName = method.getName(); // 獲取參數(shù)類型 Object[] classTemps = method.getParameterTypes(); // 存入方法名 sb.append(methodName); for (int i = 0; i < args.length; i++) { sb.append(classTemps[i] + "&"); if (null == args[i]) { sb.append("null"); } else if ("".equals(args[i])) { sb.append("*"); } else { String tt = JSON.toJSONString(args[i]); sb.append(tt); } } sb.append(key); sb.append(version.toString()); String temp = Md5Utils.getMD5(sb.toString()); return temp; } }
4,properties文件中配置memcached服務(wù)器地址
#host1:port1,host2:port2 memcached.servers=192.168.1.1:11211,192.168.1.2:11211
5,修改spring配置文件,聲明自動(dòng)為spring容器中那些配置@aspectJ切面的bean創(chuàng)建代理,織入切面。
<aop:aspectj-autoproxy proxy-target-class="true"/>
6,service層使用注解方式切入緩存
import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import org.apache.ibatis.session.RowBounds; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import com.alibaba.dubbo.common.utils.StringUtils; import com.alibaba.dubbo.config.annotation.Service; import com.node.hlhw.common.cache.XmemCache; import com.node.hlhw.common.digest.ApplicationUtils; import com.node.hlhw.core.service.BaseService; import com.node.hlhw.core.store.IBaseStore; import com.node.hlhw.core.store.PageParam; import com.node.hlhw.rbac.api.dao.UserRoleDao; import com.node.hlhw.rbac.api.entity.UserRole; import com.node.hlhw.rbac.api.service.UserRoleService; /** * @author Melody * 處理用戶角色 */ @Service(version = "1.0.0") public class UserRoleServiceImpl extends BaseService<UserRole> implements UserRoleService { private static final Logger logger = Logger .getLogger(UserRoleServiceImpl.class); @Autowired public UserRoleDao userRoleDao; @Override protected IBaseStore<UserRole> getBaseDao() { return userRoleDao; } /* * 單表操作,prefix為表名,operation為4,只進(jìn)行緩存的刪除操作 */ @XmemCache(prefix="userrole",operation=4) public void insertUserRole(UserRole userRole) throws Exception { userRoleDao.insertUserRole(userRole); logger.info("插入用戶角色數(shù)據(jù)"); } /* (non-Javadoc) * 此方法操作了兩個(gè)表,role與userole,使用‘|’進(jìn)行分隔 * operation為1,表示緩存操作,對(duì)結(jié)果集進(jìn)行緩存 * interval表示緩存時(shí)間默認(rèn)不填為3600秒,也可指定具體時(shí)長(zhǎng) */ @Override @XmemCache(prefix="role|userrole",interval=3600 , operation=1) public List<Map<String, Object>> selectUserRoleList(UserRole userrole, PageParam pageParam) throws Exception { RowBounds rowBounds = new RowBounds(pageParam.getOffset(),pageParam.getLimit()); List<Map<String, Object>> list = userRoleDao.selectUserRoleList(userrole,rowBounds); return list ; } @Override @XmemCache(prefix="userrole" , operation=4) public void modifyUserRole(UserRole userrole, String[] roleids)throws Exception { //刪除所包含的角色 userRoleDao.deleteByUserRole(userrole); for(String roleid : roleids){ if(!StringUtils.isEmpty(roleid)){ userrole.setCreatetime(new Date()); userrole.setRoleid(roleid); userrole.setUuid(ApplicationUtils.getUUID()); userRoleDao.insertUserRole(userrole); } } } @Override @XmemCache(prefix="userrole" , operation=1) public boolean existsRef(String roleids)throws Exception { String [] roleid = roleids.split(","); List<String> roleidlist = Arrays.asList(roleid); return userRoleDao.existsRef(roleidlist)>0?true:false; } }
網(wǎng)站名稱:springaop+xmemcached配置service層緩存策略
文章轉(zhuǎn)載:http://aaarwkj.com/article14/peeede.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站策劃、網(wǎng)站設(shè)計(jì)、企業(yè)建站、域名注冊(cè)、用戶體驗(yàn)、網(wǎng)站營(yíng)銷
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)