如何在 Java 中實(shí)現(xiàn)一個(gè) redis 緩存服務(wù)
緩存服務(wù)的意義
為什么要使用緩存?說(shuō)到底是為了提高系統(tǒng)的運(yùn)行速度。將用戶頻繁訪問的內(nèi)容存放在離用戶最近,訪問速度最快的地方,提高用戶的響應(yīng)速度。一個(gè) web 應(yīng)用的簡(jiǎn)單結(jié)構(gòu)如下圖。
web 應(yīng)用典型架構(gòu)
在這個(gè)結(jié)構(gòu)中,用戶的請(qǐng)求通過用戶層來(lái)到業(yè)務(wù)層,業(yè)務(wù)層在從數(shù)據(jù)層獲取數(shù)據(jù),返回給用戶層。在用戶量小,數(shù)據(jù)量不太大的情況下,這個(gè)系統(tǒng)運(yùn)行得很順暢。但是隨著用戶量越來(lái)越大,數(shù)據(jù)庫(kù)中的數(shù)據(jù)越來(lái)越多,系統(tǒng)的用戶響應(yīng)速度就越來(lái)越慢。系統(tǒng)的瓶頸一般都在數(shù)據(jù)庫(kù)訪問上。這個(gè)時(shí)候可能會(huì)將上面的架構(gòu)改成下面的來(lái)緩解數(shù)據(jù)庫(kù)的壓力。
一主多從結(jié)構(gòu)
在這個(gè)架構(gòu)中,將數(shù)據(jù)庫(kù)的讀請(qǐng)求和寫請(qǐng)求進(jìn)行分離。數(shù)量眾多的讀請(qǐng)求都分配到從數(shù)據(jù)庫(kù)上,主數(shù)據(jù)庫(kù)只負(fù)責(zé)寫請(qǐng)求。從庫(kù)保持主動(dòng)和主庫(kù)保持同步。這個(gè)架構(gòu)比上一個(gè)有了很大的改進(jìn),一般的互聯(lián)網(wǎng)應(yīng)用。這個(gè)架構(gòu)就能夠很好的支持了。他的一個(gè)缺點(diǎn)是比較復(fù)雜,主從庫(kù)之間保持高效實(shí)時(shí),或者準(zhǔn)實(shí)時(shí)的同步是一個(gè)不容易做到的事情。所以我們有了另一個(gè)思路,采用一個(gè)緩存服務(wù)器來(lái)存儲(chǔ)熱點(diǎn)數(shù)據(jù),而關(guān)系數(shù)據(jù)用來(lái)存儲(chǔ)持久化的數(shù)據(jù)。結(jié)構(gòu)如下圖所示
采用緩存服務(wù)器讀的架構(gòu)
采用緩存服務(wù)器讀的架構(gòu)
在這個(gè)架構(gòu)中,當(dāng)讀取數(shù)據(jù)的時(shí)候,先從緩存服務(wù)器中獲取數(shù)據(jù),如果獲取調(diào),則直接返回該數(shù)據(jù)。如果沒有獲取調(diào),則從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)。獲取到后,將該數(shù)據(jù)緩存到換出數(shù)據(jù)庫(kù)中,供下次訪問使用。當(dāng)插入或者更新數(shù)據(jù)的時(shí)候,先將數(shù)據(jù)寫入到關(guān)系數(shù)據(jù)庫(kù)中,然后再更新緩存數(shù)據(jù)庫(kù)中的數(shù)據(jù)。
當(dāng)然了,為了應(yīng)付更大規(guī)模的訪問量,我們還可以將上面兩個(gè)改進(jìn)的架構(gòu)組合起來(lái)使用,既有讀寫分離的關(guān)系數(shù)據(jù)庫(kù),又有可以高速訪問的緩存服務(wù)。
以上緩存服務(wù)器架構(gòu)的前提就是從緩存服務(wù)器中獲取數(shù)據(jù)的效率大大高于從關(guān)系型數(shù)據(jù)庫(kù)中獲取的效率。否則緩存服務(wù)器就沒有任何意義了。redis 的數(shù)據(jù)是保存在內(nèi)存中的,能夠保證從 redis 中獲取數(shù)據(jù)的時(shí)間效率比從關(guān)系數(shù)據(jù)庫(kù)中獲取高出很多。
基于 redis 緩存服務(wù)的實(shí)現(xiàn)
這一章節(jié)用一個(gè)實(shí)例來(lái)說(shuō)明如何來(lái)在 Java 中實(shí)現(xiàn)一個(gè) redis 的緩存服務(wù)。
建立 maven 工程并引入依賴
定義接口類com.x9710.common.redis.CacheService
在這個(gè)接口類中,主要定了下面的接口
- 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();
這些接口分別用于存儲(chǔ)不過期的對(duì)象、存儲(chǔ)將來(lái)過期對(duì)象、獲取緩存對(duì)象、獲取緩存對(duì)象剩余存活時(shí)間、刪除緩存對(duì)象、設(shè)置緩存對(duì)象過期時(shí)間、清除所有緩存對(duì)象的功能
package com.x9710.common.redis; /** * 緩存服務(wù)接口 * * @author 楊高超 * @since 2017-12-09 */ public interface CacheService { /** * 將對(duì)象存放到緩存中 * * @param key 存放的key * @param value 存放的值 */ void putObject(String key, Object value); /** * 將對(duì)象存放到緩存中 * * @param key 存放的key * @param value 存放的值 * @param expiration 過期時(shí)間,單位秒 */ void putObject(String key, Object value, int expiration); /** * 從緩存中獲取對(duì)象 * * @param key 要獲取對(duì)象的key * @return 如果存在,返回對(duì)象,否則,返回null */ Object pullObject(String key); /** * 給緩存對(duì)象設(shè)置過期秒數(shù) * * @param key 要獲取對(duì)象的key * @param expireSecond 過期秒數(shù) * @return 如果存在,返回對(duì)象,否則,返回null */ boolean expire(String key, int expireSecond); /** * 獲取緩存對(duì)象過期秒數(shù) * * @param key 要獲取對(duì)象的key * @return 如果對(duì)象不存在,返回-2,如果對(duì)象沒有過期時(shí)間,返回-1,否則返回實(shí)際過期時(shí)間 */ Long ttl(String key); /** * 從緩存中刪除對(duì)象 * * @param key 要?jiǎng)h除對(duì)象的key * @return 如果出現(xiàn)錯(cuò)誤,返回 false,否則返回true */ boolean delObject(String key); /** * 從緩存中清除對(duì)象 */ void clearObject(); }
定義序列號(hào)輔助類com.x9710.common.redis.SerializeUtil
所有要保存到 redis 數(shù)據(jù)庫(kù)中的對(duì)象需要先序列號(hào)為二進(jìn)制數(shù)組,這個(gè)類的作用是將 Java 對(duì)象序列號(hào)為二級(jí)制數(shù)組或者將二級(jí)制數(shù)組反序列化為對(duì)象。
package com.x9710.common.redis; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * 對(duì)象序列化工具類 * * @author 楊高超 * @since 2017-10-09 */ public class SerializeUtil { /** * 將一個(gè)對(duì)象序列化為二進(jìn)制數(shù)組 * * @param object 要序列化的對(duì)象,該必須實(shí)現(xiàn)java.io.Serializable接口 * @return 被序列化后的二進(jìn)制數(shù)組 */ 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; } /** * 將一個(gè)二進(jìn)制數(shù)組反序列化為一個(gè)對(duì)象。程序不檢查反序列化過程中的對(duì)象類型。 * * @param bytes 要反序列化的二進(jìn)制數(shù) * @return 反序列化后的對(duì)象 */ 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; } }
實(shí)現(xiàn) redis 緩存服務(wù)類 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; /** * 緩存服務(wù) redis 實(shí)現(xiàn)類 * * @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(); } } } }
編寫測(cè)試用例
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; /** * 緩存服務(wù)測(cè)試類 * * @author 楊高超 * @since 2017-12-09 */ public class RedisCacheTest { private CacheServiceRedisImpl cacheService; @Before public void before() { RedisConnection redisConnection = RedisConnectionUtil.create(); cacheService = new CacheServiceRedisImpl(); cacheService.setDbIndex(2); cacheService.setRedisConnection(redisConnection); } @Test public void testStringCache() { String key = "name"; String value = "grace"; cacheService.putObject(key, value); String cachValue = (String) cacheService.pullObject(key); //檢查從緩存中獲取的字符串是否等于原始的字符串 Assert.assertTrue(value.equals(cachValue)); //檢查從緩存刪除已有對(duì)象是否返回 true Assert.assertTrue(cacheService.delObject(key)); //檢查從緩存刪除已有對(duì)象是否返回 false Assert.assertFalse(cacheService.delObject(key + "1")); //檢查從緩存獲取已刪除對(duì)象是否返回 null Assert.assertTrue(cacheService.pullObject(key) == null); } @Test public 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); } @Test public 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)); } }
測(cè)試結(jié)果
測(cè)試結(jié)果
redis 作為緩存服務(wù)是一個(gè)最基本的用法。這里只實(shí)現(xiàn)了基于 k-value 數(shù)據(jù)的緩存。其余的 Hash、Set、List 等緩存的用法大同小異。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 如何基于LoadingCache實(shí)現(xiàn)Java本地緩存
- Java緩存Map設(shè)置過期時(shí)間實(shí)現(xiàn)解析
- Java中LocalCache本地緩存實(shí)現(xiàn)代碼
- Java LocalCache 本地緩存的實(shí)現(xiàn)實(shí)例
- Java本地緩存的實(shí)現(xiàn)代碼
- Java自定義注解實(shí)現(xiàn)Redis自動(dòng)緩存的方法
- Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的緩存方法
- java shiro實(shí)現(xiàn)退出登陸清空緩存
- Java 實(shí)現(xiàn)緩存的三種方式及問題匯總
相關(guān)文章
往DAO類中注入@PersistenceContext和@Resource的區(qū)別詳解
這篇文章主要介紹了往DAO類中注入@PersistenceContext和@Resource的區(qū)別詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02MyBatis中使用分頁(yè)插件PageHelper實(shí)現(xiàn)分頁(yè)功能
分頁(yè)是經(jīng)常使用的功能,本文主要介紹了Mybatis中處理特殊SQL處理邏輯,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06SpringBoot+BootStrap多文件上傳到本地實(shí)例
這篇文章主要介紹了SpringBoot+BootStrap多文件上傳到本地實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03Spring 處理 HTTP 請(qǐng)求參數(shù)注解的操作方法
這篇文章主要介紹了Spring 處理 HTTP 請(qǐng)求參數(shù)注解的操作方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友參考下吧2024-04-04如何實(shí)現(xiàn)Spring?Event(異步事件)
這篇文章主要介紹了如何實(shí)現(xiàn)Spring?Event(異步事件)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02