Day617.SpringData常见错误 -Spring编程常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day617.SpringData常见错误 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。

SpringData常见错误

Spring已经为我们继承好了去操作对应数据源的方式:SpringData,如下名:

Spring Data Commons
Spring Data JPA
Spring Data KeyValue
Spring Data LDAP
Spring Data MongoDB
Spring Data Redis
Spring Data REST
Spring Data for Apache Cassandra
Spring Data for Apache Geode
Spring Data for Apache Solr
Spring Data for Pivotal GemFire
Spring Data Couchbase (community module)
Spring Data Elasticsearch (community module)
Spring Data Neo4j (community module)

而在你使用这些各种各样的数据库时,难免会遇到问题。下面列举举例3个典型的问题。


一、读与取的一致性问题

当使用 Spring Data Redis 时,我们有时候会在项目升级的过程中,发现存储后的数据有读取不到的情况;

另外,还会出现解析出错的情况。这里我们不妨直接写出一个错误案例来模拟下:

@SpringBootApplication
public class SpringdataApplication 
    SpringdataApplication(RedisTemplate redisTemplate,StringRedisTemplate stringRedisTemplate)
        String key = "mykey";
        stringRedisTemplate.opsForValue().set(key, "myvalue");

        Object valueGotFromStringRedisTemplate = stringRedisTemplate.opsForValue().get(key);
        System.out.println(valueGotFromStringRedisTemplate);

        Object valueGotFromRedisTemplate = redisTemplate.opsForValue().get(key);
        System.out.println(valueGotFromRedisTemplate);
    

    public static void main(String[] args) 
        SpringApplication.run(SpringdataApplication.class, args);
    

在上述代码中,我们使用了 Redis 提供的两种 Template,一种 RedisTemplate,一种 stringRedisTemplate。

但是当我们使用后者去存一个数据后,你会发现使用前者是取不到对应的数据的。输出结果如下:

myvalue
null

此时你可能会想,这个问题不是很简单么?肯定是这两个 Template 不同导致的。

你可以试想一下,如果我们是不同的开发者开发不同的项目呢?一个项目只负责存储,另外一个项目只负责读取,两个项目之间缺乏沟通和协调。

这种问题在实际工作中并不稀奇,接下来我们就了解下这个问题背后的深层次原因。


首先,我们需要认清一个现实:我们不可能直接将数据存取到 Redis 中,毕竟一些数据是一个对象型

例如 String,甚至是一些自定义对象。我们需要在存取前对数据进行序列化或者反序列化操作。

具体到我们的案例而言,当带着 key 去存取数据时,它会执行 AbstractOperations#rawKey,使得在执行存储 key-value 到 Redis,或从 Redis 读取数据之前,对 key 进行序列化操作:

byte[] rawKey(Object key) 
   Assert.notNull(key, "non null key required");
   if (keySerializer() == null && key instanceof byte[]) 
      return (byte[]) key;
   
   return keySerializer().serialize(key);

从上述代码可以看出,假设存在 keySerializer,则利用它将 key 序列化stringRedisTemplate,对于StringRedisSerializer来说,它指定的其实是 StringRedisSerializer。具体实现如下:

public class StringRedisSerializer implements RedisSerializer<String> 
   private final Charset charset;
   
   @Override
   public byte[] serialize(@Nullable String string) 
      return (string == null ? null : string.getBytes(charset));
   
 

而如果我们使用的是 RedisTemplate,则使用的是 JDK 序列化,具体序列化操作参考下面的实现:

public class JdkSerializationRedisSerializer implements RedisSerializer<Object> 
 
   @Override
   public byte[] serialize(@Nullable Object object) 
      if (object == null) 
         return SerializationUtils.EMPTY_ARRAY;
      
      try 
         return serializer.convert(object);
       catch (Exception ex) 
         throw new SerializationException("Cannot serialize", ex);
      
   

很明显,上面对 key 的处理,采用的是 JDK 的序列化,最终它调用的方法如下:

public interface Serializer<T> 
    void serialize(T var1, OutputStream var2) throws IOException;

    default byte[] serializeToByteArray(T object) throws IOException 
        ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
        this.serialize(object, out);
        return out.toByteArray();
    

你可以直接将"mykey"这个字符串分别用上面提到的两种不同的序列化器进行序列化,那 必然对应序列化和反序列化的结果就会不同

这也就解释了为什么它们不能读取到"mykey"设置的"myvalue"。

至于它们是如何指定 RedisSerializer 的,我们可以以 StringRedisSerializer 为例简单看下。查看下面的代码,它是 StringRedisSerializer 的构造器,在构造器中,它直接指定了 KeySerializer 为 RedisSerializer.string():

public class StringRedisTemplate extends RedisTemplate<String, String> 
   public StringRedisTemplate() 
      setKeySerializer(RedisSerializer.string());
      setValueSerializer(RedisSerializer.string());
      setHashKeySerializer(RedisSerializer.string());
      setHashValueSerializer(RedisSerializer.string());
   

其中 RedisSerializer.string() 最终返回的实例如下:

public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);

因为不同的templete对应使用不同的序列化和反序列化器,那对应的结果也会不同


那如何解决呢?

要解决这个问题,非常简单,就是检查自己所有的数据操作,是否使用了相同的 RedisTemplate,就是相同,也要检查所指定的各种 Serializer 是否完全一致,否则就会出现各式各样的错误。


二、配置默认值的错误问题

当我们使用 Spring Data 时,就像其他 Spring 模块一样,为了应对大多数场景或者方便用户使用,Spring Data 都有很多默认值,但是不见得所有的默认值都是最合适的。

例如在一个依赖 Cassandra 的项目中,有时候我们在 写入数据之后,并不能立马读到写入的数据

这里面可能是什么原因呢?这种错误并没有什么报错,一切都是正常的,只是读取不到数据而已。


当我们什么都不去配置,而是直接使用 Spring Data Cassandra 来操作时,我们实际依赖了 Cassandra driver 内部的配置文件,具体目录如下:

.m2\\repository\\com\\datastax\\oss\\java-driver-core\\4.6.1\\java-driver-core-4.6.1.jar!\\reference.conf

我们可以看下它存在很多默认的配置,其中一项很重要的配置是 Consistency,在 driver 中默认为 LOCAL_ONE,具体如下:

basic.request 
  # The consistency level.
  #
  # Required: yes
  # Modifiable at runtime: yes, the new value will be used for requests issued after the change.
  # Overridable in a profile: yes
  consistency = LOCAL_ONE
 
//省略其他非关键配置 

所以当我们去执行读写操作时,我们都会使用 LOCAL_ONE。参考下面的运行时配置调试截图:


LOCAL_ONE:配置简单来说类型redis中多节点处理的情况下,只用一个节点处理,请一个处理到另一台机子就会导致拿到不同机子的值

实际上,当你第一次学习和应用 Cassandra 时,你一定会先只装一台机器玩玩。

此时,设置为 LOCAL_ONE 其实是最合适的,也正因为只有一台机器,你的读写都只能命中一台。这样的话,读写是完全没有问题的。但是产线上的 Cassandra 大多都是多数据中心多节点的,备份数大于 1。所以读写都用 LOCAL_ONE 就会出现问题。


解决方案

对应上面单节点的LOCAL_ONE ,肯定不可取。

所以配置多节点LOCAL_QUORUM

@Override
protected SessionBuilderConfigurer getSessionBuilderConfigurer() 
    return cqlSessionBuilder -> 
        DefaultProgrammaticDriverConfigLoaderBuilder defaultProgrammaticDriverConfigLoaderBuilder = new DefaultProgrammaticDriverConfigLoaderBuilder();
        driverConfigLoaderBuilderCustomizer().customize(defaultProgrammaticDriverConfigLoaderBuilder);
        cqlSessionBuilder.withConfigLoader(defaultProgrammaticDriverConfigLoaderBuilder.build());
        return cqlSessionBuilder;
    ;


@Bean
public DriverConfigLoaderBuilderCustomizer driverConfigLoaderBuilderCustomizer() 
    return loaderBuilder -> loaderBuilder
            .withString(REQUEST_CONSISTENCY, ConsistencyLevel.LOCAL_QUORUM.name())

这里我们将一致性级别从 LOCAL_ONE 改成了 LOCAL_QUARM,更符合我们的实际产品部署和应用情况。


三、冗余的 Session问题

有时候,我们使用 Spring Data 做连接时,会比较在意我们的内存占用。

例如我们使用 Spring Data Cassandra 操作 Cassandra 时,可能会发现类似这样的问题:

Spring Data Cassandra 在连接 Cassandra 之后,会获取 Cassandra 的 Metadata 信息,这个内存占用量是比较大的,因为它存储了数据的 Token Range 等信息。

如上图所示,在我们的应用中,占用 40M 以上已经不少了,但问题是为什么有 4 个占用 40 多 M 呢?难道不是只建立一个连接么?


我们只要找到获取 Metadata 的地方加个断点,然后找出触发获取的源头即可。

现在我们定义一个 MyService 类,当它构造时,会输出它的名称信息:

public class MyService 
    public MyService(String name)
        System.err.println(name);
    

然后我们定义两个 Configuration 类,同时让它们是继承关系,其中父 Configuration 命名如下:

@Configuration
public class BaseConfig 
    @Bean
    public MyService service()
        return new MyService("myservice defined from base config");
    

子 Configuration 命名如下:

@Configuration
public class Config extends BaseConfig 
    @Bean
    public MyService service()
        return new MyService("myservice defined from config");
    

子类的 service() 实现覆盖了父类对应的方法。最后,我们书写一个启动程序:

@SpringBootApplication
public class Application 
    public static void main(String[] args) 
        SpringApplication.run(Application.class, args);
    

为了让程序启动,我们不能将 BaseConfig 和 Config 都放到 Application 的扫描范围。我们可以按如下结构组织代码:

@Configuration
public class Config extends BaseConfig 
    @Bean
    public MyService service2()
        return new MyService("myservice defined from config");
    

经过上述的不小心修改,再次运行程序,你会发现有 2 个 MyService 的 Bean 产生:

myservice defined from config
myservice defined from base config

@bean方式注入时,spring本身注入bean的时候也会默认去拿会有方法名作为bean的名字。

说到这里你可能想到一个造成内存翻倍的原因。我们去查看案例程序的代码,可能会发现存在这样的问题:

@Configuration
@EnableCassandraRepositories
public class CassandraConfig extends AbstractCassandraConfiguration
     @Bean
     @Primary
     public CqlSessionFactoryBean session() 
         log.info("init session");
         CqlSessionFactoryBean cqlSessionFactoryBean = new CqlSessionFactoryBean();
         //省略其他非关键代码    
         return cqlSessionFactoryBean ;
     
     //省略其他非关键代码

CassandraConfig 继承于 AbstractSessionConfiguration,它已经定义了一个 CqlSessionFactoryBean,代码如下:

@Configuration
public abstract class AbstractSessionConfiguration implements BeanFactoryAware
    @Bean
    public CqlSessionFactoryBean cassandraSession() 
       CqlSessionFactoryBean bean = new CqlSessionFactoryBean();
       bean.setContactPoints(getContactPoints());
       //省略其他非关键代码
        return bean;
    
    //省略其他非关键代码

而比较这两段的 CqlSessionFactoryBean 的定义方法,你会发现它们的方法名是不同的:

cassandraSession()
session()


解决方案

@Configuration
@EnableCassandraRepositories
public class CassandraConfig extends AbstractCassandraConfiguration
     @Bean
     @Primary
     public CqlSessionFactoryBean cassandraSession() 
        //省略其他非关键代码
     
     //省略其他非关键代码

这里我们将原来的方法名 session 改成 cassandraSession

不过你可能会有一个疑问,这里不就是翻倍了么?但也不至于四倍啊。实际上,这是因为使用 Spring Data Cassandra 会创建两个 Session,它们都会获取 metadata。

具体可参考代码 CqlSessionFactoryBean#afterPropertiesSet

@Override
public void afterPropertiesSet() 

   CqlSessionBuilder sessionBuilder = buildBuilder();
   // system session 的创建 
   this.systemSession = buildSystemSession(sessionBuilder);

   initializeCluster(this.systemSession);
   // normal session 的创建
   this.session = buildSession(sessionBuilder);

   executeCql(getStartupScripts().stream(), this.session);
   performSchemaAction();

   this.systemSession.refreshSchema();
   this.session.refreshSchema();


三、总结

  • 一定要注意一致性,例如读写的序列化&反序列化方法需要一致
  • 一定要重新检查下所有的默认配置是什么,默认配置是否符合当前的需求,例如在 Spring Data Cassandra 中,默认的一致性级别在大多情况下都不适合;
  • 如果你自定义自己的 Session, 一定要避免冗余的 Session 产生。注意父类的注入方法的名字是否一致

以上是关于Day617.SpringData常见错误 -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章

Day610.SpringWebHeader解析常见错误 -Spring编程常见错误

Day616.SpringException常见错误 -Spring常见编程错误

Day609.SpringWebURL解析常见错误 -Spring编程常见错误

Day621.Spring Test 常见错误 -Spring编程常见错误

Day614.SpringWebFilter常见错误② -Spring编程常见错误

Day613.SpringWebFilter常见错误① -Spring编程常见错误