spring boot中集成了spring cache,并有多種緩存方式的實(shí)現(xiàn),如:redis、caffeine、jcache、ehcache等等。但如果只用一種緩存,要么會(huì)有較大的網(wǎng)絡(luò)消耗(如redis),要么就是內(nèi)存占用太大(如caffeine這種應(yīng)用內(nèi)存緩存)。在很多場(chǎng)景下,可以結(jié)合起來(lái)實(shí)現(xiàn)一、二級(jí)緩存的方式,能夠很大程度提高應(yīng)用的處理效率。
內(nèi)容說明:
- 緩存、兩級(jí)緩存
- spring cache:主要包含spring cache定義的接口方法說明和注解中的屬性說明
- spring boot + spring cache:rediscache實(shí)現(xiàn)中的缺陷
- caffeine簡(jiǎn)介
- spring boot + spring cache 實(shí)現(xiàn)兩級(jí)緩存(redis + caffeine)
緩存、兩級(jí)緩存
簡(jiǎn)單的理解,緩存就是將數(shù)據(jù)從讀取較慢的介質(zhì)上讀取出來(lái)放到讀取較快的介質(zhì)上,如磁盤-->內(nèi)存。平時(shí)我們會(huì)將數(shù)據(jù)存儲(chǔ)到磁盤上,如:數(shù)據(jù)庫(kù)。如果每次都從數(shù)據(jù)庫(kù)里去讀取,會(huì)因?yàn)榇疟P本身的io影響讀取速度,所以就有了像redis這種的內(nèi)存緩存。可以將數(shù)據(jù)讀取出來(lái)放到內(nèi)存里,這樣當(dāng)需要獲取數(shù)據(jù)時(shí),就能夠直接從內(nèi)存中拿到數(shù)據(jù)返回,能夠很大程度的提高速度。但是一般redis是單獨(dú)部署成集群,所以會(huì)有網(wǎng)絡(luò)io上的消耗,雖然與redis集群的鏈接已經(jīng)有連接池這種工具,但是數(shù)據(jù)傳輸上也還是會(huì)有一定消耗。所以就有了應(yīng)用內(nèi)緩存,如:caffeine。當(dāng)應(yīng)用內(nèi)緩存有符合條件的數(shù)據(jù)時(shí),就可以直接使用,而不用通過網(wǎng)絡(luò)到redis中去獲取,這樣就形成了兩級(jí)緩存。應(yīng)用內(nèi)緩存叫做一級(jí)緩存,遠(yuǎn)程緩存(如redis)叫做二級(jí)緩存
spring cache
當(dāng)使用緩存的時(shí)候,一般是如下的流程:
從流程圖中可以看出,為了使用緩存,在原有業(yè)務(wù)處理的基礎(chǔ)上,增加了很多對(duì)于緩存的操作,如果將這些耦合到業(yè)務(wù)代碼當(dāng)中,開發(fā)起來(lái)就有很多重復(fù)性的工作,并且不太利于根據(jù)代碼去理解業(yè)務(wù)。
spring cache是spring-context包中提供的基于注解方式使用的緩存組件,定義了一些標(biāo)準(zhǔn)接口,通過實(shí)現(xiàn)這些接口,就可以通過在方法上增加注解來(lái)實(shí)現(xiàn)緩存。這樣就能夠避免緩存代碼與業(yè)務(wù)處理耦合在一起的問題。spring cache的實(shí)現(xiàn)是使用spring aop中對(duì)方法切面(methodinterceptor)封裝的擴(kuò)展,當(dāng)然spring aop也是基于aspect來(lái)實(shí)現(xiàn)的。
spring cache核心的接口就兩個(gè):cache和cachemanager
cache接口
提供緩存的具體操作,比如緩存的放入、讀取、清理,spring框架中默認(rèn)提供的實(shí)現(xiàn)有:
除了rediscache是在spring-data-redis包中,其他的基本都是在spring-context-support包中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
#cache.java package org.springframework.cache; import java.util.concurrent.callable; public interface cache { // cachename,緩存的名字,默認(rèn)實(shí)現(xiàn)中一般是cachemanager創(chuàng)建cache的bean時(shí)傳入cachename string getname(); // 獲取實(shí)際使用的緩存,如:redistemplate、com.github.benmanes.caffeine.cache.cache<object, object>。暫時(shí)沒發(fā)現(xiàn)實(shí)際用處,可能只是提供獲取原生緩存的bean,以便需要擴(kuò)展一些緩存操作或統(tǒng)計(jì)之類的東西 object getnativecache(); // 通過key獲取緩存值,注意返回的是valuewrapper,為了兼容存儲(chǔ)空值的情況,將返回值包裝了一層,通過get方法獲取實(shí)際值 valuewrapper get(object key); // 通過key獲取緩存值,返回的是實(shí)際值,即方法的返回值類型 <t> t get(object key, class <t> type); // 通過key獲取緩存值,可以使用valueloader.call()來(lái)調(diào)使用@cacheable注解的方法。當(dāng)@cacheable注解的sync屬性配置為true時(shí)使用此方法。因此方法內(nèi)需要保證回源到數(shù)據(jù)庫(kù)的同步性。避免在緩存失效時(shí)大量請(qǐng)求回源到數(shù)據(jù)庫(kù) <t> t get(object key, callable<t> valueloader); // 將@cacheable注解方法返回的數(shù)據(jù)放入緩存中 void put(object key, object value); // 當(dāng)緩存中不存在key時(shí)才放入緩存。返回值是當(dāng)key存在時(shí)原有的數(shù)據(jù) valuewrapper putifabsent(object key, object value); // 刪除緩存 void evict(object key); // 刪除緩存中的所有數(shù)據(jù)。需要注意的是,具體實(shí)現(xiàn)中只刪除使用@cacheable注解緩存的所有數(shù)據(jù),不要影響應(yīng)用內(nèi)的其他緩存 void clear(); // 緩存返回值的包裝 interface valuewrapper { // 返回實(shí)際緩存的對(duì)象 object get(); } // 當(dāng){@link #get(object, callable)}拋出異常時(shí),會(huì)包裝成此異常拋出 @suppresswarnings ( "serial" ) class valueretrievalexception extends runtimeexception { private final object key; public valueretrievalexception(object key, callable<?> loader, throwable ex) { super (string.format( "value for key '%s' could not be loaded using '%s'" , key, loader), ex); this .key = key; } public object getkey() { return this .key; } } } |
cachemanager接口
主要提供cache實(shí)現(xiàn)bean的創(chuàng)建,每個(gè)應(yīng)用里可以通過cachename來(lái)對(duì)cache進(jìn)行隔離,每個(gè)cachename對(duì)應(yīng)一個(gè)cache實(shí)現(xiàn)。spring框架中默認(rèn)提供的實(shí)現(xiàn)與cache的實(shí)現(xiàn)都是成對(duì)出現(xiàn),包結(jié)構(gòu)也在上圖中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#cachemanager.java package org.springframework.cache; import java.util.collection; public interface cachemanager { // 通過cachename創(chuàng)建cache的實(shí)現(xiàn)bean,具體實(shí)現(xiàn)中需要存儲(chǔ)已創(chuàng)建的cache實(shí)現(xiàn)bean,避免重復(fù)創(chuàng)建,也避免內(nèi)存緩存對(duì)象(如caffeine)重新創(chuàng)建后原來(lái)緩存內(nèi)容丟失的情況 cache getcache(string name); // 返回所有的cachename collection<string> getcachenames(); } |
常用注解說明
@cacheable:主要應(yīng)用到查詢數(shù)據(jù)的方法上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package org.springframework.cache.annotation; 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; import java.util.concurrent.callable; import org.springframework.core.annotation.aliasfor; @target ({elementtype.method, elementtype.type}) @retention (retentionpolicy.runtime) @inherited @documented public @interface cacheable { // cachenames,cachemanager就是通過這個(gè)名稱創(chuàng)建對(duì)應(yīng)的cache實(shí)現(xiàn)bean @aliasfor ( "cachenames" ) string[] value() default {}; @aliasfor ( "value" ) string[] cachenames() default {}; // 緩存的key,支持spel表達(dá)式。默認(rèn)是使用所有參數(shù)及其計(jì)算的hashcode包裝后的對(duì)象(simplekey) string key() default "" ; // 緩存key生成器,默認(rèn)實(shí)現(xiàn)是simplekeygenerator string keygenerator() default "" ; // 指定使用哪個(gè)cachemanager string cachemanager() default "" ; // 緩存解析器 string cacheresolver() default "" ; // 緩存的條件,支持spel表達(dá)式,當(dāng)達(dá)到滿足的條件時(shí)才緩存數(shù)據(jù)。在調(diào)用方法前后都會(huì)判斷 string condition() default "" ; // 滿足條件時(shí)不更新緩存,支持spel表達(dá)式,只在調(diào)用方法后判斷 string unless() default "" ; // 回源到實(shí)際方法獲取數(shù)據(jù)時(shí),是否要保持同步,如果為false,調(diào)用的是cache.get(key)方法;如果為true,調(diào)用的是cache.get(key, callable)方法 boolean sync() default false ; } |
@cacheevict:清除緩存,主要應(yīng)用到刪除數(shù)據(jù)的方法上。相比cacheable多了兩個(gè)屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package org.springframework.cache.annotation; 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; import org.springframework.core.annotation.aliasfor; @target ({elementtype.method, elementtype.type}) @retention (retentionpolicy.runtime) @inherited @documented public @interface cacheevict { // ...相同屬性說明請(qǐng)參考@cacheable中的說明 // 是否要清除所有緩存的數(shù)據(jù),為false時(shí)調(diào)用的是cache.evict(key)方法;為true時(shí)調(diào)用的是cache.clear()方法 boolean allentries() default false ; // 調(diào)用方法之前或之后清除緩存 boolean beforeinvocation() default false ; } |
- @cacheput:放入緩存,主要用到對(duì)數(shù)據(jù)有更新的方法上。屬性說明參考@cacheable
- @caching:用于在一個(gè)方法上配置多種注解
- @enablecaching:?jiǎn)⒂胹pring cache緩存,作為總的開關(guān),在spring boot的啟動(dòng)類或配置類上需要加上此注解才會(huì)生效
spring boot + spring cache
spring boot中已經(jīng)整合了spring cache,并且提供了多種緩存的配置,在使用時(shí)只需要配置使用哪個(gè)緩存(enum cachetype)即可。
spring boot中多增加了一個(gè)可以擴(kuò)展的東西,就是cachemanagercustomizer接口,可以自定義實(shí)現(xiàn)這個(gè)接口,然后對(duì)cachemanager做一些設(shè)置,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.itopener.demo.cache.redis.config; import java.util.map; import java.util.concurrent.concurrenthashmap; import org.springframework.boot.autoconfigure.cache.cachemanagercustomizer; import org.springframework.data.redis.cache.rediscachemanager; public class rediscachemanagercustomizer implements cachemanagercustomizer<rediscachemanager> { @override public void customize(rediscachemanager cachemanager) { // 默認(rèn)過期時(shí)間,單位秒 cachemanager.setdefaultexpiration( 1000 ); cachemanager.setuseprefix( false ); map<string, long > expires = new concurrenthashmap<string, long >(); expires.put( "useridcache" , 2000l); cachemanager.setexpires(expires); } } |
加載這個(gè)bean:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.itopener.demo.cache.redis.config; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; /** * @author fuwei.deng * @date 2017年12月22日 上午10:24:54 * @version 1.0.0 */ @configuration public class cacheredisconfiguration { @bean public rediscachemanagercustomizer rediscachemanagercustomizer() { return new rediscachemanagercustomizer(); } } |
常用的緩存就是redis了,redis對(duì)于spring cache接口的實(shí)現(xiàn)是在spring-data-redis包中
這里提下我認(rèn)為的rediscache實(shí)現(xiàn)中的缺陷:
1.在緩存失效的瞬間,如果有線程獲取緩存數(shù)據(jù),可能出現(xiàn)返回null的情況,原因是rediscache實(shí)現(xiàn)中是如下步驟:
- 判斷緩存key是否存在
- 如果key存在,再獲取緩存數(shù)據(jù),并返回
因此當(dāng)判斷key存在后緩存失效了,再去獲取緩存是沒有數(shù)據(jù)的,就返回null了。
2.rediscachemanager中是否允許存儲(chǔ)空值的屬性(cachenullvalues)默認(rèn)為false,即不允許存儲(chǔ)空值,這樣會(huì)存在緩存穿透的風(fēng)險(xiǎn)。缺陷是這個(gè)屬性是final類型的,只能在創(chuàng)建對(duì)象是通過構(gòu)造方法傳入,所以要避免緩存穿透就只能自己在應(yīng)用內(nèi)聲明rediscachemanager這個(gè)bean了
3.rediscachemanager中的屬性無(wú)法通過配置文件直接配置,只能在應(yīng)用內(nèi)實(shí)現(xiàn)cachemanagercustomizer接口來(lái)進(jìn)行設(shè)置,個(gè)人認(rèn)為不太方便
caffeine
caffeine是一個(gè)基于google開源的guava設(shè)計(jì)理念的一個(gè)高性能內(nèi)存緩存,使用java8開發(fā),spring boot引入caffeine后已經(jīng)逐步廢棄guava的整合了。caffeine源碼及介紹地址:caffeine
caffeine提供了多種緩存填充策略、值回收策略,同時(shí)也包含了緩存命中次數(shù)等統(tǒng)計(jì)數(shù)據(jù),對(duì)緩存的優(yōu)化能夠提供很大幫助
caffeine的介紹可以參考:
這里簡(jiǎn)單說下caffeine基于時(shí)間的回收策略有以下幾種:
- expireafteraccess:訪問后到期,從上次讀或?qū)懓l(fā)生后的過期時(shí)間
- expireafterwrite:寫入后到期,從上次寫入發(fā)生之后的過期時(shí)間
- 自定義策略:到期時(shí)間由實(shí)現(xiàn)expiry接口后單獨(dú)計(jì)算
spring boot + spring cache 實(shí)現(xiàn)兩級(jí)緩存(redis + caffeine)
本人開頭提到了,就算是使用了redis緩存,也會(huì)存在一定程度的網(wǎng)絡(luò)傳輸上的消耗,在實(shí)際應(yīng)用當(dāng)中,會(huì)存在一些變更頻率非常低的數(shù)據(jù),就可以直接緩存在應(yīng)用內(nèi)部,對(duì)于一些實(shí)時(shí)性要求不太高的數(shù)據(jù),也可以在應(yīng)用內(nèi)部緩存一定時(shí)間,減少對(duì)redis的訪問,提高響應(yīng)速度
由于spring-data-redis框架中redis對(duì)spring cache的實(shí)現(xiàn)有一些不足,在使用起來(lái)可能會(huì)出現(xiàn)一些問題,所以就不基于原來(lái)的實(shí)現(xiàn)去擴(kuò)展了,直接參考實(shí)現(xiàn)方式,去實(shí)現(xiàn)cache和cachemanager接口
還需要注意一點(diǎn),一般應(yīng)用都部署了多個(gè)節(jié)點(diǎn),一級(jí)緩存是在應(yīng)用內(nèi)的緩存,所以當(dāng)對(duì)數(shù)據(jù)更新和清除時(shí),需要通知所有節(jié)點(diǎn)進(jìn)行清理緩存的操作。可以有多種方式來(lái)實(shí)現(xiàn)這種效果,比如:zookeeper、mq等,但是既然用了redis緩存,redis本身是有支持訂閱/發(fā)布功能的,所以就不依賴其他組件了,直接使用redis的通道來(lái)通知其他節(jié)點(diǎn)進(jìn)行清理緩存的操作
以下就是對(duì)spring boot + spring cache實(shí)現(xiàn)兩級(jí)緩存(redis + caffeine)的starter封裝步驟和源碼
定義properties配置屬性類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure; import java.util.hashmap; import java.util.hashset; import java.util.map; import java.util.set; import org.springframework.boot.context.properties.configurationproperties; /** * @author fuwei.deng * @date 2018年1月29日 上午11:32:15 * @version 1.0.0 */ @configurationproperties (prefix = "spring.cache.multi" ) public class cacherediscaffeineproperties { private set<string> cachenames = new hashset<>(); /** 是否存儲(chǔ)空值,默認(rèn)true,防止緩存穿透*/ private boolean cachenullvalues = true ; /** 是否動(dòng)態(tài)根據(jù)cachename創(chuàng)建cache的實(shí)現(xiàn),默認(rèn)true*/ private boolean dynamic = true ; /** 緩存key的前綴*/ private string cacheprefix; private redis redis = new redis(); private caffeine caffeine = new caffeine(); public class redis { /** 全局過期時(shí)間,單位毫秒,默認(rèn)不過期*/ private long defaultexpiration = 0 ; /** 每個(gè)cachename的過期時(shí)間,單位毫秒,優(yōu)先級(jí)比defaultexpiration高*/ private map<string, long > expires = new hashmap<>(); /** 緩存更新時(shí)通知其他節(jié)點(diǎn)的topic名稱*/ private string topic = "cache:redis:caffeine:topic" ; public long getdefaultexpiration() { return defaultexpiration; } public void setdefaultexpiration( long defaultexpiration) { this .defaultexpiration = defaultexpiration; } public map<string, long > getexpires() { return expires; } public void setexpires(map<string, long > expires) { this .expires = expires; } public string gettopic() { return topic; } public void settopic(string topic) { this .topic = topic; } } public class caffeine { /** 訪問后過期時(shí)間,單位毫秒*/ private long expireafteraccess; /** 寫入后過期時(shí)間,單位毫秒*/ private long expireafterwrite; /** 寫入后刷新時(shí)間,單位毫秒*/ private long refreshafterwrite; /** 初始化大小*/ private int initialcapacity; /** 最大緩存對(duì)象個(gè)數(shù),超過此數(shù)量時(shí)之前放入的緩存將失效*/ private long maximumsize; /** 由于權(quán)重需要緩存對(duì)象來(lái)提供,對(duì)于使用spring cache這種場(chǎng)景不是很適合,所以暫不支持配置*/ // private long maximumweight; public long getexpireafteraccess() { return expireafteraccess; } public void setexpireafteraccess( long expireafteraccess) { this .expireafteraccess = expireafteraccess; } public long getexpireafterwrite() { return expireafterwrite; } public void setexpireafterwrite( long expireafterwrite) { this .expireafterwrite = expireafterwrite; } public long getrefreshafterwrite() { return refreshafterwrite; } public void setrefreshafterwrite( long refreshafterwrite) { this .refreshafterwrite = refreshafterwrite; } public int getinitialcapacity() { return initialcapacity; } public void setinitialcapacity( int initialcapacity) { this .initialcapacity = initialcapacity; } public long getmaximumsize() { return maximumsize; } public void setmaximumsize( long maximumsize) { this .maximumsize = maximumsize; } } public set<string> getcachenames() { return cachenames; } public void setcachenames(set<string> cachenames) { this .cachenames = cachenames; } public boolean iscachenullvalues() { return cachenullvalues; } public void setcachenullvalues( boolean cachenullvalues) { this .cachenullvalues = cachenullvalues; } public boolean isdynamic() { return dynamic; } public void setdynamic( boolean dynamic) { this .dynamic = dynamic; } public string getcacheprefix() { return cacheprefix; } public void setcacheprefix(string cacheprefix) { this .cacheprefix = cacheprefix; } public redis getredis() { return redis; } public void setredis(redis redis) { this .redis = redis; } public caffeine getcaffeine() { return caffeine; } public void setcaffeine(caffeine caffeine) { this .caffeine = caffeine; } } |
spring cache中有實(shí)現(xiàn)cache接口的一個(gè)抽象類abstractvalueadaptingcache,包含了空值的包裝和緩存值的包裝,所以就不用實(shí)現(xiàn)cache接口了,直接實(shí)現(xiàn)abstractvalueadaptingcache抽象類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
|
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; import java.lang.reflect.constructor; import java.util.map; import java.util.set; import java.util.concurrent.callable; import java.util.concurrent.timeunit; import java.util.concurrent.locks.reentrantlock; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.cache.support.abstractvalueadaptingcache; import org.springframework.data.redis.core.redistemplate; import org.springframework.util.stringutils; import com.github.benmanes.caffeine.cache.cache; import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.cacherediscaffeineproperties; /** * @author fuwei.deng * @date 2018年1月26日 下午5:24:11 * @version 1.0.0 */ public class rediscaffeinecache extends abstractvalueadaptingcache { private final logger logger = loggerfactory.getlogger(rediscaffeinecache. class ); private string name; private redistemplate<object, object> redistemplate; private cache<object, object> caffeinecache; private string cacheprefix; private long defaultexpiration = 0 ; private map<string, long > expires; private string topic = "cache:redis:caffeine:topic" ; protected rediscaffeinecache( boolean allownullvalues) { super (allownullvalues); } public rediscaffeinecache(string name, redistemplate<object, object> redistemplate, cache<object, object> caffeinecache, cacherediscaffeineproperties cacherediscaffeineproperties) { super (cacherediscaffeineproperties.iscachenullvalues()); this .name = name; this .redistemplate = redistemplate; this .caffeinecache = caffeinecache; this .cacheprefix = cacherediscaffeineproperties.getcacheprefix(); this .defaultexpiration = cacherediscaffeineproperties.getredis().getdefaultexpiration(); this .expires = cacherediscaffeineproperties.getredis().getexpires(); this .topic = cacherediscaffeineproperties.getredis().gettopic(); } @override public string getname() { return this .name; } @override public object getnativecache() { return this ; } @suppresswarnings ( "unchecked" ) @override public <t> t get(object key, callable<t> valueloader) { object value = lookup(key); if (value != null ) { return (t) value; } reentrantlock lock = new reentrantlock(); try { lock.lock(); value = lookup(key); if (value != null ) { return (t) value; } value = valueloader.call(); object storevalue = tostorevalue(valueloader.call()); put(key, storevalue); return (t) value; } catch (exception e) { try { class <?> c = class .forname( "org.springframework.cache.cache$valueretrievalexception" ); constructor<?> constructor = c.getconstructor(object. class , callable. class , throwable. class ); runtimeexception exception = (runtimeexception) constructor.newinstance(key, valueloader, e.getcause()); throw exception; } catch (exception e1) { throw new illegalstateexception(e1); } } finally { lock.unlock(); } } @override public void put(object key, object value) { if (! super .isallownullvalues() && value == null ) { this .evict(key); return ; } long expire = getexpire(); if (expire > 0 ) { redistemplate.opsforvalue().set(getkey(key), tostorevalue(value), expire, timeunit.milliseconds); } else { redistemplate.opsforvalue().set(getkey(key), tostorevalue(value)); } push( new cachemessage( this .name, key)); caffeinecache.put(key, value); } @override public valuewrapper putifabsent(object key, object value) { object cachekey = getkey(key); object prevvalue = null ; // 考慮使用分布式鎖,或者將redis的setifabsent改為原子性操作 synchronized (key) { prevvalue = redistemplate.opsforvalue().get(cachekey); if (prevvalue == null ) { long expire = getexpire(); if (expire > 0 ) { redistemplate.opsforvalue().set(getkey(key), tostorevalue(value), expire, timeunit.milliseconds); } else { redistemplate.opsforvalue().set(getkey(key), tostorevalue(value)); } push( new cachemessage( this .name, key)); caffeinecache.put(key, tostorevalue(value)); } } return tovaluewrapper(prevvalue); } @override public void evict(object key) { // 先清除redis中緩存數(shù)據(jù),然后清除caffeine中的緩存,避免短時(shí)間內(nèi)如果先清除caffeine緩存后其他請(qǐng)求會(huì)再?gòu)膔edis里加載到caffeine中 redistemplate.delete(getkey(key)); push( new cachemessage( this .name, key)); caffeinecache.invalidate(key); } @override public void clear() { // 先清除redis中緩存數(shù)據(jù),然后清除caffeine中的緩存,避免短時(shí)間內(nèi)如果先清除caffeine緩存后其他請(qǐng)求會(huì)再?gòu)膔edis里加載到caffeine中 set<object> keys = redistemplate.keys( this .name.concat( ":" )); for (object key : keys) { redistemplate.delete(key); } push( new cachemessage( this .name, null )); caffeinecache.invalidateall(); } @override protected object lookup(object key) { object cachekey = getkey(key); object value = caffeinecache.getifpresent(key); if (value != null ) { logger.debug( "get cache from caffeine, the key is : {}" , cachekey); return value; } value = redistemplate.opsforvalue().get(cachekey); if (value != null ) { logger.debug( "get cache from redis and put in caffeine, the key is : {}" , cachekey); caffeinecache.put(key, value); } return value; } private object getkey(object key) { return this .name.concat( ":" ).concat(stringutils.isempty(cacheprefix) ? key.tostring() : cacheprefix.concat( ":" ).concat(key.tostring())); } private long getexpire() { long expire = defaultexpiration; long cachenameexpire = expires.get( this .name); return cachenameexpire == null ? expire : cachenameexpire.longvalue(); } /** * @description 緩存變更時(shí)通知其他節(jié)點(diǎn)清理本地緩存 * @author fuwei.deng * @date 2018年1月31日 下午3:20:28 * @version 1.0.0 * @param message */ private void push(cachemessage message) { redistemplate.convertandsend(topic, message); } /** * @description 清理本地緩存 * @author fuwei.deng * @date 2018年1月31日 下午3:15:39 * @version 1.0.0 * @param key */ public void clearlocal(object key) { logger.debug( "clear local cache, the key is : {}" , key); if (key == null ) { caffeinecache.invalidateall(); } else { caffeinecache.invalidate(key); } } } |
實(shí)現(xiàn)cachemanager接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; import java.util.collection; import java.util.set; import java.util.concurrent.concurrenthashmap; import java.util.concurrent.concurrentmap; import java.util.concurrent.timeunit; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.cache.cache; import org.springframework.cache.cachemanager; import org.springframework.data.redis.core.redistemplate; import com.github.benmanes.caffeine.cache.caffeine; import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.cacherediscaffeineproperties; /** * @author fuwei.deng * @date 2018年1月26日 下午5:24:52 * @version 1.0.0 */ public class rediscaffeinecachemanager implements cachemanager { private final logger logger = loggerfactory.getlogger(rediscaffeinecachemanager. class ); private concurrentmap<string, cache> cachemap = new concurrenthashmap<string, cache>(); private cacherediscaffeineproperties cacherediscaffeineproperties; private redistemplate<object, object> redistemplate; private boolean dynamic = true ; private set<string> cachenames; public rediscaffeinecachemanager(cacherediscaffeineproperties cacherediscaffeineproperties, redistemplate<object, object> redistemplate) { super (); this .cacherediscaffeineproperties = cacherediscaffeineproperties; this .redistemplate = redistemplate; this .dynamic = cacherediscaffeineproperties.isdynamic(); this .cachenames = cacherediscaffeineproperties.getcachenames(); } @override public cache getcache(string name) { cache cache = cachemap.get(name); if (cache != null ) { return cache; } if (!dynamic && !cachenames.contains(name)) { return cache; } cache = new rediscaffeinecache(name, redistemplate, caffeinecache(), cacherediscaffeineproperties); cache oldcache = cachemap.putifabsent(name, cache); logger.debug( "create cache instance, the cache name is : {}" , name); return oldcache == null ? cache : oldcache; } public com.github.benmanes.caffeine.cache.cache<object, object> caffeinecache(){ caffeine<object, object> cachebuilder = caffeine.newbuilder(); if (cacherediscaffeineproperties.getcaffeine().getexpireafteraccess() > 0 ) { cachebuilder.expireafteraccess(cacherediscaffeineproperties.getcaffeine().getexpireafteraccess(), timeunit.milliseconds); } if (cacherediscaffeineproperties.getcaffeine().getexpireafterwrite() > 0 ) { cachebuilder.expireafterwrite(cacherediscaffeineproperties.getcaffeine().getexpireafterwrite(), timeunit.milliseconds); } if (cacherediscaffeineproperties.getcaffeine().getinitialcapacity() > 0 ) { cachebuilder.initialcapacity(cacherediscaffeineproperties.getcaffeine().getinitialcapacity()); } if (cacherediscaffeineproperties.getcaffeine().getmaximumsize() > 0 ) { cachebuilder.maximumsize(cacherediscaffeineproperties.getcaffeine().getmaximumsize()); } if (cacherediscaffeineproperties.getcaffeine().getrefreshafterwrite() > 0 ) { cachebuilder.refreshafterwrite(cacherediscaffeineproperties.getcaffeine().getrefreshafterwrite(), timeunit.milliseconds); } return cachebuilder.build(); } @override public collection<string> getcachenames() { return this .cachenames; } public void clearlocal(string cachename, object key) { cache cache = cachemap.get(cachename); if (cache == null ) { return ; } rediscaffeinecache rediscaffeinecache = (rediscaffeinecache) cache; rediscaffeinecache.clearlocal(key); } } |
redis消息發(fā)布/訂閱,傳輸?shù)南㈩?/p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; import java.io.serializable; /** * @author fuwei.deng * @date 2018年1月29日 下午1:31:17 * @version 1.0.0 */ public class cachemessage implements serializable { /** */ private static final long serialversionuid = 5987219310442078193l; private string cachename; private object key; public cachemessage(string cachename, object key) { super (); this .cachename = cachename; this .key = key; } public string getcachename() { return cachename; } public void setcachename(string cachename) { this .cachename = cachename; } public object getkey() { return key; } public void setkey(object key) { this .key = key; } } |
監(jiān)聽redis消息需要實(shí)現(xiàn)messagelistener接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.data.redis.connection.message; import org.springframework.data.redis.connection.messagelistener; import org.springframework.data.redis.core.redistemplate; /** * @author fuwei.deng * @date 2018年1月30日 下午5:22:33 * @version 1.0.0 */ public class cachemessagelistener implements messagelistener { private final logger logger = loggerfactory.getlogger(cachemessagelistener. class ); private redistemplate<object, object> redistemplate; private rediscaffeinecachemanager rediscaffeinecachemanager; public cachemessagelistener(redistemplate<object, object> redistemplate, rediscaffeinecachemanager rediscaffeinecachemanager) { super (); this .redistemplate = redistemplate; this .rediscaffeinecachemanager = rediscaffeinecachemanager; } @override public void onmessage(message message, byte [] pattern) { cachemessage cachemessage = (cachemessage) redistemplate.getvalueserializer().deserialize(message.getbody()); logger.debug( "recevice a redis topic message, clear local cache, the cachename is {}, the key is {}" , cachemessage.getcachename(), cachemessage.getkey()); rediscaffeinecachemanager.clearlocal(cachemessage.getcachename(), cachemessage.getkey()); } } |
增加spring boot配置類
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.autoconfigure.autoconfigureafter; import org.springframework.boot.autoconfigure.condition.conditionalonbean; import org.springframework.boot.autoconfigure.data.redis.redisautoconfiguration; import org.springframework.boot.context.properties.enableconfigurationproperties; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.data.redis.core.redistemplate; import org.springframework.data.redis.listener.channeltopic; import org.springframework.data.redis.listener.redismessagelistenercontainer; import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.cachemessagelistener; import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.rediscaffeinecachemanager; /** * @author fuwei.deng * @date 2018年1月26日 下午5:23:03 * @version 1.0.0 */ @configuration @autoconfigureafter (redisautoconfiguration. class ) @enableconfigurationproperties (cacherediscaffeineproperties. class ) public class cacherediscaffeineautoconfiguration { @autowired private cacherediscaffeineproperties cacherediscaffeineproperties; @bean @conditionalonbean (redistemplate. class ) public rediscaffeinecachemanager cachemanager(redistemplate<object, object> redistemplate) { return new rediscaffeinecachemanager(cacherediscaffeineproperties, redistemplate); } @bean public redismessagelistenercontainer redismessagelistenercontainer(redistemplate<object, object> redistemplate, rediscaffeinecachemanager rediscaffeinecachemanager) { redismessagelistenercontainer redismessagelistenercontainer = new redismessagelistenercontainer(); redismessagelistenercontainer.setconnectionfactory(redistemplate.getconnectionfactory()); cachemessagelistener cachemessagelistener = new cachemessagelistener(redistemplate, rediscaffeinecachemanager); redismessagelistenercontainer.addmessagelistener(cachemessagelistener, new channeltopic(cacherediscaffeineproperties.getredis().gettopic())); return redismessagelistenercontainer; } } |
在resources/meta-inf/spring.factories文件中增加spring boot配置掃描
1
2
3
|
# auto configure org.springframework.boot.autoconfigure.enableautoconfiguration=\ com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.cacherediscaffeineautoconfiguration |
接下來(lái)就可以使用maven引入使用了
1
2
3
4
5
6
|
<dependency> <groupid>com.itopener</groupid> <artifactid>cache-redis-caffeine-spring-boot-starter</artifactid> <version> 1.0 . 0 -snapshot</version> <type>pom</type> </dependency> |
在啟動(dòng)類上增加@enablecaching注解,在需要緩存的方法上增加@cacheable注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package com.itopener.demo.cache.redis.caffeine.service; import java.util.random; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.cache.annotation.cacheevict; import org.springframework.cache.annotation.cacheput; import org.springframework.cache.annotation.cacheable; import org.springframework.stereotype.service; import com.itopener.demo.cache.redis.caffeine.vo.uservo; import com.itopener.utils.timestamputil; @service public class cacherediscaffeineservice { private final logger logger = loggerfactory.getlogger(cacherediscaffeineservice. class ); @cacheable (key = "'cache_user_id_' + #id" , value = "useridcache" , cachemanager = "cachemanager" ) public uservo get( long id) { logger.info( "get by id from db" ); uservo user = new uservo(); user.setid(id); user.setname( "name" + id); user.setcreatetime(timestamputil.current()); return user; } @cacheable (key = "'cache_user_name_' + #name" , value = "usernamecache" , cachemanager = "cachemanager" ) public uservo get(string name) { logger.info( "get by name from db" ); uservo user = new uservo(); user.setid( new random().nextlong()); user.setname(name); user.setcreatetime(timestamputil.current()); return user; } @cacheput (key = "'cache_user_id_' + #uservo.id" , value = "useridcache" , cachemanager = "cachemanager" ) public uservo update(uservo uservo) { logger.info( "update to db" ); uservo.setcreatetime(timestamputil.current()); return uservo; } @cacheevict (key = "'cache_user_id_' + #id" , value = "useridcache" , cachemanager = "cachemanager" ) public void delete( long id) { logger.info( "delete from db" ); } } |
properties文件中redis的配置跟使用redis是一樣的,可以增加兩級(jí)緩存的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#兩級(jí)緩存的配置 spring.cache.multi.caffeine.expireafteraccess= 5000 spring.cache.multi.redis.defaultexpiration= 60000 #spring cache配置 spring.cache.cache-names=useridcache,usernamecache #redis配置 #spring.redis.timeout= 10000 #spring.redis.password=redispwd #redis pool #spring.redis.pool.maxidle= 10 #spring.redis.pool.minidle= 2 #spring.redis.pool.maxactive= 10 #spring.redis.pool.maxwait= 3000 #redis cluster spring.redis.cluster.nodes= 127.0 . 0.1 : 7001 , 127.0 . 0.1 : 7002 , 127.0 . 0.1 : 7003 , 127.0 . 0.1 : 7004 , 127.0 . 0.1 : 7005 , 127.0 . 0.1 : 7006 spring.redis.cluster.maxredirects= 3 |
擴(kuò)展
個(gè)人認(rèn)為redisson的封裝更方便一些
- 對(duì)于spring cache緩存的實(shí)現(xiàn)沒有那么多的缺陷
- 使用redis的hash結(jié)構(gòu),可以針對(duì)不同的hashkey設(shè)置過期時(shí)間,清理的時(shí)候會(huì)更方便
- 如果基于redisson來(lái)實(shí)現(xiàn)多級(jí)緩存,可以繼承redissoncache,在對(duì)應(yīng)方法增加一級(jí)緩存的操作即可
- 如果有使用分布式鎖的情況就更方便了,可以直接使用redisson中封裝的分布式鎖
- redisson中的發(fā)布訂閱封裝得更好用
后續(xù)可以增加對(duì)于緩存命中率的統(tǒng)計(jì)endpoint,這樣就可以更好的監(jiān)控各個(gè)緩存的命中情況,以便對(duì)緩存配置進(jìn)行優(yōu)化
starter目錄:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent
示例代碼目錄: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持服務(wù)器之家。
原文鏈接:https://my.oschina.net/dengfuwei/blog/1616221