一、Redis簡(jiǎn)述
Redis is an opensource (BSD licensed), in-memory data structure store, used as a database,cache and message broker. It supports data structures such as strings, hashes,lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-diskpersistence, and provides high availability via Redis Sentinel and automaticpartitioning with Redis Cluster. (借用官網(wǎng)描述)
二、使用訴求
在分布式應(yīng)用中我們每個(gè)模塊都會(huì)有一些緩存數(shù)據(jù)需要存放起來(lái),有三種方式,關(guān)系型數(shù)據(jù)庫(kù),如MYSQL,非關(guān)系型數(shù)據(jù)庫(kù)如NoSQL、Cloudant,JVM緩存,由于讀取速度的要求,我們棄用MYSQL,針對(duì)后兩種場(chǎng)景提一下所設(shè)想的兩種方案。
三、兩種方案對(duì)比
首先簡(jiǎn)述一下分布式系統(tǒng)的緩存同步痛點(diǎn)。我們項(xiàng)目是采用spring cloud進(jìn)行開發(fā)的,我的服務(wù)作為其中一個(gè)服務(wù),服務(wù)下屬多個(gè)示例,每個(gè)示例都是一模一樣的,包括功能和配置,這就要求服務(wù)亦或者實(shí)例是無(wú)狀態(tài)的,但是在實(shí)際開發(fā)中很難做到服務(wù)無(wú)狀態(tài),實(shí)例或多或少都會(huì)帶有一些緩存信息,這里不得不提一下經(jīng)典的CAP理論。CAP原則又稱CAP定理,指的是在一個(gè)分布式系統(tǒng)中,Consistency(數(shù)據(jù)一致性)、 Availability(服務(wù)可用性)、Partition tolerance(服務(wù)分區(qū)容錯(cuò)性),三者不可兼得,spring cloud設(shè)計(jì)者認(rèn)為分布式系統(tǒng)AP大于CP,所以spring cloud服務(wù)是不支持Consistency的,因此為了數(shù)據(jù)一致性的目標(biāo)我們有兩種選擇,要么基礎(chǔ)服務(wù)無(wú)狀態(tài),要么我們自己實(shí)現(xiàn)數(shù)據(jù)一致性。此處本應(yīng)點(diǎn)一下有狀態(tài)和無(wú)狀態(tài)的區(qū)別,但是篇幅有限,大家自行了解即可,給出一篇示例: http://www.itdecent.cn/p/51fee96f2e62;
http://dockone.io/article/3682。
前文贅述,為了解決數(shù)據(jù)一致性,我們提出兩種方案,具體闡述一下兩種方案。
方案一 實(shí)例同步
通過(guò)Eureka反向獲取服務(wù)注冊(cè)的所有實(shí)例,在spring 4中通過(guò)RestTemplate調(diào)用具體路徑實(shí)現(xiàn)服務(wù)同步,在spring 5中webFlux框架下也可使用WebClient實(shí)現(xiàn),通過(guò)請(qǐng)求返回信息判斷同步是否成功,此時(shí)為了同步可靠性,借鑒TCP三次握手實(shí)現(xiàn),流程如下:

實(shí)例獲取同步示例如下:
@Autowired
private DiscoveryClientdiscoveryClient;
/**
*
服務(wù)上線
* @return
*/
@RequestMapping(value ="basic/synchronization", method = RequestMethod.GET)
public SimpleResponsesynchronization() {
ListserviceInstanceList=discoveryClient.getInstances("EVO-BASIC");
RestTemplate restTemplate=newRestTemplate();
for (ServiceInstances:serviceInstanceList
) {
/**
* do sth
**/
}
returnSimpleResponse.successResponse("Synchronization success!");
}
方案二Redis
通過(guò)Redis實(shí)現(xiàn),將信息保存在Redis中,所有實(shí)例訪問(wèn)同一個(gè)Redis-Server。Redis提供了簡(jiǎn)單的事務(wù)機(jī)制,通過(guò)事務(wù)機(jī)制可以有效保證在高井發(fā)的場(chǎng)景下數(shù)據(jù)的一致性。同時(shí)Redis提供了流水線技術(shù),極大地提升了Redis命令執(zhí)行效率。Spring對(duì)Redis的支持算是十分友好的。

到這里感覺(jué)已經(jīng)很簡(jiǎn)單了,但是真正的踩坑記才剛剛開始,我們從序列化,事務(wù)和流水線三方面進(jìn)行踩坑記錄。依賴首先說(shuō)明我們使用spring-data-redis,依賴為:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
要注意的是默認(rèn)使用的是lettuce而不是Jedis,如果你要使用Jedis,請(qǐng)把lettuce剔除,增加Jedis的依賴,二者的區(qū)別自行百度。
序列化
先從RedisTemplate的序列化開始說(shuō)起。首先為什么要采用合適的序列化器,Redis默認(rèn)使用的是JdkSerializationRedisSerializer,如果我們的key采用默認(rèn)的序列化器,序列化過(guò)程如圖所示:

由圖可知,在Redis中將會(huì)把key變成一個(gè)二進(jìn)制串,結(jié)果就是你使用原先的key進(jìn)行查找時(shí)查找失敗。RedisTemplate中的序列化器屬性如圖所示:

spring-data-redis的序列化類有下面這幾個(gè):
GenericToStringSerializer:可以將任何對(duì)象泛化為字符串并序列化
Jackson2JsonRedisSerializer:跟JacksonJsonRedisSerializer實(shí)際上是一樣的
JacksonJsonRedisSerializer:序列化object對(duì)象為json字符串
JdkSerializationRedisSerializer:序列化java對(duì)象(被序列化的對(duì)象必須實(shí)現(xiàn)Serializable接口
StringRedisSerializer:簡(jiǎn)單的字符串序列化
GenericToStringSerializer:類似StringRedisSerializer的字符串序列化
GenericJackson2JsonRedisSerializer:類似Jackson2JsonRedisSerializer,但使用時(shí)構(gòu)造函數(shù)不用特定的類參考以上序列化,自定義序列化類;
這里給出大家一個(gè)使用示例,即Basic中的序列化器采用:
@Bean(name="Evo_Basic_Redis")
public RedisTemplate objectRedisTemplate(){
RedisTemplate template=new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializerjackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = newObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializerstringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
template.setEnableTransactionSupport(false);
return template;
}
至此,序列化使用完成,下面開始闡述Redis事務(wù)。
Redis事務(wù)
Redis事務(wù)使用是很方便的,關(guān)鍵在于
template.setEnableTransactionSupport(true);
當(dāng)把這個(gè)開關(guān)打開以后,在方法中調(diào)用RedisTemplate時(shí),只需要在方法上加@Transactional注解即可,值得注意的是,Redis沒(méi)有自己的事務(wù)管理器,因此需要和MYSQL共用同一個(gè)事務(wù)控制器,慶幸的是我們?cè)谂渲肑DBC的是一般會(huì)配置PlatformTransactionManager,這一步我們可以忽略。
@Bean
public PlatformTransactionManagertransactionManager() throws SQLException {
return newDataSourceTransactionManager(dataSource());
}
當(dāng)我們打開了Redis事務(wù)支持后,在標(biāo)明@Transactional注解的方法中調(diào)用RedisTemplate時(shí),將會(huì)把Redis命令放于一個(gè)隊(duì)列中,發(fā)生異常時(shí),可以和MYSQL命令一起回滾,值得注意的兩個(gè)坑說(shuō)明:
1、在未用@Transactional注解標(biāo)明的方法中調(diào)用RedisTemplate后,RedisTemplate連接不會(huì)主動(dòng)釋放,需要手動(dòng)釋放連接,原因是@Transactional在方法執(zhí)行時(shí)會(huì)遍歷得到每一個(gè)TransactionSynchronization,然后調(diào)用它的afterCompletion方法,afterCompletion方法源碼如下:
publicvoid afterCompletion(int status) {
try {
switch (status) {
//如果事務(wù)正常,最終提交事務(wù)
case TransactionSynchronization.STATUS_COMMITTED:
connection.exec();
break;
//如果有異常,事務(wù)回滾
case TransactionSynchronization.STATUS_ROLLED_BACK:
case TransactionSynchronization.STATUS_UNKNOWN:
default:
connection.discard();
}
} finally {
if (log.isDebugEnabled()) {
log.debug("Closing bound connection after transaction completed with "+ status);
}
connHolder.setTransactionSyncronisationActive(false);
//關(guān)閉連接
connection.close();
//從當(dāng)前線程釋放連接
TransactionSynchronizationManager.unbindResource(factory);
}
}
我們可以看到在調(diào)用結(jié)束后會(huì)主動(dòng)釋放連接,但是在未用@Transactional注解標(biāo)明的方法中調(diào)用后就需要我們手動(dòng)釋放了,釋放連接代碼示例:
/**
*普通緩存獲取
* @param key 鍵
* @return value
*/
public Object get(String key){
Object value = redisTemplate.opsForValue().get(key);
TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
return value;
}
未釋放連接的原因如下:
public static void releaseConnection(RedisConnection conn, RedisConnectionFactoryfactory) {
if (conn == null) {
return;
}
RedisConnectionUtils.RedisConnectionHolder connHolder =(RedisConnectionUtils.RedisConnectionHolder)TransactionSynchronizationManager.getResource(factory);
/**
*可以獲取到connHolder 但是connHolder.isTransactionSyncronisationActive()卻是false,
*因?yàn)橹敖壎ㄟB接的時(shí)候,并沒(méi)有在一個(gè)事務(wù)中,連接綁定了,但是isTransactionSyncronisationActive屬性
*并沒(méi)有給值,可以看一下第四步potentiallyRegisterTransactionSynchronisation中的代碼,其實(shí)是沒(méi)有執(zhí)行的,
*所以 isTransactionSyncronisationActive 的默認(rèn)值是false
**/
if (connHolder != null &&connHolder.isTransactionSyncronisationActive()) {
if (log.isDebugEnabled()) {
log.debug("Redis Connection will be closed when transactionfinished.");
}
return;
}
// release transactional/read-only and non-transactional/non-bound connections.
// transactional connections for read-only transactions get no synchronizerregistered
//第一個(gè)條件判斷為true 但是第二個(gè)條件判斷為false 不是一個(gè)只讀事務(wù),所以u(píng)nbindConnection(factory) 代碼沒(méi)有執(zhí)行
if (isConnectionTransactional(conn, factory)&&TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
unbindConnection(factory);
//然后 else if 也是返回false 因?yàn)閕sConnectionTransactional(conn, factory)返回的是true 內(nèi)部代碼判斷連接這個(gè)連接和 線程中綁定的連接是不是同一個(gè),是同一個(gè) 由于前面加了一個(gè) ! 號(hào) 所以結(jié)果為false
} else if (!isConnectionTransactional(conn, factory)) {
if (log.isDebugEnabled()) {
log.debug("Closing Redis Connection");
}
conn.close();
}
}
第一個(gè)坑闡述完畢,我們?cè)僦v第二個(gè)坑
2、Redis事務(wù)和MYSQL事務(wù)不同,Redis事務(wù)在執(zhí)行時(shí)不會(huì)立即執(zhí)行命令,而是放到隊(duì)列中,延遲執(zhí)行,因此如果你這樣使用的話:
@Transactional
public Long getCurrentTenantId() {
Object tenantIdStr=context.redisTemplate.opsForValue().get(Redis_Tenant_Id);
return Long.valueOf(tenantIdStr.toString());
}
那么恭喜你,你查詢到值一定是NULL,因?yàn)檫@條命令不會(huì)被執(zhí)行,Basic采用的用法是配置兩個(gè)RedisTemplet,如下:
@Bean(name="Evo_Basic_Redis")
public RedisTemplateobjectRedisTemplate(){
RedisTemplate template=new RedisTemplate<>();
template.setConnectionFactory(factory);
/**
* do sth
**/
template.setEnableTransactionSupport(false);
return template;
}
@Bean(name="Evo_Basic_Redis_Write")
public RedisTemplate objectWriteRedisTemplate(){
RedisTemplate template=new RedisTemplate<>();
template.setConnectionFactory(factory);
/**
* do sth
**/
template.setEnableTransactionSupport(true);
return template;
}
一個(gè)關(guān)閉事務(wù)支持用來(lái)執(zhí)行讀操作,另一個(gè)打開事務(wù)支持執(zhí)行寫操作。
Redis流水線
我們可能一直沒(méi)有思考過(guò)如下代碼的執(zhí)行過(guò)程:
redisTemplateopsForValue () .set (” keyl”,”valuel” ) ;
redisTemplate opsForHash( .put (”hash ”,”field ", "value");
看著在一個(gè)方法中執(zhí)行,但實(shí)際上它們是在兩個(gè)連接中完成的,即執(zhí)行完第一個(gè)命令后redisTemplate會(huì)斷開連接,執(zhí)行第二條命令時(shí)再申請(qǐng)新的連接,如果想深挖的話可以研究一下Redis的連接池。這樣顯然存在資源浪費(fèi)的問(wèn)題。為了克服這個(gè)問(wèn)題,Spring 為我們提供了RedisCallback和SessionCallback兩個(gè)接口,它們的作用是讓RedisTemplate進(jìn)行回調(diào),通過(guò)他們可以在同一條連接下執(zhí)行多Redis命令。其中SessionCallback提供了良好的封裝,對(duì)于開發(fā)者比較友好,因此在實(shí)際的開發(fā)中應(yīng)該優(yōu)先選擇使用它;相對(duì)而言RedisCallback接口比較底層,需要處理的內(nèi)容也比較多,可讀性較差,所以非必要的時(shí)候盡量不選擇使用它。代碼示例如圖所示:

而流水線接口的調(diào)用類似于excute,調(diào)用方法為executePipelined,二者的區(qū)別我決定采用官網(wǎng)原文來(lái)描述,
Redis provides support forpipelining,which involves sending multiple commands to the server without waiting for thereplies and then reading the replies in a single step. Pipelining can improveperformance when you need to send several commands in a row, such as addingmany elements to the same List.Spring Data Redis provides severalRedisTemplatemethodsfor executing commands in a pipeline. If you do not care about the results ofthe pipelined operations, you can use the standardexecutemethod, passingtruefor thepipelineargument. TheexecutePipelinedmethods run theprovidedRedisCallbackorSessionCallbackin a pipeline andreturn the results, as shown in the following example:
//popa specified number of items from a queue
List results = stringRedisTemplate.executePipelined(
new RedisCallback() {
public ObjectdoInRedis(RedisConnection connection) throws DataAccessException {
StringRedisConnectionstringRedisConn = (StringRedisConnection)connection;
for(int i=0; i< batchSize; i++){
stringRedisConn.rPop("myqueue");
}
return null;
}
});
關(guān)于這三塊的描述感覺(jué)自己還是很多沒(méi)有講出來(lái),更多細(xì)節(jié)還是希望大家通過(guò)源碼或者官網(wǎng)doc進(jìn)行探索,官網(wǎng)地址為:https://docs.spring.io/spring-data/redis/docs/2.1.6.RELEASE/reference/html/#tx.spring
四、基礎(chǔ)服務(wù)Redis使用流程
Basic服務(wù)的更新流程如下,為了保證數(shù)據(jù)一致性,我們覺(jué)得更新必須保證;兩個(gè)操作都正常完成,否則不予更新,流程圖如下:

查詢流程如下:

我們?cè)谄渲屑尤肓瞬樵僊YSQL的流程,主要是為了防止Redis_Server發(fā)生異常的情況,保證系統(tǒng)的可用性即服務(wù)的Availability。至于系統(tǒng)啟動(dòng)時(shí)對(duì)Redis服務(wù)器的同步流程則不做贅述。