1. 程式人生 > >11、Spring技術棧-整合Redis,通過Redis的Master-Slave實現快取資料讀寫分離

11、Spring技術棧-整合Redis,通過Redis的Master-Slave實現快取資料讀寫分離

1、Redis主從複製(Master-Salve Replication)簡介

Redis 支援簡單且易用的主從複製(master-slave replication)功能, 該功能可以讓從伺服器(slave server)成為主伺服器(master server)的精確複製品。

以下是關於 Redis 複製功能的幾個重要方面:

  1. Redis 使用非同步複製。 從 Redis 2.8 開始, 從伺服器會以每秒一次的頻率向主伺服器報告複製流(replication stream)的處理進度。

  2. 一個主伺服器可以有多個從伺服器。

  3. 不僅主伺服器可以有從伺服器, 從伺服器也可以有自己的從伺服器, 多個從伺服器之間可以構成一個圖狀結構。

  4. 複製功能不會阻塞主伺服器: 即使有一個或多個從伺服器正在進行初次同步, 主伺服器也可以繼續處理命令請求。

  5. 複製功能也不會阻塞從伺服器: 只要在 redis.conf 檔案中進行了相應的設定, 即使從伺服器正在進行初次同步, 伺服器也可以使用舊版本的資料集來處理命令查詢。

  6. 不過, 在從伺服器刪除舊版本資料集並載入新版本資料集的那段時間內, 連線請求會被阻塞。

  7. 你還可以配置從伺服器, 讓它在與主伺服器之間的連線斷開時, 向客戶端傳送一個錯誤。

  8. 複製功能可以單純地用於資料冗餘(data redundancy), 也可以通過讓多個從伺服器處理只讀命令請求來提升擴充套件性(scalability): 比如說, 繁重的 SORT 命令可以交給附屬節點去執行。

  9. 可以通過複製功能來讓主伺服器免於執行持久化操作: 只要關閉主伺服器的持久化功能, 然後由從伺服器去執行持久化操作即可。

2、例項介紹

在(Spring技術棧-整合Redis,使用RedisTemplate實現資料快取實戰)一文中我們已經介紹如何整合Redis,使用RedisTemplate實現資料的快取。本文將在此基礎上通過修改,實現快取資料的讀寫分離。

首先需要安裝兩臺虛擬機器(本文只講述一主一從的方式,關於一主多從的方式,各位可自行研究)並在虛擬機器上安裝Redis,具體如何將Redis配置成為主從複製(Master-Salve Replication)模式,請參考Redis主從(Master-Slave)複製(Replication)設定

具體場景描述:以部落格系統為例,使用者在釋出部落格時,系統將部落格寫入Redis,在詳細頁面讀取部落格時,系統從Redis中讀取部落格資訊展示。其中寫入時寫入到的是Master伺服器,讀則是從Slave伺服器讀取。

2.1 在resources目錄新建spring-context-redis-ms.xml,寫入如下內容

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
    xmlns:task="http://www.springframework.org/schema/task" xmlns:cache="http://www.springframework.org/schema/cache"
    xmlns:c='http://www.springframework.org/schema/c' xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.1.xsd"
    default-lazy-init="true">

    <!-- 開啟spring cache註解功能 -->
    <cache:annotation-driven cache-manager="redisCacheManager" />

    <context:annotation-config />

    <context:property-placeholder
        ignore-unresolvable="true" location="classpath:config.properties" />

    <!-- Redis -->
     <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig" >  
          <property name="maxTotal" value="${redis.maxTotal}"/> 
          <property name="maxIdle" value="${redis.maxIdle}" />  
          <property name="maxWaitMillis" value="${redis.maxWait}" />  
          <property name="testOnBorrow" value="${redis.testOnBorrow}" />  
    </bean>  

    <!-- redis主伺服器中心 -->  
    <bean id="jedisConnectionFactory"  class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >  
          <property name="poolConfig" ref="poolConfig" />  
          <property name="port" value="${redis.master.port}" />  
          <property name="hostName" value="${redis.master.host}" />
          <property name="timeout" value="${redis.timeout}" ></property>  
    </bean>
    <!-- redis從伺服器中心 -->  
    <bean id="slaveJedisConnectionFactory"  class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >  
          <property name="poolConfig" ref="poolConfig" />  
          <property name="port" value="${redis.slave.port}" />  
          <property name="hostName" value="${redis.slave.host}" />
          <property name="timeout" value="${redis.timeout}" ></property>  
    </bean>

    <!-- 主RedisTemplate -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="keySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
    </bean>
    <!-- 從RedisTemplate -->
    <bean id="slaveRedisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="slaveJedisConnectionFactory" />
        <property name="keySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
    </bean>

    <!-- 主redis快取管理器 -->
    <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg name="redisOperations" ref="redisTemplate" />
    </bean>
    <!-- 從redis快取管理器 -->
    <bean id="slaveRedisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg name="redisOperations" ref="slaveRedisTemplate" />
    </bean>

    <bean id="redisUtils" class="ron.blog.blog_service.utils.MsRedisUtils">
        <constructor-arg name="redisTemplate" ref="redisTemplate" />
        <constructor-arg name="slaveRedisTemplate" ref="slaveRedisTemplate" />
    </bean>  
</beans>

在以上的配置檔案中,我們配置了主(Master)伺服器和從(Slave)伺服器的相關資訊,並配置了id為redisTemplate的RedisTemplate物件來操作主(Master)伺服器,id為slaveRedisTemplate的RedisTemplate物件來操作從(Slave)伺服器。所有對Redis的操作我們都是通過一個叫做MsRedisUtils的幫助類來完成,該類的物件初始化時,我們往建構函式中傳入主從(Master-Slave)伺服器的RedisTemplate操作物件。

2.2 MsRedisUtils類構建如下

public class MsRedisUtils {
    /** 
     * RedisTemplate是一個簡化Redis資料訪問的一個幫助類, 
     * 此類對Redis命令進行高階封裝,通過此類可以呼叫ValueOperations和ListOperations等等方法。 
     */  
    private RedisTemplate<Serializable, Object> redisTemplate;  
    private RedisTemplate<Serializable, Object> slaveRedisTemplate;

    MsRedisUtils(RedisTemplate<Serializable, Object> redisTemplate,RedisTemplate<Serializable, Object> slaveRedisTemplate){
        this.redisTemplate=redisTemplate;
        this.slaveRedisTemplate=slaveRedisTemplate;
    }

    /** 
     * 寫入快取 
     *  
     * @param key 
     * @param value 
     * @return 
     */  
    public boolean set(final String key, Object value) {  
        boolean result = false;  
        try {  
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();  
            operations.set(key, value);  
            result = true;  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return result;  
    }  

    /** 
     *  
     * @Author Ron 
     * @param key 
     * @param hashKey 
     * @param value 
     * @return 
     */  
    public boolean set(final String key, final String hashKey, Object value) {  
        boolean result = false;  
        try {  
            HashOperations<Serializable,Object,Object> operations = redisTemplate.opsForHash();  
            operations.put(key, hashKey, value);  
            result = true;  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return result;  
    }  

    /** 
     * 寫入快取 
     *  
     * @param key 
     * @param value 
     * @return 
     */  
    public boolean set(final String key, Object value, Long expireTime) {  
        boolean result = false;  
        try {  
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();  
            operations.set(key, value);  
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);  
            result = true;  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return result;  
    } 

    /** 
     * 批量刪除對應的value 
     *  
     * @param keys 
     */  
    public void remove(final String... keys) {  
        for (String key : keys) {  
            remove(key);  
        }
    }  

    /** 
     * 批量刪除key 
     *  
     * @param pattern 
     */  
    public void removePattern(final String pattern) {  
        Set<Serializable> keys = redisTemplate.keys(pattern);  
        if (keys.size() > 0)  
            redisTemplate.delete(keys);  
    }  

    /** 
     * 刪除對應的value 
     * @param key 
     */  
    public void remove(final String key) {  
        if (exists(key)) {  
            redisTemplate.delete(key);  
        }  
    }  

    /** 
     * 快取是否存在
     * @param key 
     * @return 
     */  
    public boolean exists(final String key) {  
        return slaveRedisTemplate.hasKey(key);  
    }  

    /** 
     * 讀取快取 
     * @param key 
     * @return 
     */  
    public Object get(final String key) {  
        Object result = null;  
        ValueOperations<Serializable, Object> operations = slaveRedisTemplate.opsForValue();  
        result = operations.get(key);  
        return result;  
    }  

    /** 
     *  
     * @Author Ron
     * @param key 
     * @param hashKey 
     * @return 
     */  
    public Object get(final String key, final String hashKey){  
        Object result = null;  
        HashOperations<Serializable,Object,Object> operations = slaveRedisTemplate.opsForHash();  
        result = operations.get(key, hashKey);  
        return result;  
    }  
}

在該類中,所有寫操作(set、update、delete)都使用redisTemplate進行操作,所有的讀操作(get)都是用slaveRedisTemplate進行操作。

2.3 在spring-context.xml中匯入配置資訊

在spring-context.xml檔案中,加入如下配置資訊:

<import resource="classpath:spring-context-redis-ms.xml" />

2.4 使用MsRedisUtils類操作Redis

在服務中(本例項是BlogService類),使用MsRedisUtils作為Redis的操作類,實現往Redis伺服器寫入和讀取資料的功能。

    @Autowired  
    MsRedisUtils redisUtils;  

    /**
     * @Comment 新增部落格內容
     * @Author Ron
     * @Date 2017年10月25日 下午2:57:51
     * @return
     */
    @Override
    public Resp insertBlog(BlogContent blogContent) {
        String bid = IdGenerator.genUUID();
        blogContent.setBid(bid);
        blogContent.setCrtTime(new Date());
        blogContentMapper.insert(blogContent);
        redisUtils.set(bid, blogContent);
        return new Resp(ResCode.SUCCESS, "");
    }

    /**
     * @Comment 獲取部落格內容
     * @Author Ron
     * @Date 2017年10月25日 下午3:05:27
     * @return
     */
    @Override
    public BlogContent getBlog(String bid) {
        if(redisUtils.exists(bid)){
            logger.info("快取命中部落格"+bid);
            return (BlogContent) redisUtils.get(bid);
        }else{
            logger.info("快取尚未命中部落格"+bid);
            BlogContent blogContent = blogContentMapper.selectByPrimaryKey(bid);
            redisUtils.set(bid, blogContent);
            return blogContent;
        }
    }