Springboot中Spring-cache與redis整合
也是在整合redis的時候偶然間發現spring-cache的。這也是一個不錯的框架,與spring的事務使用類似,只要新增一些註解方法,就可以動態的去操作快取了,減少程式碼的操作。如果這些註解不滿足專案的需求,我們也可以參考spring-cache的實現思想,使用AOP代理+快取操作來管理快取的使用。
在這個例子中我使用的是redis,當然,因為spring-cache的存在,我們可以整合多樣的快取技術,例如Ecache、Mamercache等。
下面來看springcache的具體操作吧!
附上官方的文件: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html
redis中整合spring-cache
快取的配置如下
1、在RedisCacheConfig上添加註解
@EnableCaching 加上這個註解是的支援快取註解
2、建立RedisCacheManager
/** * 設定RedisCacheManager * 使用cache註解管理redis快取 * * @return */ @Bean public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate()); return redisCacheManager; }
3、自定義快取的key
/** * 自定義生成redis-key * * @return */ @Override public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object o, Method method, Object... objects) { StringBuilder sb = new StringBuilder(); sb.append(o.getClass().getName()).append("."); sb.append(method.getName()).append("."); for (Object obj : objects) { sb.append(obj.toString()); } System.out.println("keyGenerator=" + sb.toString()); return sb.toString(); } }; }
在RedisCacheConfig中新增以上的程式碼,就可以使用springcache的註解了。下面介紹springcache的註解如何使用
spring cache與redis快取結合
對springCache概念的瞭解
springCache支援透明的新增快取到應用程式,類似事務處理一般,不需要複雜的程式碼支援。
快取的主要使用方式包括以下兩方面
1. 快取的宣告,需要根據專案需求來妥善的應用快取
2. 快取的配置方式,選擇需要的快取支援,例如Ecache、redis、memercache等
快取的註解介紹
@Cacheable 觸發快取入口
@CacheEvict 觸發移除快取
@CacahePut 更新快取
@Caching 將多種快取操作分組
@CacheConfig 類級別的快取註解,允許共享快取名稱
@CacheConfig
該註解是可以將快取分類,它是類級別的註解方式。我們可以這麼使用它。
這樣的話,UseCacheRedisService的所有快取註解例如@Cacheable的value值就都為user。
@CacheConfig(cacheNames = "user")
@Service
public class UseCacheRedisService {}
在redis的快取中顯示如下
127.0.0.1:6379> keys *
1) "user~keys"
2) "user_1"
127.0.0.1:6379> get user~keys
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> type user~keys
zset
127.0.0.1:6379> zrange user~keys 0 10
1) "user_1"
我們注意到,生成的user~keys,它是一個zset型別的key,如果使用get會報WRONGTYPE Operation against a key holding the wrong kind of value。這個問題坑了我很久
@Cacheable
一般用於查詢操作,根據key查詢快取.
如果key不存在,查詢db,並將結果更新到快取中。
如果key存在,直接查詢快取中的資料。
查詢的例子,當第一查詢的時候,redis中不存在key,會從db中查詢資料,並將返回的結果插入到redis中。
@Cacheable
public List<User> selectAllUser(){
return userMapper.selectAll();
}
呼叫方式。
@Test
public void selectTest(){
System.out.println("===========第一次呼叫=======");
List<User> list = useCacheRedisService.selectAllUser();
System.out.println("===========第二次呼叫=======");
List<User> list2 = useCacheRedisService.selectAllUser();
for (User u : list2){
System.out.println("u = " + u);
}
}
列印結果,大家也可以試一試
只輸出一次sql查詢的語句,說明第二次查詢是從redis中查到的。
===========第一次呼叫=======
keyGenerator=com.lzl.redisService.UseCacheRedisService.selectAllUser.
keyGenerator=com.lzl.redisService.UseCacheRedisService.selectAllUser.
DEBUG [main] - ==> Preparing: SELECT id,name,sex,age,password,account FROM user
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 1
===========第二次呼叫=======
keyGenerator=com.lzl.redisService.UseCacheRedisService.selectAllUser.
u = User{id=1, name='fsdfds', sex='fdsfg', age=24, password='gfdsg', account='gfds'}
redis中的結果
我們可以看到redis中已經存在
com.lzl.redisService.UseCacheRedisService.selectAllUser.記錄了。
這麼長的一串字元key是根據自定義key值生成的。
127.0.0.1:6379> keys *
1) "user~keys"
2) "com.lzl.redisService.UseCacheRedisService.selectAllUser."
3) "user_1"
127.0.0.1:6379> get com.lzl.redisService.UseCacheRedisService.selectAllUser.
"[\"java.util.ArrayList\",[[\"com.lzl.bean.User\",{\"id\":1,\"name\":\"fsdfds\",\"sex\":\"fdsfg\",\"age\":24,\"password\":\"gfdsg\",\"account\":\"gfds\"}]]]"
@CachePut
一般用於更新和插入操作,每次都會請求db
通過key去redis中進行操作。
1. 如果key存在,更新內容
2. 如果key不存在,插入內容。
/**
* 單個user物件的插入操作,使用user+id
* @param user
* @return
*/
@CachePut(key = "\"user_\" + #user.id")
public User saveUser(User user){
userMapper.insert(user);
return user;
}
redis中的結果
多了一條記錄user_2
127.0.0.1:6379> keys *
1) "user~keys"
2) "user_2"
3) "com.lzl.redisService.UseCacheRedisService.selectAllUser."
4) "user_1"
127.0.0.1:6379> get user_2
"[\"com.lzl.bean.User\",{\"id\":2,\"name\":\"fsdfds\",\"sex\":\"fdsfg\",\"age\":24,\"password\":\"gfdsg\",\"account\":\"gfds\"}]"
@CacheEvict
根據key刪除快取中的資料。allEntries=true表示刪除快取中的所有資料。
@CacheEvict(key = "\"user_\" + #id")
public void deleteById(Integer id){
userMapper.deleteByPrimaryKey(id);
}
測試方法
@Test
public void deleteTest(){
useCacheRedisService.deleteById(1);
}
redis中的結果
user_1已經移除掉。
127.0.0.1:6379> keys *
1) "user~keys"
2) "user_2"
3) "com.lzl.redisService.UseCacheRedisService.selectAllUser."
測試allEntries=true時的情形。
@Test
public void deleteAllTest(){
useCacheRedisService.deleteAll();
}
@CacheEvict(allEntries = true)
public void deleteAll(){
userMapper.deleteAll();
}
redis中的結果
redis中的資料已經全部清空
127.0.0.1:6379> keys *
(empty list or set)
@Caching
通過註解的屬性值可以看出來,這個註解將其他註解方式融合在一起了,我們可以根據需求來自定義註解,並將前面三個註解應用在一起
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
使用例子如下
@Caching(
put = {
@CachePut(value = "user", key = "\"user_\" + #user.id"),
@CachePut(value = "user", key = "#user.name"),
@CachePut(value = "user", key = "#user.account")
}
)
public User saveUserByCaching(User user){
userMapper.insert(user);
return user;
}
@Test
public void saveUserByCachingTest(){
User user = new User();
user.setName("dkjd");
user.setAccount("dsjkf");
useCacheRedisService.saveUserByCaching(user);
}
redis中的執行結果
一次新增三個key
127.0.0.1:6379> keys *
1) "user~keys"
2) "dsjkf"
3) "dkjd"
4) "user_3"
結合@Caching還可以設定自定義的註解
自定義註解
@Caching(
put = {
@CachePut(value = "user", key = "\"user_\" + #user.id"),
@CachePut(value = "user", key = "#user.name"),
@CachePut(value = "user", key = "#user.account")
}
)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SaveUserInfo {
}
使用如下
@SaveUserInfo
public User saveUserByInfo(User user){
userMapper.insert(user);
return user;
}
測試
@Test
public void saveUserByInfoTest(){
User user = new User();
user.setName("haha");
user.setAccount("hhhcc");
useCacheRedisService.saveUserByInfo(user);
}
redis結果
127.0.0.1:6379> keys *
1) "user_4"
2) "dsjkf"
3) "dkjd"
4) "user~keys"
5) "haha"
6) "hhhcc"
7) "user_3"
通過以上的例子基本可以瞭解springcache的使用了,當然還有更加複雜的操作,這裡只是簡單的介紹一下,運用到實際的專案中還是有所欠缺的。不過有這個基礎應該不會太難。同時有時間可以再研究一下spring-cache的實現原理。是基於AOP的實現的,這也是我們在專案中學習的地方。
問題
WRONGTYPE Operation against a key holding the wrong kind of value
這個就是上面所說的型別不一致,使用redis命令不當造成的。所以在查詢redis的value時候,需要知道key的型別。
type key
Invalid argument(s)
還是redis現實的錯誤,這個有些困惑,在get的時候,一定要加上”“(引號)才行。
127.0.0.1:6379> keys *
1) "user_4"
2) "com.lzl.redisService.UseCacheRedisService.saveUserByErr.User{id=5, name='fsdsg', sex='vcxvx', age=24, password='vcxvcxc', account='vxcvxc'}"
3) "dsjkf"
4) "dkjd"
5) "user~keys"
6) "haha"
7) "hhhcc"
8) "user_3"
127.0.0.1:6379> get com.lzl.redisService.UseCacheRedisService.saveUserByErr.User{id=5, name='fsdsg', sex='vcxvx', age=24, password='vcxvcxc', account='vxcvxc'}
Invalid argument(s)
127.0.0.1:6379> type com.lzl.redisService.UseCacheRedisService.saveUserByErr.User{id=5, name='fsdsg', sex='vcxvx', age=24, password='vcxvcxc', account='vxcvxc'}
Invalid argument(s)
127.0.0.1:6379> get "com.lzl.redisService.UseCacheRedisService.saveUserByErr.User{id=5, name='fsdsg', sex='vcxvx', age=24, password='vcxvcxc', account='vxcvxc'}"
"[\"com.lzl.bean.User\",{\"id\":5,\"name\":\"fsdsg\",\"sex\":\"vcxvx\",\"age\":24,\"password\":\"vcxvcxc\",\"account\":\"vxcvxc\"}]"
spring boot從redis取快取發生java.lang.ClassCastException異常
解決方法將devtools熱部署註釋掉。
仔細看你會發現這個類轉換異常很奇怪,為什麼呢?我們注意到這兩個User不管是包名還是類名是完全一樣的[ java.lang.ClassCastException: com.winds.admin.core.model.system.User cannot be cast tocom.winds.admin.core.model.system.User ],但也發生了強制轉換異常,那麼還有什麼原因會引起這種情況呢?那就只有一種情況了:使用的類載入器不一樣。主要原因是pom檔案中引入了DevTools配置。 當你使用DevTools進行快取時,需要了解這一限制。 當物件序列化到快取中時,應用程式類載入器是C1。然後,更改一些程式碼或者配置後,devtools會自動重新啟動上下文並建立一個新的類載入器C2。所以當你通過redis操作獲取快取反序列化的時候應用的類載入器是C2,雖然包名及其來類名完全一致,但是序列化與反序列化是通過不同的類載入器載入則在JVM中它們也不是同一個類。如果快取庫沒有考慮上下文類載入器,那麼這個物件會附加錯誤的類載入器 ,也就是我們常見的類強制轉換異常(ClassCastException)。
java.lang.IllegalArgumentException: Null key returned for cache operation
@Cacheable key 中值為null 報得錯誤 儘量