緩存
一般來(lái)說(shuō)緩存分:本地緩存描滔、分布式緩存蔬顾。
本地緩存有:Guava、Caffeine等昭齐;分布式緩存大部分由NoSql數(shù)據(jù)庫(kù)組成:redis尿招、memcache等。
本地緩存最大的優(yōu)勢(shì)是沒有網(wǎng)絡(luò)的限制阱驾、穩(wěn)定就谜、速度快,缺點(diǎn)是容量上限小里覆,同時(shí)只能供單體應(yīng)用使用丧荐。
我們先分別簡(jiǎn)單試一下本地緩存和分布式緩存,然后嘗試寫一個(gè)兼容兩者的版本喧枷。
緩存邏輯
我們對(duì)dao
層進(jìn)行切面虹统,大致的思路如下:
- 針對(duì)
dao
層每個(gè)類每個(gè)方法做切面。 -
Before
隧甚,判斷SqlCommandType
:- 如果是
SELECT
车荔,則嘗試取緩存。 - 否則清除緩存呻逆。
- 清除緩存時(shí)夸赫,順便把相關(guān)表下的所有key清除。
- 如果是
- 非
SELECT
或取不到緩存就執(zhí)行dao
-
after
咖城,判斷操作是:- 如果是
SELECT
,且沒有緩存呼奢,則緩存當(dāng)前方法及返回值宜雀。 - 保存緩存時(shí),順便把相關(guān)表和key的映射放入緩存
- 如果是
邏輯實(shí)現(xiàn)
MyCacheInterceptor
攔截器握础,包含了大部分的邏輯辐董。
package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
@Aspect
@Component
public class MyCacheInterceptor {
@Autowired
SqlSessionFactory sqlSessionFactory;
@Autowired
MyCacheFactory myCacheFactory;
//execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
@Pointcut("execution(public * com.it_laowu.springbootstudy.springbootstudydemo.dao.*.*(..))")
private void anyMethod(){}
@Around("anyMethod()")
public Object aroundDao(ProceedingJoinPoint pjp) throws Throwable {
CacheKeyInfo cacheKeyInfo = getCacheKeyInfo(pjp);
Pair<Boolean, Object> ret = beforeProceed(cacheKeyInfo);
Object retData;
if (ret.getFirst()){
retData = ret.getSecond();
}
else{
retData=pjp.proceed();
}
afterProceed(cacheKeyInfo,retData,ret.getFirst());
return retData;
}
private void afterProceed(CacheKeyInfo cacheKeyInfo, Object retData,boolean exists) {
Console.log("after caching*******************************************");
if (cacheKeyInfo.getSqlCommandType()==SqlCommandType.SELECT && !exists){
setCacheData(cacheKeyInfo,retData);
}
// gc無(wú)法回收caffeine的正常引用類型
// System.gc();
}
private Pair<Boolean, Object> beforeProceed(CacheKeyInfo cacheKeyInfo) {
Console.log("before caching******************************************");
//這里粗糙一點(diǎn),其實(shí)可以自定義哪些command取緩存禀综,哪些刪緩存
if (cacheKeyInfo.getSqlCommandType()==SqlCommandType.SELECT){
return getCacheData(cacheKeyInfo);
}
else{
evictCacheData(cacheKeyInfo);
}
return Pair.of(false, "");
}
private CacheKeyInfo getCacheKeyInfo(ProceedingJoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Class<?> daoInterface = pjp.getTarget().getClass().getInterfaces()[0];
Method method = methodSignature.getMethod();
return new CacheKeyInfo(daoInterface,method,pjp.getArgs(),sqlSessionFactory.getConfiguration());
}
private Pair<Boolean, Object> getCacheData(CacheKeyInfo cacheKeyInfo) {
MyCache cache = myCacheFactory.getCache(cacheKeyInfo);
String key = cacheKeyInfo.getCacheKey();
if (cache.exists(key)) {
return Pair.of(true, cache.get(key));
} else {
return Pair.of(false, "");
}
}
private void setCacheData(CacheKeyInfo cacheKeyInfo,Object value){
MyCache cache = myCacheFactory.getCache(cacheKeyInfo);
cache.set(cacheKeyInfo, value);
}
private void evictCacheData(CacheKeyInfo cacheKeyInfo){
MyCache cache = myCacheFactory.getCache(cacheKeyInfo);
cache.evict(cacheKeyInfo);
}
}
CacheKeyInfo
用于計(jì)算處理緩存的key
简烘、sqlCommandType
苔严、涉及表refTables
。
pom.xml增加依賴:
<!-- sql語(yǔ)句解析 -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>3.2</version>
</dependency>
package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
@Data
public class CacheKeyInfo {
private Object[] methodArgs;
private Method method;
private Configuration configuration;
private MapperMethod.MethodSignature methodSignature;
private Class<?> daoInterface;
private String cacheKey;
private SqlCommandType sqlCommandType;
private List<String> refTableNames;
public CacheKeyInfo(Class<?> daoInterface, Method method, Object[] methodArgs, Configuration configuration) {
this.daoInterface = daoInterface;
this.method = method;
this.methodArgs = methodArgs;
this.configuration = configuration;
this.methodSignature = new MapperMethod.MethodSignature(configuration, daoInterface, method);
this.cacheKey = createCacheKey();
this.sqlCommandType = createSqlCommandType();
this.refTableNames = createRefTableNames();
}
private String createCacheKey() {
StringBuilder sb = new StringBuilder();
sb.append(daoInterface.getName());
sb.append("::");
sb.append(method.getName());
sb.append("::");
Arrays.stream(getMethodArgs()).forEach(i -> {
sb.append(i.toString());
});
return sb.toString();
}
private SqlCommandType createSqlCommandType() {
String mapperId = getDaoInterface().getName() + "." + getMethod().getName();
return getConfiguration().getMappedStatement(mapperId, false).getSqlCommandType();
}
private List<String> createRefTableNames() {
String mapperId = getDaoInterface().getName() + "." + getMethod().getName();
MappedStatement mappedStatement = getConfiguration().getMappedStatement(mapperId);
Object sqlCommandParam = getMethodSignature().convertArgsToSqlCommandParam(getMethodArgs());
BoundSql boundSql = mappedStatement.getBoundSql(wrapCollection(sqlCommandParam));
String sql = boundSql.getSql();
return getTablesFromSql(sql);
}
private List<String> getTablesFromSql(String sql) {
try {
Statement statement;
statement = CCJSqlParserUtil.parse(sql);
TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
List<String> tableList = tablesNamesFinder.getTableList(statement);
// Select selectStatement = (Select) statement;
return tableList;
} catch (JSQLParserException e) {
e.printStackTrace();
}
return new ArrayList<String>();
}
private Object wrapCollection(final Object object) {
if (object instanceof Collection) {
DefaultSqlSession.StrictMap<Object> map = new DefaultSqlSession.StrictMap<Object>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
DefaultSqlSession.StrictMap<Object> map = new DefaultSqlSession.StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}
}
抽象Cache
我們使用一個(gè)MyCache
接口孤澎,定義一下常用的緩存操作届氢,再定一個(gè)MyCacheFactory
,管理多種緩存覆旭。
MyCache
注意這里清除緩存用了兩個(gè)方法退子,一個(gè)清除key,一個(gè)清除當(dāng)前操作相關(guān)的所有緩存型将。
package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
public interface MyCache {
Object get(String key);
boolean set(String key,Object value);
boolean set(CacheKeyInfo cacheKeyInfo,Object value);
boolean evict(String key);
boolean evict(CacheKeyInfo cacheKeyInfo);
boolean exists(String key);
}
MyCacheFactory
這里先考慮本地緩存寂祥,所以寫死用local
。
package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
@Component
public class MyCacheFactory {
@Autowired
private RedisUtil redisUtil;
@Autowired
private CacheProperties cacheProperties;
private Map<String, MyCache> caches = new ConcurrentHashMap<String, MyCache>();
public MyCache getCache(CacheKeyInfo cacheKeyInfo) {
if (caches.isEmpty()) {
init();
}
//這里可以根據(jù)cacheProperties七兜,返回不同類型cache
return caches.get("distribution");
}
private synchronized void init() {
Cache<String, Object> cacheObj = Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.HOURS).maximumSize(10000).build();
caches.put("local", new MyCaffeineCache(cacheObj));
caches.put("distribution", new MyRedisCache(redisUtil, cacheProperties));
}
}
本地緩存實(shí)現(xiàn)
我選擇使用Caffeine做為本地緩存丸凭。實(shí)現(xiàn)MyCache
接口就可以了。
Caffeine
是一種非常好用的本地緩存腕铸,特點(diǎn)是使用了W-TinyLFU
算法贮乳,大致原理是:
- 將所有鍵通過(guò)hash值映射到某塊bit數(shù)組,并統(tǒng)計(jì)訪問(wèn)頻率恬惯。同時(shí)使用
- 將緩存分為:窗口緩存LRU算法占1%向拆、主緩存SLRU+LFU算法占99%(又分為熱80%、冷20%)
- 窗口緩存中的LRU犧牲者酪耳,進(jìn)入主緩存的冷區(qū)域浓恳,進(jìn)行PK。
- 算法3大大降低了熱點(diǎn)key由于沒有積累頻率被淘汰的可能性碗暗。
pom中添加依賴:
<!-- 緩存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.6</version>
</dependency>
MyCaffeineCache
package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
public class MyCaffeineCache implements MyCache {
private Cache<String, Object> cacheObj;
public MyCaffeineCache(Cache<String, Object> cacheObj) {
this.cacheObj = cacheObj;
}
@Override
public Object get(String key) {
DebugUtils.writeInColor("this is get from caffeine");
return cacheObj.getIfPresent(key);
}
@Override
public boolean set(String key, Object value) {
try {
cacheObj.put(key, value);
DebugUtils.writeInColor("this is put in caffeine");
return true;
} catch (Exception ex) {
return false;
}
}
@Override
public boolean evict(String key) {
try {
cacheObj.invalidate(key);
return true;
} catch (Exception ex) {
return false;
}
}
@Override
public boolean exists(String key) {
return cacheObj.asMap().containsKey(key);
}
@Override
public boolean set(CacheKeyInfo cacheKeyInfo, Object value) {
try {
String key = cacheKeyInfo.getCacheKey();
cacheObj.put(key, value);
cacheKeyInfo.getRefTableNames().forEach(i -> {
cacheObj.asMap().compute(i, (k, v) -> {
if (v == null) {
List<String> o = new ArrayList<String>();
o.add(key);
return o;
} else {
List<String> o = (List<String>) v;
o.remove(key);
o.add(key);
return o;
}
});
});
DebugUtils.writeInColor("this is put ref in caffeine");
return true;
} catch (Exception ex) {
return false;
}
}
@Override
public boolean evict(CacheKeyInfo cacheKeyInfo) {
try {
//清緩存并不是清除當(dāng)前key颈将,而是key -->表-->key
List<String> tables = cacheKeyInfo.getRefTableNames();
List<String> keys = new ArrayList<String>();
cacheKeyInfo.getRefTableNames().forEach(i -> {
keys.addAll((ArrayList<String>)cacheObj.getIfPresent(i));
});
cacheObj.invalidateAll(keys);
cacheObj.invalidateAll(tables);
DebugUtils.writeInColor("this is evict ref in caffeine");
return true;
} catch (Exception ex) {
return false;
}
}
}
分布式緩存Redis
package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
public class MyRedisCache implements MyCache {
private RedisUtil redisUtil;
private CacheProperties cacheProperties;
public MyRedisCache(RedisUtil _redisUtil, CacheProperties _cacheProperties) {
redisUtil = _redisUtil;
cacheProperties = _cacheProperties;
}
@Override
public Object get(String key) {
DebugUtils.writeInColor("this is get from redis");
return redisUtil.get(key);
}
@Override
public boolean set(String key, Object value) {
try {
DebugUtils.writeInColor("this is put in redis");
return redisUtil.set(key, value);
} catch (Exception ex) {
return false;
}
}
@Override
public boolean evict(String key) {
try {
redisUtil.del(key);
return true;
} catch (Exception ex) {
return false;
}
}
@Override
public boolean exists(String key) {
return redisUtil.hasKey(key);
}
@Override
public boolean set(CacheKeyInfo cacheKeyInfo, Object value) {
try {
String key = cacheKeyInfo.getCacheKey();
Boolean b = redisUtil.set(key, value,cacheProperties.getExpireSecond());
cacheKeyInfo.getRefTableNames().forEach(i -> {
redisUtil.sSetAndTime(i, cacheProperties.getExpireSecond(), key);
});
DebugUtils.writeInColor("this is put ref in redis");
return b;
} catch (Exception ex) {
return false;
}
}
@Override
public boolean evict(CacheKeyInfo cacheKeyInfo) {
try {
//清緩存并不是清除當(dāng)前key,而是key -->表-->key
String[] tables = cacheKeyInfo.getRefTableNames().toArray(new String[0]);
List<String> keys = new ArrayList<String>();
cacheKeyInfo.getRefTableNames().forEach(i -> {
keys.addAll(Arrays.asList(redisUtil.sGet(i).toArray(new String[0])));
});
redisUtil.del(keys.toArray(new String[0]));
redisUtil.del(tables);
DebugUtils.writeInColor("this is evict ref in redis");
return true;
} catch (Exception ex) {
return false;
}
}
}
總結(jié)
本文初步實(shí)現(xiàn)了一個(gè)切面層級(jí)的多級(jí)緩存(本地+分布式)言疗。應(yīng)該說(shuō)是相當(dāng)初步的晴圾,還有很多細(xì)節(jié)可以完善:
- 把一些常用參數(shù)維護(hù)到application.yml中
- 增加注解型緩存,優(yōu)先級(jí)應(yīng)該高于切面噪奄。
- 緩存分級(jí)死姚,同時(shí)加入算法
- 實(shí)際使用中,發(fā)現(xiàn)讀取mybatis設(shè)置比較費(fèi)時(shí)勤篮,可以考慮加入字典都毒。
- redis支持Cluster
感興趣的可以留言,給我進(jìn)一步完善的動(dòng)力碰缔。
完畢账劲。