標籤:res 分離 domain final 運行速度 www war 速度 定義
快取服務的意義
為什麼要使用緩衝?說到底是為了提高系統的運行速度。將使用者頻繁訪問的內容存放在離使用者最近,訪問速度最快的地方,提高使用者的響應速度。一個 web 應用的簡單結構如。
web 應用典型架構
在這個結構中,使用者的請求通過使用者層來到業務層,業務層在從資料層擷取資料,返回給使用者層。在使用者量小,資料量不太大的情況下,這個系統運行得很順暢。但是隨著使用者量越來越大,資料庫中的資料越來越多,系統的使用者響應速度就越來越慢。系統的瓶頸一般都在資料庫訪問上。這個時候可能會將上面的架構改成下面的來緩解資料庫的壓力。
一主多從結構
在這個架構中,將資料庫的讀請求和寫請求進行分離。數量眾多的讀請求都分配到從資料庫上,主要資料庫只負責寫請求。從庫保持主動和主庫保持同步。這個架構比上一個有了很大的改進,一般的互連網應用。這個架構就能夠很好的支援了。他的一個缺點是比較複雜,主從庫之間保持高效即時,或者准即時的同步是一個不容易做到的事情。所以我們有了另一個思路,採用一個快取服務器來儲存熱點資料,而關係資料用來儲存持久化的資料。結構如所示
採用快取服務器讀的架構 採用快取服務器讀的架構
在這個架構中,當讀取資料的時候,先從快取服務器中擷取資料,如果擷取調,則直接返回該資料。如果沒有擷取調,則從資料庫中擷取資料。擷取到後,將該資料緩衝到換出資料庫中,供下次訪問使用。當插入或者更新資料的時候,先將資料寫入到關聯式資料庫中,然後再更新快取資料庫中的資料。
當然了,為了應付更大規模的訪問量,我們還可以將上面兩個改進的架構組合起來使用,既有讀寫分離的關聯式資料庫,又有可以高速訪問的快取服務。
以上快取服務器架構的前提就是從快取服務器中擷取資料的效率大大高於從關係型資料庫中擷取的效率。否則快取服務器就沒有任何意義了。redis 的資料是儲存在記憶體中的,能夠保證從 redis 中擷取資料的時間效率比從關聯式資料庫中擷取高出很多。
基於 redis 快取服務的實現
這一章節用一個執行個體來說明如何來在 Java 中實現一個 redis 的快取服務。該代碼是在上一篇文章 《在 Java 中使用 redis》 中實現的代碼基礎上增加完成的。代碼同步發布在 GitHub 倉庫中
建立 maven 工程並引入依賴
參考文章 《在 Java 中使用 redis》 中的 pom.xml 檔案內容
定義介面類com.x9710.common.redis.CacheService
在這個介面類中,主要定了下面的介面
- void putObject(String key, Object value);
- void putObject(String key, Object value, int expiration);
- Object pullObject(String key);
- Long ttl(String key);
- boolean delObject(String key);
- boolean expire(String key, int expireSecond);
- void clearObject();
這些介面分別用於儲存不到期的對象、儲存將來到期對象、擷取緩衝對象、擷取緩衝對象剩餘存活時間、刪除緩衝對象、設定緩衝對象到期時間、清除所有緩衝對象的功能
package com.x9710.common.redis;/** * 快取服務介面 * * @author 楊高超 * @since 2017-12-09 */public interface CacheService {/** * 將對象存放到緩衝中 * * @param key 存放的key * @param value 存放的值 */void putObject(String key, Object value);/** * 將對象存放到緩衝中 * * @param key 存放的key * @param value 存放的值 * @param expiration 到期時間,單位秒 */void putObject(String key, Object value, int expiration);/** * 從緩衝中擷取對象 * * @param key 要擷取對象的key * @return 如果存在,返回對象,否則,返回null */Object pullObject(String key);/** * 給緩衝對象設定到期秒數 * * @param key 要擷取對象的key * @param expireSecond 到期秒數 * @return 如果存在,返回對象,否則,返回null */boolean expire(String key, int expireSecond);/** * 擷取緩衝對象到期秒數 * * @param key 要擷取對象的key * @return 如果對象不存在,返回-2,如果對象沒有到期時間,返回-1,否則返回實際到期時間 */Long ttl(String key);/** * 從緩衝中刪除對象 * * @param key 要刪除對象的key * @return 如果出現錯誤,返回 false,否則返回true */boolean delObject(String key);/** * 從緩衝中清除對象 */void clearObject();}
定義序號輔助類com.x9710.common.redis.SerializeUtil
所有要儲存到 redis 資料庫中的對象需要先序號為位元組,這個類的作用是將 Java 對象序號為二級制數組或者將二級制數組還原序列化為對象。
package com.x9710.common.redis;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;/** * 對象序列化工具類 * * @author 楊高超 * @since 2017-10-09 */public class SerializeUtil {/** * 將一個對象序列化為位元組 * * @param object 要序列化的對象,該必須實現java.io.Serializable介面 * @return 被序列化後的位元組 */public static byte[] serialize(Object object) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(object); return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return null;}/** * 將一個位元組還原序列化為一個對象。程式不檢查還原序列化過程中的物件類型。 * * @param bytes 要還原序列化的位元 * @return 還原序列化後的對象 */public static Object unserialize(byte[] bytes) { try { ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais); return ois.readObject(); } catch (Exception e) { e.printStackTrace(); } return null;}}
實現 redis 快取服務類 com.x9710.common.redis.impl.CacheServiceRedisImpl
package com.x9710.common.redis.impl;import com.x9710.common.redis.CacheService;import com.x9710.common.redis.RedisConnection;import com.x9710.common.redis.SerializeUtil;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import redis.clients.jedis.Jedis;/** * 快取服務 redis 實作類別 * * @author 楊高超 * @since 2017-12-09 */public class CacheServiceRedisImpl implements CacheService {private static Log log = LogFactory.getLog(CacheServiceRedisImpl.class);private RedisConnection redisConnection;private Integer dbIndex;public void setRedisConnection(RedisConnection redisConnection) { this.redisConnection = redisConnection;}public void setDbIndex(Integer dbIndex) { this.dbIndex = dbIndex;}public void putObject(String key, Object value) { putObject(key, value, -1);}public void putObject(String key, Object value, int expiration) { Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); if (expiration > 0) { jedis.setex(key.getBytes(), expiration, SerializeUtil.serialize(value)); } else { jedis.set(key.getBytes(), SerializeUtil.serialize(value)); } } catch (Exception e) { log.warn(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } }}public Object pullObject(String key) { log.trace("strar find cache with " + key); Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); byte[] result = jedis.get(key.getBytes()); if (result == null) { log.trace("can not find caceh with " + key); return null; } else { log.trace("find cache success with " + key); return SerializeUtil.unserialize(result); } } catch (Exception e) { log.warn(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } } return null;}public boolean expire(String key, int expireSecond) { log.trace("strar set expire " + key); Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); return jedis.expire(key, expireSecond) == 1; } catch (Exception e) { log.warn(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } } return false;}public Long ttl(String key) { log.trace("get set expire " + key); Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); return jedis.ttl(key); } catch (Exception e) { log.warn(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } } return -2L;}public boolean delObject(String key) { log.trace("strar delete cache with " + key); Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); return jedis.del(key.getBytes()) > 0; } catch (Exception e) { log.warn(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } } return false;}public void clearObject() { Jedis jedis = null; try { jedis = redisConnection.getJedis(); jedis.select(dbIndex); jedis.flushDB(); } catch (Exception e) { log.warn(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } }}}
編寫測試案例
package com.x9710.common.redis.test;import com.x9710.common.redis.RedisConnection;import com.x9710.common.redis.impl.CacheServiceRedisImpl;import com.x9710.common.redis.test.domain.Student;import org.junit.Assert;import org.junit.Before;import org.junit.Test;/** * 快取服務測試類別 * * @author 楊高超 * @since 2017-12-09 */public class RedisCacheTest {private CacheServiceRedisImpl cacheService;@Beforepublic void before() { RedisConnection redisConnection = RedisConnectionUtil.create(); cacheService = new CacheServiceRedisImpl(); cacheService.setDbIndex(2); cacheService.setRedisConnection(redisConnection);}@Testpublic void testStringCache() { String key = "name"; String value = "grace"; cacheService.putObject(key, value); String cachValue = (String) cacheService.pullObject(key); //檢查從緩衝中擷取的字串是否等於原始的字串 Assert.assertTrue(value.equals(cachValue)); //檢查從緩衝刪除已有對象是否返回 true Assert.assertTrue(cacheService.delObject(key)); //檢查從緩衝刪除已有對象是否返回 false Assert.assertFalse(cacheService.delObject(key + "1")); //檢查從緩衝擷取已刪除對象是否返回 null Assert.assertTrue(cacheService.pullObject(key) == null);}@Testpublic void testObjectCache() { Student oriStudent = new Student(); oriStudent.setId("2938470s9d8f0"); oriStudent.setName("柳白猿"); oriStudent.setAge(36); cacheService.putObject(oriStudent.getId(), oriStudent); Student cacheStudent = (Student) cacheService.pullObject(oriStudent.getId()); Assert.assertTrue(oriStudent.equals(cacheStudent)); Assert.assertTrue(cacheService.delObject(oriStudent.getId())); Assert.assertTrue(cacheService.pullObject(oriStudent.getId()) == null);}@Testpublic void testExpireCache() { String key = "name"; String value = "grace"; cacheService.putObject(key, value); cacheService.expire(key, 300); String cachValue = (String) cacheService.pullObject(key); Assert.assertTrue(value.equals(cachValue)); Long ttl = cacheService.ttl(key); Assert.assertTrue(ttl > 250 && ttl <= 300); Assert.assertTrue(value.equals(cachValue)); Assert.assertTrue(cacheService.delObject(key));}}
測試結果 測試結果
redis 作為快取服務是一個最基本的用法。這裡只實現了基於 k-value 資料的緩衝。其餘的 Hash、Set、List 等緩衝的用法大同小異。
後面我還將講述 redis 實現分布式鎖、全域唯一標識、LBS 服務和訊息佇列的服務。
歡迎加入學習交流群569772982,大家一起學習交流。
為什麼要用快取服務器以及在 Java 中實現一個 redis 快取服務