基礎(chǔ)服務(wù)中Redis的使用

一、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),流程如下:

image

實(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的支持算是十分友好的。

image

到這里感覺(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ò)程如圖所示:

image

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

image
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í)候盡量不選擇使用它。代碼示例如圖所示:


1556088333952-cc2c12f1-ee8b-40d7-a170-0ca394e6eae8.jpeg

而流水線接口的調(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è)操作都正常完成,否則不予更新,流程圖如下:


1556093267581-5f56794a-4e53-4934-a80f-7c5b1d2fc1e7.jpeg

查詢流程如下:


1556093417169-2215da82-41f6-4584-b113-de767236fcc5.jpeg

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容