一 什麼是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…