1. 程式人生 > >JavaEE進階——Redis叢集搭建與快取實現

JavaEE進階——Redis叢集搭建與快取實現

一、Redis簡介

​ Redis是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫,並提供多種語言的API。從2010年3月15日起,Redis的開發工作由VMware主持。從2013年5月開始,Redis的開發由Pivotal贊助。

​ Redis是一個key-value儲存系統。和Memcached類似,它支援儲存的value型別相對更多,包括string(字串)、list(連結串列、set(集合)、zset(sorted set –有序集合)和hash(雜湊型別)。這些資料型別都支援push/pop、add/remove及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,redis支援各種不同方式的排序。與memcached一樣,為了保證效率,資料都是快取在記憶體中。區別的是redis會週期性的把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案,並且在此基礎上實現了master-slave(主從)同步。

​ Redis支援主從同步。資料可以從主伺服器向任意數量的從伺服器上同步,從伺服器可以是關聯其他從伺服器的主伺服器。這使得Redis可執行單層樹複製。存檔可以有意無意的對資料進行寫操作。由於完全實現了釋出/訂閱機制,使得從資料庫在任何地方同步樹時,可訂閱一個頻道並接收主伺服器完整的訊息釋出記錄。同步對讀取操作的可擴充套件性和資料冗餘很有幫助。

二、Redis五種資料型別

  • string 字元型別
  • hash 雜湊型別
  • list 列表型別
  • set 集合型別
  • zset(sorted set) 有序集合型別

三、Redis單機版安裝

下載Redis

安裝步驟

  1. 安裝gcc編譯環境

    yum install gcc-c++
  2. 把redis的原始碼上傳到linux伺服器

  3. 解壓縮

    tar -zxvf redis-4.0.10.tar.gz
  4. 編譯安裝

    make
    make install PREFIX=/usr/local/redis

啟動Redis

兩種啟動方式:前端啟動、後臺啟動。

前端啟動:./redis-server

後臺啟動

  • 複製redis.conf到redis的安裝目錄

    cp redis-4.0.10/redis.conf -c /usr/local/redis/bin/
  • 修改redis.conf,修改daemonize yes

  • ./redis-server redis.conf

檢視啟動結果:

ps aux|grep redis

關閉Redis

./redis-cli -a password -h 192.168.1.20 -p 6379 shutdown
ps aux|grep redis

四、客戶端連線Redis

redis-cli

命令格式:redis-cli -p 埠 -h IP地址 <-c>連線叢集時使用此引數

預設埠:6379

預設IP:localhost(127.0.0.1)

RedisDesktopManager

設定防火牆:

/sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
/etc/rc.d/init.d/iptables save

修改redis.conf配置檔案:註釋掉本地連結限制設定密碼取消redis的預設保護模式


重啟redis服務,客戶端連線測試:

五、搭建Redis叢集

redis-cluster架構圖

架構細節

  1. 所有的redis節點彼此互聯(PING-PONG機制),內部使用二進位制協議優化傳輸速度和頻寬.。
  2. 節點的fail是通過叢集中超過半數的節點檢測失效時才生效。
  3. 客戶端與redis節點直連,不需要中間proxy層。客戶端不需要連線叢集所有節點,連線叢集中任何一個可用節點即可。
  4. redis-cluster把所有的物理節點對映到[0-16383]slot上,cluster 負責維護node<->slot<->value。Redis 叢集中內建了 16384 個雜湊槽,當需要在 Redis 叢集中放置一個 key-value 時,redis 先對 key 使用 crc16 演算法算出一個結果,然後把結果對 16384 求餘數,這樣每個 key 都會對應一個編號在 0-16383 之間的雜湊槽,redis 會根據節點數量大致均等的將雜湊槽對映到不同的節點。

搭建步驟

偽分散式叢集配置:叢集中應該至少有3個節點,每個節點有1個備份節點,需要6臺伺服器(6個Redis例項)。

  1. 複製6份編譯安裝好的單機版Redis例項,分別為redis01-06;


  2. 刪除每個例項的dump.rdb檔案,修改配置檔案redis.conf的埠號7001-7006以及開放叢集配置;


  3. 拷貝Redis原始碼包src下用於搭建叢集的ruby指令碼:redis-trib.rb;

    cp /root/redis-4.0.10/src/redis-trib.rb -c ./
  4. 安裝ruby環境;

    
    # 下載Ruby原始碼包
    
    wget --no-check-certificate https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.0.tar.gz
    
    # 解壓安裝
    
    tar -zxvf ruby-2.3.0.tar.gz
    ./configure
    make
    make install
    
    # 檢視Ruby版本
    
    ruby -v
    
    
    # 安裝redis-trib.rb執行依賴的ruby的包
    
    gem install redis
  5. 啟動所有Redis例項;

    cd redis01/bin/
    ./redis-server redis.conf
    cd ../../
    cd redis02/bin/
    ./redis-server redis.conf
    cd ../../
    cd redis03/bin/
    ./redis-server redis.conf
    cd ../../
    cd redis04/bin/
    ./redis-server redis.conf
    cd ../../
    cd redis05/bin/
    ./redis-server redis.conf
    cd ../../
    cd redis06/bin/
    ./redis-server redis.conf
    cd ../../

  6. 使用redis-trib.rb指令碼建立Redis叢集(防火牆開啟7001-7006)。

    service iptables stop  # 臨時關閉防火牆
    
    ./redis-trib.rb create --replicas 1 192.168.74.128:7001 192.168.74.128:7002 192.168.74.128:7003 192.168.74.128:7004 192.168.74.128:7005  192.168.74.128:7006

    如果出現Sorry, can’t connect to node,需要修改redis-trib.rb指令碼,新增連線密碼。

搭建結果

五、Jedis客戶端

單機版測試

需要新增jedis依賴的jar包。

/**
  * 單機版測試
  */
@Test
public void testJedis() {
    // 建立一個Jedis物件
    Jedis jedis = new Jedis("192.168.74.128", 6379);
    // 設定連線密碼
    jedis.auth("123456");
    jedis.set("single", "hello single redis");
    String str = jedis.get("single");
    System.out.println(str);

    jedis.close();
}

結果:

生產環境一般使用連線池的方式對Redis連線進行管理,所有Jedis物件先放在池子中每一次需要的時候連線Redis,只需要在池子中借,用完了再歸還給池子。

優點缺點
直連簡單方便,適用於少量長期連線的場景1. 存在每次新建/關閉TCP連線開銷2. 資源無法控制,極端情況下會出現連線洩露3. Jedis物件執行緒不安全
連線池1. 無需每次連線都生成Jedis物件,降低開銷2、使用連線池的形式保護和控制資源的使用相對於直連,使用相對麻煩,尤其在資源管理上需要很多引數來保證,一旦規劃不合理也會出現問題。

使用連線池: 

/**
  * 使用連線池
  */
@Test
public void testJedisPool() {
    // 建立一個連線池物件 (系統中應該是單例的)
    JedisPool jedisPool = new JedisPool("192.168.74.128", 6379);
    Jedis jedis = jedisPool.getResource();
    jedis.auth("123456");
    String str = jedis.get("single");
    System.out.println(str);
    // jedis必須關閉
    jedis.close();

    // 系統關閉時關閉連線池
    jedisPool.close();
}

叢集版測試

redis叢集在jedis2.9.0版後添加了JedisCluster配置叢集密碼的功能,所以新增Jedis的jar包最好是2.9版本以上。

/**
  * 連線redis叢集
  */
@Test
public void testJedisCluster() throws IOException {
    // 建立節點集合
    Set<HostAndPort> nodes = new HashSet<>();
    nodes.add(new HostAndPort("192.168.74.128", 7001));
    nodes.add(new HostAndPort("192.168.74.128", 7002));
    nodes.add(new HostAndPort("192.168.74.128", 7003));
    nodes.add(new HostAndPort("192.168.74.128", 7004));
    nodes.add(new HostAndPort("192.168.74.128", 7005));
    nodes.add(new HostAndPort("192.168.74.128", 7006));

    // 建立一個JedisCluster物件,在系統中是單例的。
    JedisCluster jedisCluster = new JedisCluster(nodes, 2000, 2000, 5, 
                                                 "123456", new GenericObjectPoolConfig());
    jedisCluster.set("cluster", "hello cluster redis");
    String str = jedisCluster.get("cluster");
    System.out.println(str);

    // 系統關閉時關閉jedisCluster
    jedisCluster.close();
}

結果:

六、Spring配置Redis

Spring使用單機版和叢集版Redis方式不一致,我們可以使用面向介面程式設計的思想實現兩者的無縫切換:定義一個Redis操作介面,分別實現單機版和叢集版的實現類。當使用單機版redis時,配置單機版的實現類,當使用叢集版本的時候,配置叢集版的實現類。

介面類

public interface JedisClient {
    String set(String key, String value);
    String get(String key);
    Long hset(String key, String item, String value);
    String hget(String key, String item);
    Long incr(String key);
    Long decr(String key);
    Long expire(String key, int second);
    Long ttl(String key);
}

單機版實現類

import guo.ping.taotao.rest.component.JedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * redis客戶端單機版實現類
 */
public class JedisClientSingle implements JedisClient {

    @Autowired
    private JedisPool jedisPool;

    @Value("${REDIS_PASSWORD}")
    private String REDIS_PASSWORD;

    private Jedis getJedisResource(JedisPool jedisPool) {
        Jedis jedis = jedisPool.getResource();
        jedis.auth(REDIS_PASSWORD);
        return jedis;
    }

    @Override
    public String set(String key, String value) {
        Jedis jedis = getJedisResource(jedisPool);
        String result = jedis.set(key, value);
        jedis.close();
        return result;
    }

    @Override
    public String get(String key) {
        Jedis jedis = getJedisResource(jedisPool);
        String result = jedis.get(key);
        jedis.close();
        return result;
    }

    @Override
    public Long hset(String key, String item, String value) {
        Jedis jedis = getJedisResource(jedisPool);
        Long result = jedis.hset(key, item, value);
        jedis.close();
        return result;
    }

    @Override
    public String hget(String key, String item) {
        Jedis jedis = getJedisResource(jedisPool);
        String result = jedis.hget(key, item);
        jedis.close();
        return result;
    }

    @Override
    public Long incr(String key) {
        Jedis jedis = getJedisResource(jedisPool);
        Long result = jedis.incr(key);
        jedis.close();
        return result;
    }

    @Override
    public Long decr(String key) {
        Jedis jedis = getJedisResource(jedisPool);
        Long result = jedis.decr(key);
        jedis.close();
        return result;
    }

    @Override
    public Long expire(String key, int second) {
        Jedis jedis = getJedisResource(jedisPool);
        Long result = jedis.expire(key, second);
        jedis.close();
        return result;
    }

    @Override
    public Long ttl(String key) {
        Jedis jedis = getJedisResource(jedisPool);
        Long result = jedis.ttl(key);
        jedis.close();
        return result;
    }
}

叢集版實現類

import guo.ping.taotao.rest.component.JedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.JedisCluster;

/**
 * redis客戶端叢集版實現類
 */
public class JedisClientCluster implements JedisClient {

    @Autowired
    private JedisCluster jedisCluster;

    @Override
    public String set(String key, String value) {
        return jedisCluster.set(key, value);
    }

    @Override
    public String get(String key) {
        return jedisCluster.get(key);
    }

    @Override
    public Long hset(String key, String item, String value) {
        return jedisCluster.hset(key, item, value);
    }

    @Override
    public String hget(String key, String item) {
        return jedisCluster.hget(key, item);
    }

    @Override
    public Long incr(String key) {
        return jedisCluster.incr(key);
    }

    @Override
    public Long decr(String key) {
        return jedisCluster.decr(key);
    }

    @Override
    public Long expire(String key, int second) {
        return jedisCluster.expire(key, second);
    }

    @Override
    public Long ttl(String key) {
        return jedisCluster.ttl(key);
    }
}

Spirng配置Redis

redis的相關屬性設定:

# redis_password
REDIS_PASSWORD=123456
# redis_connection_timeout
REDIS_CONNECTION_TIMEOUT=2000
# redis_so_timeout
REDIS_SO_TIMEOUT=2000
# redis_attempts
REDIS_ATTEMPTS=5
# redis_nodes
REDIS_HOST1=192.168.74.128
REDIS_HOST2=192.168.74.128
REDIS_HOST3=192.168.74.128
REDIS_HOST4=192.168.74.128
REDIS_HOST5=192.168.74.128
REDIS_HOST6=192.168.74.128
REDIS_PORT1=7001
REDIS_PORT2=7002
REDIS_PORT3=7003
REDIS_PORT4=7004
REDIS_PORT5=7005
REDIS_PORT6=7006

單機版配置:

<!--配置單擊版Redis-->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
    <constructor-arg name="host" value="192.168.74.128" />
    <constructor-arg name="port" value="6379" />
</bean>
<bean id="jedisClientSingle" class="guo.ping.taotao.rest.component.impl.JedisClientSingle" />
<!-- end -->

叢集版配置(含密碼):

<!--配置叢集版Redis-->

<!--redisPool配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <!--最大連線數 -->
    <property name="maxTotal" value="30"/>
    <!--最大空閒連線數 -->
    <property name="maxIdle" value="10"/>
    <!--每次釋放連線的最大數目 -->
    <property name="numTestsPerEvictionRun" value="1024"/>
    <!--釋放連線的掃描間隔(單位:毫秒) -->
    <property name="timeBetweenEvictionRunsMillis" value="30000"/>
    <!--連線最小空閒時間(單位:毫秒) -->
    <property name="minEvictableIdleTimeMillis" value="100000"/>
    <!--連線空閒多久後釋放,當空閒時間大於該值並且空閒連線大於最大空閒連線時直接釋放連線 -->
    <property name="softMinEvictableIdleTimeMillis" value="10000"/>
    <!--獲取連線時最大等待毫秒數,如果該值小於0,則阻塞不確定的時長,預設值-1 -->
    <property name="maxWaitMillis" value="1500"/>
    <!--在獲取連線時檢查連線有效性,預設為false -->
    <property name="testOnBorrow" value="false"/>
    <!--在連線空閒時檢查連線有效性,預設為false -->
    <property name="testWhileIdle" value="true"/>
    <!--連線耗盡是否阻塞,false代表拋異常,true代表阻塞直到超時,預設為true -->
    <property name="blockWhenExhausted" value="false"/>
</bean>

<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
    <constructor-arg>
        <set>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="${REDIS_HOST1}"/>
                <constructor-arg name="port" value="${REDIS_PORT1}"/>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="${REDIS_HOST2}"/>
                <constructor-arg name="port" value="${REDIS_PORT2}"/>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="${REDIS_HOST3}"/>
                <constructor-arg name="port" value="${REDIS_PORT3}"/>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="${REDIS_HOST4}"/>
                <constructor-arg name="port" value="${REDIS_PORT4}"/>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="${REDIS_HOST5}"/>
                <constructor-arg name="port" value="${REDIS_PORT5}"/>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value="${REDIS_HOST6}"/>
                <constructor-arg name="port" value="${REDIS_PORT6}"/>
            </bean>
        </set>
    </constructor-arg>
    <!--設定連線超時時間 -->
    <constructor-arg name="connectionTimeout" value="${REDIS_CONNECTION_TIMEOUT}"/>
    <!--設定網路通訊超時時間 -->
    <constructor-arg name="soTimeout" value="${REDIS_SO_TIMEOUT}"/>
    <!--設定叢集訪問密碼 -->
    <constructor-arg name="password" value="${REDIS_PASSWORD}"/>
    <!--設定最大重試次數 -->
    <constructor-arg name="maxAttempts" value="${REDIS_ATTEMPTS}"/>
    <!--設定jedisPool配置 -->
    <constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
</bean>

<bean id="jedisClientCluster" class="guo.ping.taotao.rest.component.impl.JedisClientCluster"/>
<!-- end -->

測試

/**
  * 測試spring配置
  * @throws Exception
  */
@Test
public void testJedisClientSpring() throws Exception {
    //建立一個spring容器
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-*.xml");
    //從容器中獲得JedisClient物件
    JedisClient jedisClient = applicationContext.getBean(JedisClient.class);
    //jedisClient操作redis
    jedisClient.set("cliet1", "1000");
    String string = jedisClient.get("cliet1");
    System.out.println(string);
}


七、實際專案之新增快取

​ 平時我們使用的關係型資料庫資料全部儲存在我們部署資料庫的機器的硬碟中,通常通過資料驅動來連結資料庫進行增刪改查。伺服器的讀寫效率是網站執行速度的重要條件,伺服器處理資料的速度與網站速度息息相關,而資料查詢、資料處理等和資料庫處理速度有關。

​ 其實,sql語句優化可以提高處理效率。但是如果網站的訪問量非常大的時候,我們的資料庫壓力就變大了。資料庫的連線池、處理資料的能力就會面臨很大的挑戰。此時就要使用高併發處理、負載均衡和分散式資料庫。但是這些會花費很大的人力、資金。

​ 快取就是在記憶體中對儲存的資料備份,當資料沒有發生本質變化的時候,我們避免資料的查詢操作直接連線資料庫,而是去記憶體中讀取資料,這樣就大大降低了資料庫的讀寫次數,而且從記憶體中讀資料的速度要比從資料庫查詢要快很多。

案例

Service層通過Dao方法查詢資料庫獲得資料:

@Service
public class ContentServiceImpl implements ContentService {

    @Autowired
    private TbContentMapper tbContentMapper;

    @Override
    public List<TbContent> getContentList(Long cid) {
        return tbContentMapper.getContentListByCategoryId(cid);
    }
}

新增快取:需要先查詢redis是否已經快取資料,若有則直接返回資料,沒有則需要訪問資料庫獲取資料並寫入Redis中。注意新增快取時不要影響正常的業務邏輯。

@Service
public class ContentServiceImpl implements ContentService {

    @Autowired
    private TbContentMapper tbContentMapper;
    @Autowired
    private JedisClient jedisClient;
    @Value("${REDIS_CONTENT_KEY}")
    private String REDIS_CONTENT_KEY;

    @Override
    public List<TbContent> getContentList(Long cid) {
        // 新增快取
        // 查詢資料庫之前先查詢快取,如果有直接返回
        try {
            //從redis中取快取資料
            String json = jedisClient.hget(REDIS_CONTENT_KEY, cid + "");
            if (!StringUtils.isBlank(json)) {
                return JsonUtils.jsonToList(json, TbContent.class);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        List<TbContent> tbContentList = tbContentMapper.getContentListByCategoryId(cid);

        // 返回結果之前,向快取中新增資料
        try {
            jedisClient.hset(REDIS_CONTENT_KEY, cid+"", JsonUtils.objectToJson(tbContentList));
        } catch (Exception e) {
            e.printStackTrace();
        }

        return tbContentList;
    }
}

結果

快取同步

當後臺(CMS內容管理系統)修改內容資訊後,只需要把redis中快取的資料刪除即可(後臺系統不直接操作redis資料庫)。我們可以在釋出一個服務,當CMS對資料庫資料資訊修改後,呼叫服務刪除redis快取即可。

釋出Restful服務:

@ResponseBody
@RequestMapping("/sync/{cid}")
public TaotaoResult syncContent(@PathVariable Long cid) {
    try {
        return contentService.syncContent(cid);
    } catch (Exception e) {
        return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
    }
}
@Controller
@RequestMapping("/content")
public class ContentController {

    @Autowired
    private ContentService contentService;
    private String REDIS_SYNC_BASE_URL = "http://localhost:8081/rest";
    private String REDIS_CONTENT_SYNC_URL = "content/sync/";

    // CMS系統新增內容
    @ResponseBody
    @RequestMapping("/save")
    public TaotaoResult insertContent(TbContent tbContent) {
        TaotaoResult result = contentService.insertContent(tbContent);
        // 呼叫服務
        HttpClientUtil.doGet(REDIS_SYNC_BASE_URL + REDIS_CONTENT_SYNC_URL + tbContent.getCategoryId());
        return result;
    }
}