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

一 什麼是Cache

1 Cache

Cache一般意義上是指高速緩存,它與數據庫最大的區別是「更快」,可能會快上100倍,並且Cache是所有運行在內存中,而數據庫中的數據通常都是存在硬盤中,而IO一直都是網站等大規模系統的瓶頸,若是不使用Cache,徹底用數據庫,當訪問量過大時將致使數據丟失,更嚴重時會致使系統崩潰,特別是遇到惡意攻擊的狀況,因此緩存構成了網絡的第一道防線。前端

當用戶請求網絡資源時,會先訪問緩存中的數據,若是緩存中沒有,再去訪問數據庫,請求返回給用戶的同時,更新到緩存中。而因爲網絡請求的定律,80%的請求會集中在20%的數據上,因此緩存會極大的提升服務的響應能力。git

2 應用場景

針對數據庫的增、刪、查、改,數據庫緩存技術應用場景絕大部分針對的是「查」的場景。好比,一篇常常訪問的帖子/文章/新聞、熱門商品的描述信息、好友評論/留言等。由於在常見的應用中,數據庫層次的壓力有80%的是查詢,20%的纔是數據的變動操做。因此絕大部分的應用場景的仍是「查」緩存。固然,「增、刪、改」的場景也是有的。好比,一篇文章訪問的次數,不可能每訪問一次,咱們就去數據庫裏面加一次吧?這種時候,咱們通常「增」場景的緩存就必不可少。不然,一篇文章被訪問了十萬次,代碼層次不會還去作十萬次的數據庫操做吧。github

3 應用舉例

讀操做流程redis

有了數據庫和緩存兩個地方存放數據以後(uid->money),每當須要讀取相關數據時(money),操做流程通常是這樣的:spring

(1)讀取緩存中是否有相關數據,uid->money數據庫

(2)若是緩存中有相關數據money,則返回【這就是所謂的數據命中「hit」】json

(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...

相關文章
相關標籤/搜索