咱們但願可以將數據庫查詢結果緩存到Redis中,這樣在第二次作一樣的查詢時即可以直接從redis取結果,從而減小數據庫讀寫次數。redis
Key
),能惟一肯定一個查詢結果,同一個查詢結果,必定能映射到同一個key
。只有這樣才能保證緩存內容的正確性List
。咱們緩存了查詢結果,那麼一旦數據庫中的數據發生變化,緩存的結果就不可用了。爲了實現這一保證,能夠在執行相關表的更新查詢(update
, delete
, insert
)查詢前,讓相關的緩存過時。這樣下一次查詢時程序就會從新從數據庫中讀取新數據緩存到redis中。那麼問題來了,在執行一條insert
前我怎麼知道應該讓哪些緩存過時呢?對於Redis,咱們可使用Hash Set
數據結構,讓一張表對應一個Hash Set
,全部在這張表上的查詢都保存到該Set下。這樣當表數據發生變更時,直接讓Set過時便可。咱們能夠自定義一個註解,在數據庫查詢方法上經過註解的屬性註明這個操做與哪些表相關,這樣在執行過時操做時,就能直接從註解中得知應該讓哪些Set過時了。spring
對於MyBatis
,咱們能夠直接使用SQL
字符串作爲key
。可是這樣就必須編寫基於MyBatis
的攔截器,從而使你的緩存代碼與MyBatis
牢牢耦合在一塊兒。若是哪天更換了持久層的框架,你的緩存代碼就白寫了,因此這個方案並不完美。
仔細想想,其實若是兩次查詢調用的類名、方法名和參數值相同,咱們就能夠肯定這兩次查詢結果必定是相同的(在數據沒有變更的前提下)。所以,咱們能夠將這三個元素組合成一個字符串作爲key, 就解決了標識問題。數據庫
最方便的序列化方式就是使用JDK自帶的ObjectOutputStream
和ObjectInputStream
。優勢是幾乎任何一個對象,只要實現了Serializable
接口,都用同一套代碼能被序列化和反序列化。但缺點也很致命,那就是序列化的結果容量偏大,在redis中會消耗大量內存(是對應JSON格式的3倍左右)。那麼咱們只剩下JSON這一個選擇了。
JSON的優勢是結構緊湊,可讀性強,但美中不足的是,反序列化對象時必須提供具體的類型參數(Class
對象),若是是List
對象,還必須提供List
和List中的元素類型兩種信息,才能被正確反序列化。這樣就增長了代碼的複雜度。不過這些困難都是能夠克服的,因此咱們仍是選擇JSON做爲序列化存儲方式。json
毫無疑問,該AOP
上場了。在咱們的例子中,持久化框架使用的是MyBatis
,所以咱們的任務就是攔截Mapper
接口方法的調用,經過Around(環繞通知)
編寫如下邏輯:緩存
Key
Key
向Redis
發起查詢Key
將序列化結果放入redis中。由於咱們要攔截的是Mapper
接口方法,所以必須命令spring使用JDK的動態代理而不是cglib
的代理。爲此,咱們須要作如下配置:安全
<!-- 當proxy-target-class爲false時使用JDK動態代理 --> <!-- 爲true時使用cglib --> <!-- cglib沒法攔截接口方法 --> <aop:aspectj-autoproxy proxy-target-class="false" />
而後定義兩個標註在接口方法上的註解,用於傳遞類型參數:數據結構
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface RedisCache { Class type(); }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RedisEvict { Class type(); }
註解的使用方式以下:app
// 表示該方法須要執行 (緩存是否命中 ? 返回緩存並阻止方法調用 : 執行方法並緩存結果)的緩存邏輯 @RedisCache(type = JobPostModel.class) JobPostModel selectByPrimaryKey(Integer id);
// 表示該方法須要執行清除緩存邏輯 @RedisEvict(type = JobPostModel.class) int deleteByPrimaryKey(Integer id);
AOP的代碼以下:框架
@Aspect @Component public class RedisCacheAspect { public static final Logger infoLog = LogUtils.getInfoLogger(); @Qualifier("redisTemplateForString") @Autowired StringRedisTemplate rt; /** * 方法調用前,先查詢緩存。若是存在緩存,則返回緩存數據,阻止方法調用; * 若是沒有緩存,則調用業務方法,而後將結果放到緩存中 * @param jp * @return * @throws Throwable */ @Around("execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.select*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.get*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.find*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.search*(..))") public Object cache(ProceedingJoinPoint jp) throws Throwable { // 獲得類名、方法名和參數 String clazzName = jp.getTarget().getClass().getName(); String methodName = jp.getSignature().getName(); Object[] args = jp.getArgs(); // 根據類名,方法名和參數生成key String key = genKey(clazzName, methodName, args); if (infoLog.isDebugEnabled()) { infoLog.debug("生成key:{}", key); } // 獲得被代理的方法 Method me = ((MethodSignature) jp.getSignature()).getMethod(); // 獲得被代理的方法上的註解 Class modelType = me.getAnnotation(RedisCache.class).type(); // 檢查redis中是否有緩存 String value = (String)rt.opsForHash().get(modelType.getName(), key); // result是方法的最終返回結果 Object result = null; if (null == value) { // 緩存未命中 if (infoLog.isDebugEnabled()) { infoLog.debug("緩存未命中"); } // 調用數據庫查詢方法 result = jp.proceed(args); // 序列化查詢結果 String json = serialize(result); // 序列化結果放入緩存 rt.opsForHash().put(modelType.getName(), key, json); } else { // 緩存命中 if (infoLog.isDebugEnabled()) { infoLog.debug("緩存命中, value = {}", value); } // 獲得被代理方法的返回值類型 Class returnType = ((MethodSignature) jp.getSignature()).getReturnType(); // 反序列化從緩存中拿到的json result = deserialize(value, returnType, modelType); if (infoLog.isDebugEnabled()) { infoLog.debug("反序列化結果 = {}", result); } } return result; } /** * 在方法調用前清除緩存,而後調用業務方法 * @param jp * @return * @throws Throwable */ @Around("execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.insert*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.update*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.delete*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.increase*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.decrease*(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.complaint(..))" + "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.set*(..))") public Object evictCache(ProceedingJoinPoint jp) throws Throwable { // 獲得被代理的方法 Method me = ((MethodSignature) jp.getSignature()).getMethod(); // 獲得被代理的方法上的註解 Class modelType = me.getAnnotation(RedisEvict.class).type(); if (infoLog.isDebugEnabled()) { infoLog.debug("清空緩存:{}", modelType.getName()); } // 清除對應緩存 rt.delete(modelType.getName()); return jp.proceed(jp.getArgs()); } /** * 根據類名、方法名和參數生成key * @param clazzName * @param methodName * @param args 方法參數 * @return */ protected String genKey(String clazzName, String methodName, Object[] args) { StringBuilder sb = new StringBuilder(clazzName); sb.append(Constants.DELIMITER); sb.append(methodName); sb.append(Constants.DELIMITER); for (Object obj : args) { sb.append(obj.toString()); sb.append(Constants.DELIMITER); } return sb.toString(); } protected String serialize(Object target) { return JSON.toJSONString(target); } protected Object deserialize(String jsonString, Class clazz, Class modelType) { // 序列化結果應該是List對象 if (clazz.isAssignableFrom(List.class)) { return JSON.parseArray(jsonString, modelType); } // 序列化結果是普通對象 return JSON.parseObject(jsonString, clazz); } }
這樣咱們就完成了數據庫查詢緩存的實現。ide
UPDATE:
最好爲Hash Set設置一個過時時間,這樣即便緩存策略有誤(致使讀出髒數據),過時時間到了之後依然能夠與數據庫保持同步:
// 序列化結果放入緩存 rt.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection redisConn) throws DataAccessException { // 配置文件中指定了這是一個String類型的鏈接 // 因此這裏向下強制轉換必定是安全的 StringRedisConnection conn = (StringRedisConnection) redisConn; // 判斷hash名是否存在 // 若是不存在,建立該hash並設置過時時間 if (false == conn.exists(hashName) ){ conn.hSet(hashName, key, json); conn.expire(hashName, Constants.HASH_EXPIRE_TIME); } else { conn.hSet(hashName, key, json); } return null; } });