為什麼要用快取伺服器以及在 Java 中實現一個 redis 快取服務

楊高超發表於2018-01-02

快取服務的意義

為什麼要使用快取?說到底是為了提高系統的執行速度。將使用者頻繁訪問的內容存放在離使用者最近,訪問速度最快的地方,提高使用者的響應速度。一個 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;

@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

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

相關文章