基础服务中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,依赖为:



    org.springframework.boot

    spring-boot-starter-data-redis



要注意的是默认使用的是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服务器的同步流程则不做赘述。

你可能感兴趣的:(基础服务中Redis的使用)