前言
項目使用redis作為緩存數據,但面臨著問題,比如,項目a,項目b都用到redis,而且用的redis都是一套集群,這樣會帶來一些問題。
問題:比如項目a的開發人員,要緩存一些熱門數據,想到了redis,于是乎把數據放入到了redis,自定義一個緩存key:hot_data_key,數據格式是項目a自己的數據格式,項目b也遇到了同樣的問題,也要緩存熱門數據,也是hot_data_key,數據格式是項目b是自己的數據格式,由于用的都是同一套redis集群,這樣key就是同一個key,有的數據格式適合項目a,有的數據格式適合項目b,會報錯的,我們項目中就遇到這樣的一個錯誤,找不到原因,結果就是兩個平臺用到了同一key,很懊惱。
解決方式:
1、弄一個常量類工程,所有的redis的key都放入到這個工程里,加上各自的平臺標識,這樣就不錯錯了
2、spring aop結合redis,再相應的service層,加上注解,key的規范是包名+key名,這樣就不錯重復了
思路:
1、自定義注解,加在需要緩存數據的地方
2、spring aop 結合redis實現
3、spel解析注解參數,用來得到響應的注解信息
4、redis的key:包名+key 防止redis的key重復
實現如下:
項目準備,由于是maven項目,需要引入相關的包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
fastjson包 <dependency> <groupid>com.alibaba</groupid> <artifactid>fastjson</artifactid> <version>${com.alibaba.fastjson}</version> </dependency> spring-data-redis <dependency> <groupid>org.springframework.data</groupid> <artifactid>spring-data-redis</artifactid> <version>${spring.redis.version}</version> </dependency> <dependency> <groupid>redis.clients</groupid> <artifactid>jedis</artifactid> <version>${jedis.redis.clients.version}</version> </dependency> |
還有一些必備的就是spring工程相關的包
1、自定義注解
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
|
import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; import java.util.concurrent.timeunit; /** * 緩存注解 * * @author shangdc * */ @target ({elementtype.method}) @retention (retentionpolicy.runtime) public @interface rediscache { /** * 緩存key的名稱 * @return */ string key(); /** * key是否轉換成md5值,有的key是整個參數對象,有的大內容的,比如一個大文本,導致redis的key很長 * 需要轉換成md5值作為redis的key * @return */ boolean keytransformmd5() default false ; /** * key 過期日期 秒 * @return */ int expiretime() default 60 ; /** * 時間單位,默認為秒 * @return */ timeunit dateunit() default timeunit.seconds; } |
2、定義切點pointcut
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
|
/** * redis 緩存切面 * * @author shangdc * */ public class rediscacheaspect { //由于每個人的環境,日志用的不一樣,怕報錯,就直接關掉了此日志輸出,如有需要可加上 //private logger log = logger.getlogger(rediscacheaspect.class); /** * 這塊可配置,每個公司都要自己的緩存配置方式,到時候可配置自己公司所用的緩存框架和配置方式 */ @resource (name = "redistemplate" ) private valueoperations<string, string> valueoperations; /** * 具體的方法 * @param jp * @return * @throws throwable */ public object cache(proceedingjoinpoint jp, rediscache cacheable) throws throwable{ // result是方法的最終返回結果 object result = null ; // 得到類名、方法名和參數 object[] args = jp.getargs(); //獲取實現類的方法 method method = getmethod(jp); //注解信息 key string key = cacheable.key(); //是否轉換成md5值 boolean keytransformmd5 = cacheable.keytransformmd5(); //---------------------------------------------------------- // 用spel解釋key值 //---------------------------------------------------------- //解析el表達式后的的redis的值 string keyval = springexpressionutils.parsekey(key, method, jp.getargs(), keytransformmd5); // 獲取目標對象 object target = jp.gettarget(); //這塊是全路徑包名+目標對象名 ,默認的前綴,防止有的開發人員亂使用key,亂定義key的名稱,導致重復key,這樣在這加上前綴了,就不會重復使用key string target_class_name = target.getclass().getname(); stringbuilder redis_key = new stringbuilder(target_class_name); redis_key.append(keyval); //最終的redis的key string redis_final_key = redis_key.tostring(); string value = valueoperations.get(redis_final_key); if (value == null ) { //這塊是判空 // 緩存未命中,這塊沒用log輸出,可以自定義輸出 system.out.println(redis_final_key + "緩存未命中緩存" ); // 如果redis沒有數據則執行攔截的方法體 result = jp.proceed(args); //存入json格式字符串到redis里 string result_json_data = jsonobject.tojsonstring(result); system.out.println(result_json_data); // 序列化結果放入緩存 valueoperations.set(redis_final_key, result_json_data, getexpiretimeseconds(cacheable), timeunit.seconds); } else { // 緩存命中,這塊沒用log輸出,可以自定義輸出 system.out.println(redis_final_key + "命中緩存,得到數據" ); // 得到被代理方法的返回值類型 class <?> returntype = ((methodsignature) jp.getsignature()).getreturntype(); //拿到數據格式 result = getdata(value, returntype); } return result; } /** * 根據不同的class返回數據 * @param value * @param clazz * @return */ public <t> t getdata(string value, class <t> clazz){ t result = jsonobject.parseobject(value, clazz); return result; } /** * 獲取方法 * @param pjp * @return * @throws nosuchmethodexception */ public static method getmethod(proceedingjoinpoint pjp) throws nosuchmethodexception { // -------------------------------------------------------------------------- // 獲取參數的類型 // -------------------------------------------------------------------------- object[] args = pjp.getargs(); class [] argtypes = new class [pjp.getargs().length]; for ( int i = 0 ; i < args.length; i++) { argtypes[i] = args[i].getclass(); } string methodname = pjp.getsignature().getname(); class <?> targetclass = pjp.gettarget().getclass(); method[] methods = targetclass.getmethods(); // -------------------------------------------------------------------------- // 查找class<?>里函數名稱、參數數量、參數類型(相同或子類)都和攔截的method相同的method // -------------------------------------------------------------------------- method method = null ; for ( int i = 0 ; i < methods.length; i++) { if (methods[i].getname() == methodname) { class <?>[] parametertypes = methods[i].getparametertypes(); boolean issamemethod = true ; // 如果相比較的兩個method的參數長度不一樣,則結束本次循環,與下一個method比較 if (args.length != parametertypes.length) { continue ; } // -------------------------------------------------------------------------- // 比較兩個method的每個參數,是不是同一類型或者傳入對象的類型是形參的子類 // -------------------------------------------------------------------------- for ( int j = 0 ; parametertypes != null && j < parametertypes.length; j++) { if (parametertypes[j] != argtypes[j] && !parametertypes[j].isassignablefrom(argtypes[j])) { issamemethod = false ; break ; } } if (issamemethod) { method = methods[i]; break ; } } } return method; } /** * 計算根據cacheable注解的expire和dateunit計算要緩存的秒數 * @param cacheable * @return */ public int getexpiretimeseconds(rediscache rediscache) { int expire = rediscache.expiretime(); timeunit unit = rediscache.dateunit(); if (expire <= 0 ) { //傳入非法值,默認一分鐘,60秒 return 60 ; } if (unit == timeunit.minutes) { return expire * 60 ; } else if (unit == timeunit.hours) { return expire * 60 * 60 ; } else if (unit == timeunit.days) { return expire * 60 * 60 * 24 ; } else { //什么都不是,默認一分鐘,60秒 return 60 ; } } } |
3、spring相關配置
由于是公司的項目,所有包就的路徑就去掉了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<!-- aop配置,切面類 .rediscacheaspect類 bean--> <bean id= "rediscacheaspect" class = "包.rediscacheaspect" > </bean> <!-- 攔截所有指定 包和指定類型下的 下所有的方法 ,你是想這個在哪些包下可以實現--> <aop:config proxy-target- class = "true" > <aop:aspect ref= "rediscacheaspect" > <aop:pointcut id= "rediscacheaoppointcut" expression= "(execution(* 包.business.web.service.*.*(..)) and @annotation(cacheable))" /> <!-- 環繞 ,命中緩存則直接放回緩存數據,不會往下走,未命中直接放行,直接執行對應的方法--> <aop:around pointcut-ref= "rediscacheaoppointcut" method= "cache" /> </aop:aspect> </aop:config> |
4、工具類 spel
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
|
/** * spring el表達式 * * @author shangdc * */ public class springexpressionutils { /** * 獲取緩存的key key 定義在注解上,支持spel表達式 注: method的參數支持javabean和map * method的基本類型要定義為對象,否則沒法讀取到名稱 * * example1: phone phone = new phone(); "#{phone.cpu}" 為對象的取值 、 * example2: map apple = new hashmap(); apple.put("name","good apple"); "#{apple[name]}" 為map的取值 * example3: "#{phone.cpu}_#{apple[name]}" * * * @param key * @param method * @param args * @return */ public static string parsekey(string key, method method, object[] args, boolean keytransformmd5) { // 獲取被攔截方法參數名列表(使用spring支持類庫) localvariabletableparameternamediscoverer u = new localvariabletableparameternamediscoverer(); string[] paranamearr = u.getparameternames(method); // 使用spel進行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]); } parsercontext parsercontext = new templateparsercontext(); // ---------------------------------------------------------- // 把 #{ 替換成 #{# ,以適配spel模板的格式 // ---------------------------------------------------------- //例如,@注解名稱(key="#{player.username}",expire = 200) //#{phone[cpu]}_#{phone[ram]} //#{player.username}_#{phone[cpu]}_#{phone[ram]}_#{pageno}_#{pagesize} object returnval = parser.parseexpression(key.replace( "#{" , "#{#" ), parsercontext).getvalue(context, object. class ); //這塊這么做,是為了object和string都可以轉成string類型的,可以作為key string return_data_key = jsonobject.tojsonstring(returnval); //轉換成md5,是因為redis的key過長,并且這種大key的數量過多,就會占用內存,影響性能 if (keytransformmd5) { return_data_key = md5util.digest(return_data_key); } return returnval == null ? null : return_data_key; } } |
5、redis相關配置
重要的是自己的redis配置,可能跟我的不太一樣,用自己的就好
1
2
3
4
|
<!-- redistemplate defination --> <bean id= "redistemplate" class = "org.springframework.data.redis.core.stringredistemplate" > <property name= "connectionfactory" ref= "jedisconnectionfactory" /> </bean> |
測試
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class rediscachetest { @test public void test() { applicationcontext context = new classpathxmlapplicationcontext( "classpath:spring/spring-applicationcontext.xml" ); rediscacheaopservice rediscacheaopservice = (rediscacheaopservice) context.getbean( "rediscacheaopservice" ); api api = rediscacheaopservice.getapi(1l); system.out.println(api.getclass()); system.out.println(jsonobject.tojsonstring(api)); apiparam param = new apiparam(); param.setid(2l); param.setapiname( "短信服務接口數據" ); // system.out.println("tostring:" + param.tostring()); // system.out.println(md5util.digest(param.tostring())); api api_1 = rediscacheaopservice.getapibyparam(param); system.out.println(api_1.getclass()); system.out.println(jsonobject.tojsonstring(api_1)); } } |
測試打印信息:
大體思路是這樣,需要自己動手實踐,不要什么都是拿過來直接copy,使用,整個過程都不操作,也不知道具體的地方,該用什么,自己實際操作,可以得到很多信息。
輔助信息類:
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
|
public class api implements serializable { private static final long serialversionuid = 1l; /** * * 自增主鍵id */ private long id; /** * * api名稱 */ private string apiname; /** * * api描述 */ private string apidescription; /** * * 有效時間 */ private integer valid; /** * 處理類 */ private string handlerclass; /** * * */ private date updatetime; /** * * */ private date createtime; public api() { } public string tostring() { return "id:" + id + ", apiname:" + apiname + ", apidescription:" + apidescription + ", valid:" + valid + ", updatetime:" + updatetime + ", createtime:" + createtime; } public long getid() { return this .id; } public void setid( long id) { this .id = id; } public string getapiname() { return this .apiname; } public void setapiname(string apiname) { this .apiname = apiname; } public string getapidescription() { return this .apidescription; } public void setapidescription(string apidescription) { this .apidescription = apidescription; } public integer getvalid() { return this .valid; } public void setvalid(integer valid) { this .valid = valid; } public date getupdatetime() { return this .updatetime; } public void setupdatetime(date updatetime) { this .updatetime = updatetime; } public date getcreatetime() { return this .createtime; } public void setcreatetime(date createtime) { this .createtime = createtime; } public string gethandlerclass() { return handlerclass; } public void sethandlerclass(string handlerclass) { this .handlerclass = handlerclass; } } |
參數類信息
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
|
public class apiparam { /** * */ private static final long serialversionuid = 1l; /** * api表主鍵id */ private long id; /** * * api名稱 */ private string apiname; /** * * 有效or無效 */ private integer valid; public string getapiname() { return apiname; } public void setapiname(string apiname) { this .apiname = apiname; } public integer getvalid() { return valid; } public void setvalid(integer valid) { this .valid = valid; } public long getid() { return id; } public void setid( long id) { this .id = id; } /*@override public string tostring() { return "apiparam [id=" + id + ", apiname=" + apiname + ", valid=" + valid + "]"; } */ } |
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:http://blog.51cto.com/shangdc/2153324