11、Spring技術棧-整合Redis,通過Redis的Master-Slave實現快取資料讀寫分離
1、Redis主從複製(Master-Salve Replication)簡介
Redis 支援簡單且易用的主從複製(master-slave replication)功能, 該功能可以讓從伺服器(slave server)成為主伺服器(master server)的精確複製品。
以下是關於 Redis 複製功能的幾個重要方面:
Redis 使用非同步複製。 從 Redis 2.8 開始, 從伺服器會以每秒一次的頻率向主伺服器報告複製流(replication stream)的處理進度。
一個主伺服器可以有多個從伺服器。
不僅主伺服器可以有從伺服器, 從伺服器也可以有自己的從伺服器, 多個從伺服器之間可以構成一個圖狀結構。
複製功能不會阻塞主伺服器: 即使有一個或多個從伺服器正在進行初次同步, 主伺服器也可以繼續處理命令請求。
複製功能也不會阻塞從伺服器: 只要在 redis.conf 檔案中進行了相應的設定, 即使從伺服器正在進行初次同步, 伺服器也可以使用舊版本的資料集來處理命令查詢。
不過, 在從伺服器刪除舊版本資料集並載入新版本資料集的那段時間內, 連線請求會被阻塞。
你還可以配置從伺服器, 讓它在與主伺服器之間的連線斷開時, 向客戶端傳送一個錯誤。
複製功能可以單純地用於資料冗餘(data redundancy), 也可以通過讓多個從伺服器處理只讀命令請求來提升擴充套件性(scalability): 比如說, 繁重的 SORT 命令可以交給附屬節點去執行。
可以通過複製功能來讓主伺服器免於執行持久化操作: 只要關閉主伺服器的持久化功能, 然後由從伺服器去執行持久化操作即可。
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;
}
}