1. 程式人生 > >Springboot中Spring-cache與redis整合

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.ClassCastExceptioncom.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 報得錯誤 儘量