概述
本文先簡單介紹spring-cache的使用即流程系任。再來了解使用cache會帶來與數(shù)據(jù)庫不一致的問題
- spring-cache使用介紹
- spring-cache實現(xiàn)原理
- 使用redis緩存是數(shù)據(jù)庫一致性解決方案
spring-cache使用介紹
spring-cache支持注解(annotation)和xml兩種配置.本次只展示注解(annotation)的使用.它本質(zhì)上不是一個具體的緩存實現(xiàn)方案(例如 EHCache和Redis),而是一個對緩存使用的抽象框架
同時spring-cache還有一個強大的地方就是配置SpEL表達式來定義各種緩存的key和condition,還提供開箱即用的緩存臨時存儲方案慌烧,也支持和主流的專業(yè)緩存例如 EHCache 集成负乡。
1.spring-cache使用
spring-cache常用注解:
- @Cacheable (存在緩存則直接返回,不存在則調(diào)用業(yè)務(wù)方法,保存到緩存)
- @CacheEvict (清楚緩存,可清楚cache里全部緩存)
- @CachePut (不管緩存存不存在,調(diào)用業(yè)務(wù)方法,將返回值set到緩存里面)
示例代碼:
public class AccountService {
@Cacheable(value="accountCache", key="#userName", )// 使用了一個緩存名叫 accountCache,key為userName,value為Account對象
public Account getAccountByName(String userName) {
// 方法內(nèi)部實現(xiàn)不考慮緩存邏輯贯吓,直接實現(xiàn)業(yè)務(wù)
System.out.println("real query account."+userName);
return getFromDB(userName);
}
@CacheEvict(value="accountCache",key="#account.getName()")// 刪除 accountCache 緩存 里面key為userName的緩存
public void updateAccount(Account account) {
updateDB(account);
}
@CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 緩存
public void reload() {
}
private Account getFromDB(String acctName) {
System.out.println("real querying db..."+acctName);
return new Account(acctName);
}
private void updateDB(Account account) {
System.out.println("real update db..."+account.getName());
}
}
建議:假如使用的cache方案是redis的話,因為大多數(shù)場景都是多個業(yè)務(wù)線使用同一個redis,一不小心定義的的緩存key可能你會相同.所以最好在初始化RedisCacheManager(spring-data-redis.jar)時設(shè)置usePrefix為true.這樣生成的key都會帶cacheName前綴,防止和其他業(yè)務(wù)的key重復(fù)
生成redis-key的代碼:
/**
* Get the {@link Byte} representation of the given key element using prefix if available.
*/
public byte[] getKeyBytes() {
byte[] rawKey = serializeKeyElement();
if (!hasPrefix()) {
return rawKey;//沒有前綴直接返回用戶設(shè)置的key
}
byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length);
System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length); //拼裝prefix和key
return prefixedKey;
}
/**
* @return true if prefix is not empty.
*/
public boolean hasPrefix() {
//usePrefix為true時,該prefix為cacheName
return (prefix != null && prefix.length > 0);
}
由于這方面的使用網(wǎng)上一大堆,這里就在累述.
2. spring-cache實現(xiàn)原理
本質(zhì)是使用spring-aop實現(xiàn).<cache:annotation-driven>
開啟CacheInterceptor
注冊到springContext里面.業(yè)務(wù)運行時調(diào)用代理類執(zhí)行方法spring的所有Interceptor方法,里面包括CacheInterceptor
.
上圖表現(xiàn)spring-aop兩種重播方式,體現(xiàn)了aop的兩種配置方式(@Before,@Around)方式.
cacheInterceptor的實現(xiàn)流程:
上圖用顏色區(qū)分了每個注解具體的作用:
黃色:@CacheEvict 根據(jù)beforInvocation判斷是前置刪除還是后置刪除.默認(rèn)是false后置上出
藍(lán)色:@Cacheable 判斷condition條件是否滿足再去緩存里面獲取數(shù)據(jù),沒有命中最后會更新到緩存里面
綠色:@CachePut 判斷condition條件是否滿足,然后會更新到緩存里面
深藍(lán):@Cacheable@CachePut 兩個都有更新緩存的操作,所以代碼整理到一塊.
3.高并發(fā)使用的問題
-
先更新數(shù)據(jù)庫在更新緩存時失敗
image.png
如上圖redis更新失敗則會造成數(shù)據(jù)不一致的情況,知道緩存超時自動刪除或則下次更新才可能一致
解決辦法:
如上圖,把刪除緩存方法放前面,加入刪除失敗則不會操作數(shù)據(jù)庫,這樣就不會造成數(shù)據(jù)不一致的情況.就算出現(xiàn)redis刪除成功,但是超時的問題,最多也是多執(zhí)行一次存入緩存的操作.
-
高并發(fā)下不一致問題
image.png
如上圖正好卡在剛刪除緩存就有一個線程來查詢緩存,就會出現(xiàn)redis里面是舊的數(shù)據(jù),數(shù)據(jù)庫時新的數(shù)據(jù).
解決辦法:
如上圖的解決辦法,主要思想就是把可能出現(xiàn)的(刪除,修改)并發(fā)執(zhí)行通過redis的分布式鎖實現(xiàn)串行.這里有個優(yōu)化點就是讀數(shù)據(jù)沒有獲取鎖成功的話會等待200ms在嘗試讀取緩存,不存在則直接讀取數(shù)據(jù)庫返回.