Cache一般意義上是指高速緩存,它與數據庫最大的區別是「更快」,可能會快上100倍,並且Cache是所有運行在內存中,而數據庫中的數據通常都是存在硬盤中,而IO一直都是網站等大規模系統的瓶頸,若是不使用Cache,徹底用數據庫,當訪問量過大時將致使數據丟失,更嚴重時會致使系統崩潰,特別是遇到惡意攻擊的狀況,因此緩存構成了網絡的第一道防線。前端
當用戶請求網絡資源時,會先訪問緩存中的數據,若是緩存中沒有,再去訪問數據庫,請求返回給用戶的同時,更新到緩存中。而因爲網絡請求的定律,80%的請求會集中在20%的數據上,因此緩存會極大的提升服務的響應能力。git
針對數據庫的增、刪、查、改,數據庫緩存技術應用場景絕大部分針對的是「查」的場景。好比,一篇常常訪問的帖子/文章/新聞、熱門商品的描述信息、好友評論/留言等。由於在常見的應用中,數據庫層次的壓力有80%的是查詢,20%的纔是數據的變動操做。因此絕大部分的應用場景的仍是「查」緩存。固然,「增、刪、改」的場景也是有的。好比,一篇文章訪問的次數,不可能每訪問一次,咱們就去數據庫裏面加一次吧?這種時候,咱們通常「增」場景的緩存就必不可少。不然,一篇文章被訪問了十萬次,代碼層次不會還去作十萬次的數據庫操做吧。github
讀操做流程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類型,這樣就解決的跨服務對象傳輸的問題
在redis中直接存儲爲String就能夠了,因此只要把字段挑出來,存儲爲HashMap就能夠了,因此將代碼優化下
Map jsonMap = new HashMap<String,Object>(); ...... jsonRedisTemplate.opsForValue().set(key, jsonMap);
分佈式存儲需考慮存入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); } }
}
領導又要求將redis的關鍵操做,好比說存的操做存到數據庫,所以要將一些操做記錄到數據庫,此時顯然不能直接存數據庫,形成額外的開銷,因此須要使用消息隊列
歡迎使用、拍磚:https://github.com/vvsuperman...