基础服务中Redis的使用

一、Redis简述

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. (借用官网描述)

二、使用诉求

在分布式应用中我们每个模块都会有一些缓存数据需要存放起来,有三种方式,关系型数据库,如MYSQL,非关系型数据库如NoSQL、Cloudant,JVM缓存,由于读取速度的要求,我们弃用MYSQL,针对后两种场景提一下所设想的两种方案。

三、两种方案对比

首先简述一下分布式系统的缓存同步痛点。我们项目是采用spring cloud进行开发的,我的服务作为其中一个服务,服务下属多个示例,每个示例都是一模一样的,包括功能和配置,这就要求服务亦或者实例是无状态的,但是在实际开发中很难做到服务无状态,实例或多或少都会带有一些缓存信息,这里不得不提一下经典的CAP理论。CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(数据一致性)、 Availability(服务可用性)、Partition tolerance(服务分区容错性),三者不可兼得,spring cloud设计者认为分布式系统AP大于CP,所以spring cloud服务是不支持Consistency的,因此为了数据一致性的目标我们有两种选择,要么基础服务无状态,要么我们自己实现数据一致性。此处本应点一下有状态和无状态的区别,但是篇幅有限,大家自行了解即可,给出一篇示例: https://www.jianshu.com/p/51fee96f2e62
http://dockone.io/article/3682
前文赘述,为了解决数据一致性,我们提出两种方案,具体阐述一下两种方案。

方案一 实例同步

通过Eureka反向获取服务注册的所有实例,在spring 4中通过RestTemplate调用具体路径实现服务同步,在spring 5中webFlux框架下也可使用WebClient实现,通过请求返回信息判断同步是否成功,此时为了同步可靠性,借鉴TCP三次握手实现,流程如下:

image

实例获取同步示例如下:


@Autowired

    private DiscoveryClientdiscoveryClient;

    /**

     *

服务上线

     * @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

通过Redis实现,将信息保存在Redis中,所有实例访问同一个Redis-Server。Redis提供了简单的事务机制,通过事务机制可以有效保证在高井发的场景下数据的一致性。同时Redis提供了流水线技术,极大地提升了Redis命令执行效率。Spring对Redis的支持算是十分友好的。

image

到这里感觉已经很简单了,但是真正的踩坑记才刚刚开始,我们从序列化,事务和流水线三方面进行踩坑记录。依赖首先说明我们使用spring-data-redis,依赖为:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

要注意的是默认使用的是lettuce而不是Jedis,如果你要使用Jedis,请把lettuce剔除,增加Jedis的依赖,二者的区别自行百度。

序列化

先从RedisTemplate的序列化开始说起。首先为什么要采用合适的序列化器,Redis默认使用的是JdkSerializationRedisSerializer,如果我们的key采用默认的序列化器,序列化过程如图所示:

image

由图可知,在Redis中将会把key变成一个二进制串,结果就是你使用原先的key进行查找时查找失败。RedisTemplate中的序列化器属性如图所示:

image
spring-data-redis的序列化类有下面这几个:
  • GenericToStringSerializer:可以将任何对象泛化为字符串并序列化

  • Jackson2JsonRedisSerializer:跟JacksonJsonRedisSerializer实际上是一样的

  • JacksonJsonRedisSerializer:序列化object对象为json字符串

  • JdkSerializationRedisSerializer:序列化java对象(被序列化的对象必须实现Serializable接口

  • StringRedisSerializer:简单的字符串序列化

  • GenericToStringSerializer:类似StringRedisSerializer的字符串序列化

  • GenericJackson2JsonRedisSerializer:类似Jackson2JsonRedisSerializer,但使用时构造函数不用特定的类参考以上序列化,自定义序列化类;

这里给出大家一个使用示例,即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事务。

Redis事务

Redis事务使用是很方便的,关键在于

template.setEnableTransactionSupport(true);

当把这个开关打开以后,在方法中调用RedisTemplate时,只需要在方法上加@Transactional注解即可,值得注意的是,Redis没有自己的事务管理器,因此需要和MYSQL共用同一个事务控制器,庆幸的是我们在配置JDBC的是一般会配置PlatformTransactionManager,这一步我们可以忽略。

 @Bean

 public PlatformTransactionManagertransactionManager() throws SQLException {

     return newDataSourceTransactionManager(dataSource());

 }

当我们打开了Redis事务支持后,在标明@Transactional注解的方法中调用RedisTemplate时,将会把Redis命令放于一个队列中,发生异常时,可以和MYSQL命令一起回滚,值得注意的两个坑说明:

1、在未用@Transactional注解标明的方法中调用RedisTemplate后,RedisTemplate连接不会主动释放,需要手动释放连接,原因是@Transactional在方法执行时会遍历得到每一个TransactionSynchronization,然后调用它的afterCompletion方法,afterCompletion方法源码如下:

publicvoid afterCompletion(int status) {

try {

  switch (status) {
    //如果事务正常,最终提交事务
  case TransactionSynchronization.STATUS_COMMITTED:
      connection.exec();
      break;
  //如果有异常,事务回滚
  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);
  //关闭连接
  connection.close();
  //从当前线程释放连接
  TransactionSynchronizationManager.unbindResource(factory);
  }
}

我们可以看到在调用结束后会主动释放连接,但是在未用@Transactional注解标明的方法中调用后就需要我们手动释放了,释放连接代码示例:

/**
*普通缓存获取
 * @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,
  *因为之前绑定连接的时候,并没有在一个事务中,连接绑定了,但是isTransactionSyncronisationActive属性
  *并没有给值,可以看一下第四步potentiallyRegisterTransactionSynchronisation中的代码,其实是没有执行的,
  *所以 isTransactionSyncronisationActive 的默认值是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

//第一个条件判断为true 但是第二个条件判断为false 不是一个只读事务,所以unbindConnection(factory) 代码没有执行
  if (isConnectionTransactional(conn, factory)&&TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
    unbindConnection(factory);
//然后 else if 也是返回false  因为isConnectionTransactional(conn, factory)返回的是true 内部代码判断连接这个连接和 线程中绑定的连接是不是同一个,是同一个 由于前面加了一个 ! 号 所以结果为false
  } else if (!isConnectionTransactional(conn, factory)) {
    if (log.isDebugEnabled()) {
      log.debug("Closing Redis Connection");
    }
    conn.close();
    }
 }

第一个坑阐述完毕,我们再讲第二个坑

2、Redis事务和MYSQL事务不同,Redis事务在执行时不会立即执行命令,而是放到队列中,延迟执行,因此如果你这样使用的话:

@Transactional
public Long getCurrentTenantId() {

  Object tenantIdStr=context.redisTemplate.opsForValue().get(Redis_Tenant_Id);
  return Long.valueOf(tenantIdStr.toString());
}

那么恭喜你,你查询到值一定是NULL,因为这条命令不会被执行,Basic采用的用法是配置两个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;
 }

一个关闭事务支持用来执行读操作,另一个打开事务支持执行写操作。

Redis流水线

我们可能一直没有思考过如下代码的执行过程:

redisTemplateopsForValue () .set (” keyl”,”valuel” ) ;
redisTemplate opsForHash( .put (”hash ”,”field ", "value");

看着在一个方法中执行,但实际上它们是在两个连接中完成的,即执行完第一个命令后redisTemplate会断开连接,执行第二条命令时再申请新的连接,如果想深挖的话可以研究一下Redis的连接池。这样显然存在资源浪费的问题。为了克服这个问题,Spring 为我们提供了RedisCallback和SessionCallback两个接口,它们的作用是让RedisTemplate进行回调,通过他们可以在同一条连接下执行多Redis命令。其中SessionCallback提供了良好的封装,对于开发者比较友好,因此在实际的开发中应该优先选择使用它;相对而言RedisCallback接口比较底层,需要处理的内容也比较多,可读性较差,所以非必要的时候尽量不选择使用它。代码示例如图所示:


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

而流水线接口的调用类似于excute,调用方法为executePipelined,二者的区别我决定采用官网原文来描述,
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;
  }
});

关于这三块的描述感觉自己还是很多没有讲出来,更多细节还是希望大家通过源码或者官网doc进行探索,官网地址为:https://docs.spring.io/spring-data/redis/docs/2.1.6.RELEASE/reference/html/#tx.spring

四、基础服务Redis使用流程

Basic服务的更新流程如下,为了保证数据一致性,我们觉得更新必须保证;两个操作都正常完成,否则不予更新,流程图如下:


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

查询流程如下:


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

我们在其中加入了查询MYSQL的流程,主要是为了防止Redis_Server发生异常的情况,保证系统的可用性即服务的Availability。至于系统启动时对Redis服务器的同步流程则不做赘述。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容