微服務spring-cloud中 redis cache annotation操作指北

方老司發表於2017-10-20

一 什麼是Cache

1 Cache

Cache通常意義上是指快取記憶體,它與資料庫最大的區別是“更快”,可能會快上100倍,而且Cache是全部執行在記憶體中,而資料庫中的資料一般都是存在硬碟中,而IO一直都是網站等大規模系統的瓶頸,如果不使用Cache,完全用資料庫,當訪問量過大時將導致資料丟失,更嚴重時會導致系統崩潰,特別是遇到惡意攻擊的情況,所以快取構成了網路的第一道防線。

當使用者請求網路資源時,會先訪問快取中的資料,如果快取中沒有,再去訪問資料庫,請求返回給使用者的同時,更新到快取中。而由於網路請求的定律,80%的請求會集中在20%的資料上,所以快取會極大的提高服務的響應能力。

2 應用場景

針對資料庫的增、刪、查、改,資料庫快取技術應用場景絕大部分針對的是“查”的場景。比如,一篇經常訪問的帖子/文章/新聞、熱門商品的描述資訊、好友評論/留言等。因為在常見的應用中,資料庫層次的壓力有80%的是查詢,20%的才是資料的變更操作。所以絕大部分的應用場景的還是“查”快取。當然,“增、刪、改”的場景也是有的。比如,一篇文章訪問的次數,不可能每訪問一次,我們就去資料庫裡面加一次吧?這種時候,我們一般“增”場景的快取就必不可少。否則,一篇文章被訪問了十萬次,程式碼層次不會還去做十萬次的資料庫操作吧。

3 應用舉例

讀操作流程

有了資料庫和快取兩個地方存放資料之後(uid->money),每當需要讀取相關資料時(money),操作流程一般是這樣的:

(1)讀取快取中是否有相關資料,uid->money

(2)如果快取中有相關資料money,則返回【這就是所謂的資料命中“hit”】

(3)如果快取中沒有相關資料money,則從資料庫讀取相關資料money【這就是所謂的資料未命中“miss”】,放入快取中uid->money,再返回

二 使用

spring中的annotation極大的方便了快取操作,加上annotation就能夠自動實現redis的讀取、更新等策略。

比如

@Cacheable(value="users")  
public User findByUsername(String username)  

此時,會首先在redis中查 users:username 中構成的鍵值,比如username:張三,那麼會到redis中查key=”users:張三”,如果redis中沒有,會到資料庫中去查,查好後返回前端,同時將資料更新到redis。

這是預設key的情況,當然,也可以手動加上key,比如

@Cacheable(value="users",key="#username")  
public User findByUsername(String username,String gender) 

此時,會按照key表示式的值,去引數裡面去取,這裡同樣,key=”users:張三”

此外,還有

@Cacheput(value="users",key="#username")  
public int insertUser(User user)  

會首先查資料,然後更新資料到redis中,key=”users:user.username”

三 改進

專案中存在這樣的問題,有的物件有30+欄位,但需要快取的只有3個,查如果全部都存到redis中,無疑將加大redis的負擔,能否指定欄位呢?其實可以專門定義一個檢視物件,裡面只存放需要的欄位,用來返回,但一來加大了工作量,導致程式碼膨脹,二來put還是沒法操作,所以我們寫了各自定義註解,用來指定redis中的儲存欄位。

使用方式如下

@RedisCacheAble(value="users",names={"name","gender","age"})  
public User findByUsername(String username)  

如此一來就能只儲存User物件中的name,gender,age屬性,其它屬性為null,減少了redis中物件的儲存大小。

同樣,還有cacheput

@RedisCachePut(value="users",key="#username",names={"name","gender","age"})  
public int insertUser(User user)  

四 實現原理

結合annotation以及aop實現
首先,定義annotataion

@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
@Documented  
public @interface RedisCacheAble {  
    String value() default "";   //key名稱、字首
    String[] names() default {};  //所需要包含的鍵值
    long timeout() default 30; //過期時間
    
}  

定義切面類,用於接受annotation的響應

@Component // 註冊到Spring容器,必須加入這個註解  
@Aspect // 該註解標示該類為切面類,切面是由通知和切點組成的。  
public class ApiAspect {  

    @Pointcut("@annotation(cn.com.spdbccc.hotelbank.rediscache.RedisCacheAble)")// 定義註解型別的切點,只要方法上有該註解,都會匹配  
    public void annotationAble(){        
    } 
    
   @Around("annotationAble()&& @annotation(rd)") //定義註解的具體實現,以及能夠接受註解物件,定義 @annotation(rd)就可以直接取到annotation的例項了
    public Object redisCacheAble(ProceedingJoinPoint joinPoint, RedisCacheAble rd) throws Throwable {
        String preKey = rd.value();
        String arg0 = joinPoint.getArgs()[0].toString();
        //TODO arg0判斷
        String key = preKey + ":" +arg0;
        //如果redis中已經有值,直接返回
        Object rtObject = redisTemplate.opsForValue().get(key);
        if (rtObject != null) {
            return rtObject;
        }

        // 執行函式,如果返回值為空,返回
        Object sourceObject = joinPoint.proceed();
        if (sourceObject == null) {
            return null;
        }

        // 根據values獲取object裡的值,並生成用於redis儲存的物件
        Class cl = sourceObject.getClass();


        // 插入資料庫成功
        // 如果values沒有值,那麼redis對應的value為輸入物件;否則根據輸入引數重新生成物件
        if (rd.names() == null) {
            // 存入目標物件
            redisTemplate.opsForValue().set(key, sourceObject,rd.timeout(),TimeUnit.MINUTES);
        } else {
            // 將目標物件特定欄位存入redis
            Object targetObject = cl.newInstance();
            for (String name : rd.names()) {
                try {
                    // 生成值到新的物件中
                    String upChar = name.substring(0, 1).toUpperCase();
                    String getterStr = "get" + upChar + name.substring(1);
                    Method getMethod = cl.getMethod(getterStr, new Class[] {});
                    Object objValue = getMethod.invoke(sourceObject, new Object[] {});

                    String setterStr = "set" + upChar + name.substring(1);
                    Method setMethod = cl.getMethod(setterStr, String.class);
                    setMethod.invoke(targetObject, objValue);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
            }

            // 存入目標物件,key=類名:keyvalue
            redisTemplate.opsForValue().set(key, targetObject,rd.timeout(),TimeUnit.MINUTES);
        }
        return sourceObject;

    }

五 專案規範化

我們使用了引數:

@RedisCachePut(value="users",key="#username",names={"name","gender","age"})

但在實際的使用中要求用常量來表示key字首,比如

public final static String PRE_USER="users"

字串固然是沒有問題,陣列貌似是沒有辦法用常量來定義的,PRE_USERS={“user1″,”user2”},此時會報編譯錯誤,解決方式就是直接使用String型別了,而後在具體的切面處理函式中再轉成字串。

六 分散式中使用

在分散式系統中redis中物件序列化、反序列化無法跨服務,即使對於同一個類名,在不同的服務中,是無法反序列化出來的,必須儲存為純String型別,所以新加了個轉換器

public class StringJackson2JsonSerializer<T> extends Jackson2JsonRedisSerializer<T> {      
    private ObjectMapper objectMapper = new ObjectMapper();   
    public StringJackson2JsonSerializer(Class<T> type) {
        super(type);
        // TODO Auto-generated constructor stub
    }       
    public byte[] serialize(Object t) throws SerializationException {

        if (t == null) {
            return  new byte[0];
        }
        try {
            //將物件轉為Json String然後再序列化,方便跨服務
            return this.objectMapper.writeValueAsBytes(JacksonUtil.objToJson(t));
        } catch (Exception ex) {
            throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex);
        }
    }

這個轉換器能夠將所有的key-value的value存為string型別,這樣就解決的跨服務物件傳輸的問題

七 優化

1 縮減儲存欄位

在redis中直接儲存為String就可以了,所以只要把欄位挑出來,儲存為HashMap就可以了,所以將程式碼優化下

Map jsonMap = new HashMap<String,Object>();
......
jsonRedisTemplate.opsForValue().set(key, jsonMap);

2 重定義RedisTemplate

分散式儲存需考慮存入json字串,而原生則不能,而有些情況必須使用底層的RedisTemplate,所以必須定義一個xxTemplate來專職處理該情況,包括hash,set等。
//用來專門處理需要以json字串存入redis中的redistemplate

@Bean
public RedisTemplate<String, Object> jsonRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    //序列化、反序列化,使用原始的json string儲存到redis,方便跨服務
    StringJackson2JsonSerializer<Object> jackson2JsonRedisSerializer = new StringJackson2JsonSerializer<Object>(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);
    redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
    redisTemplate.setKeySerializer(new StringRedisSerializer())
    
    redisTemplate.afterPropertiesSet();
    return redisTemplate;
}   

我們專門定義了一個StringJackson2JsonSerializer來處理redis的序列化,在序列化前將物件轉為string

public class StringJackson2JsonSerializer<T>   
               extends Jackson2JsonRedisSerializer<T> {

private ObjectMapper objectMapper = new ObjectMapper();

public StringJackson2JsonSerializer(Class<T> type) {
    super(type);
    // TODO Auto-generated constructor stub
}

public byte[] serialize(Object t) throws SerializationException {

    if (t == null) {
        return  new byte[0];
    }
    try {
        //將物件轉為Json String然後再序列化,方便跨服務
        return this.objectMapper.writeValueAsBytes(JacksonUtil.objToJson(t));
    } catch (Exception ex) {
        throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex);
    }
}

}

3 關鍵操作非同步入庫

領導又要求將redis的關鍵操作,比如說存的操作存到資料庫,因此要將一些操作記錄到資料庫,此時顯然不能直接存資料庫,造成額外的開銷,所以需要使用訊息佇列

八 Git地址

歡迎使用、拍磚:https://github.com/vvsuperman…

相關文章