爲何要用緩存服務器以及在 Java 中實現一個 redis 緩存服務

緩存服務的意義

爲何要使用緩存?說究竟是爲了提升系統的運行速度。將用戶頻繁訪問的內容存放在離用戶最近,訪問速度最快的地方,提升用戶的響應速度。一個 web 應用的簡單結構以下圖。 java

web 應用典型架構

在這個結構中,用戶的請求經過用戶層來到業務層,業務層在從數據層獲取數據,返回給用戶層。在用戶量小,數據量不太大的狀況下,這個系統運行得很順暢。可是隨着用戶量愈來愈大,數據庫中的數據愈來愈多,系統的用戶響應速度就愈來愈慢。系統的瓶頸通常都在數據庫訪問上。這個時候可能會將上面的架構改爲下面的來緩解數據庫的壓力。 git

一主多從結構

在這個架構中,將數據庫的讀請求和寫請求進行分離。數量衆多的讀請求都分配到從數據庫上,主數據庫只負責寫請求。從庫保持主動和主庫保持同步。這個架構比上一個有了很大的改進,通常的互聯網應用。這個架構就可以很好的支持了。他的一個缺點是比較複雜,主從庫之間保持高效實時,或者準實時的同步是一個不容易作到的事情。因此咱們有了另外一個思路,採用一個緩存服務器來存儲熱點數據,而關係數據用來存儲持久化的數據。結構以下圖所示github

採用緩存服務器讀的架構

採用緩存服務器讀的架構

在這個架構中,當讀取數據的時候,先從緩存服務器中獲取數據,若是獲取調,則直接返回該數據。若是沒有獲取調,則從數據庫中獲取數據。獲取到後,將該數據緩存到換出數據庫中,供下次訪問使用。當插入或者更新數據的時候,先將數據寫入到關係數據庫中,而後再更新緩存數據庫中的數據。web

固然了,爲了應付更大規模的訪問量,咱們還能夠將上面兩個改進的架構組合起來使用,既有讀寫分離的關係數據庫,又有能夠高速訪問的緩存服務。redis

以上緩存服務器架構的前提就是從緩存服務器中獲取數據的效率大大高於從關係型數據庫中獲取的效率。不然緩存服務器就沒有任何意義了。redis 的數據是保存在內存中的,可以保證從 redis 中獲取數據的時間效率比從關係數據庫中獲取高出不少。數據庫


基於 redis 緩存服務的實現

這一章節用一個實例來講明如何來在 Java 中實現一個 redis 的緩存服務。該代碼是在上一篇文章 《在 Java 中使用 redis》 中實現的代碼基礎上增長完成的。代碼同步發佈在 GitHub 倉庫apache

創建 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;

@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));
    //檢查從緩存刪除已有對象是否返回 true
    Assert.assertTrue(cacheService.delObject(key));
    //檢查從緩存刪除已有對象是否返回 false
    Assert.assertFalse(cacheService.delObject(key + "1"));
    //檢查從緩存獲取已刪除對象是否返回 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));
}
}
複製代碼

測試結果

測試結果

redis 做爲緩存服務是一個最基本的用法。這裏只實現了基於 k-value 數據的緩存。其他的 Hash、Set、List 等緩存的用法大同小異。

後面我還將講述 redis 實現分佈式鎖、全局惟一標識、LBS 服務和消息隊列的服務。

對應的代碼發佈到了 GitHub

原文發表在簡書中,原始連接

相關文章
相關標籤/搜索