首页 > 其他分享 >Spring Data 常见错误

Spring Data 常见错误

时间:2023-05-26 11:22:25浏览次数:38  
标签:错误 Spring 我们 key Data public Cassandra

案例 1:注意读与取的一致性

当使用 Spring Data Redis 时,我们有时候会在项目升级的过程中,发现存储后的数据有读取不到的情况;另外,还会出现解析出错的情况。这里我们不妨直接写出一个错误案例来模拟下:

https://www.java567.com,搜"spring")

 @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 不同导致的。

没错,这是一个极度简化的案例,我们的学习目的是举一反三。你可以试想一下,如果我们是不同的开发者开发不同的项目呢?一个项目只负责存储,另外一个项目只负责读取,两个项目之间缺乏沟通和协调。这种问题在实际工作中并不稀奇,接下来我们就了解下这个问题背后的深层次原因。

案例解析

要了解这个问题,需要我们对 Spring Data Redis 的操作流程有所了解。

首先,我们需要认清一个现实:我们不可能直接将数据存取到 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 序列化。而对于 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);

案例修正

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

https://www.java567.com,搜"spring")

 

案例 2:默认值的错误

当我们使用 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。参考下面的运行时配置调试截图:

 

如果你稍微了解下 Cassandra 的话,你就知道 Cassandra 使用的一个核心原则,就是要使得R(读)+W(写)>N,即读和写的节点数之和需要大于备份数。

例如,假设我们的数据备份是 3 份,待写入的数据分别存储在 A、B、C 三个节点上。那么常见的搭配是 R(读)和 W(写)的一致性都是 LOCAL_QURAM,这样可以保证能及时读到写入的数据;而假设在这种情况下,我们读写都是用 LOCAL_ONE,那么则可能发生这样的情况,即用户写入一个节点 A 就返回了,但是用户 B 立马读的节点是 C,且由于是 LOCAL_ONE 一致性,则读完 C 就可以立马返回。此时,就会出现数据读取可能落空的情况。

 

那么考虑一个问题,为什么 Cassandra driver 默认是使用 LOCAL_ONE 呢?

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

案例修正

通过这个案例的分析,我们知道 Spring Data Cassandra 的默认值不见得适应于所有情况,甚至说,不一定适合于产线环境,所以这里我们不妨修改下默认值,还是以 consistency 为例。

我们看下如何修改它:

 @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,更符合我们的实际产品部署和应用情况。

https://www.java567.com,搜"spring")

 

案例 3:冗余的 Session

有时候,我们使用 Spring Data 做连接时,会比较在意我们的内存占用。例如我们使用 Spring Data Cassandra 操作 Cassandra 时,可能会发现类似这样的问题:

 

Spring Data Cassandra 在连接 Cassandra 之后,会获取 Cassandra 的 Metadata 信息,这个内存占用量是比较大的,因为它存储了数据的 Token Range 等信息。如上图所示,在我们的应用中,占用 40M 以上已经不少了,但问题是为什么有 4 个占用 40 多 M 呢?难道不是只建立一个连接么?

案例解析

要定位这个问题,或许不是特别难,我们只要找到获取 Metadata 的地方加个断点,然后找出触发获取的源头即可。但是毕竟这是 Spring Data 间接操作,Cassandra driver 本身就可能够复杂了,再加上 Spring Data 的复杂度,想迅速定位问题的根源其实也不是一件容易的事情。

这里我们可以先写一个例子,直接展示下问题的原因,然后再来看看我们的问题到底出现在什么地方!

现在我们定义一个 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 的扫描范围。我们可以按如下结构组织代码:

 

最终我们会发现,当程序启动时,我们只有一个 MyService 的 Bean 产生,输出日志如下:

myservice defined from config

这里可以看出,如果我们的子类标识 Bean 的方法正好覆盖了对应的父类,那么只能利用子类的方法产生一个 Bean。

但是假设我们不小心在子类实现时,没有意识到父类方法的存在,定义如下呢?

 @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

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

 @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();
 }
 ​

上述代码中的 systemSession 和 session 即为上文提及的两个 Session。

重点回顾

学习完这3个案例,我们会发现,有些错误的直接结果很严重,以至于你很快就能定位并解决问题,但有一些问题会很隐蔽,例如案例 2 引发的问题就是如此,因为它不能 100%被重现。

结合案例,我们可以总结出使用 Spring Data 时必须注意的一些关键点:

  1. 一定要注意一致性,例如读写的序列化方法需要一致;

  2. 一定要重新检查下所有的默认配置是什么,是否符合当前的需求,例如在 Spring Data Cassandra 中,默认的一致性级别在大多情况下都不适合;

  3. 如果你自定义自己的Session,一定要避免冗余的Session产生。

记住这3点,你就能规避不少 Spring Data 使用上的问题了。

https://www.java567.com,搜"spring")

标签:错误,Spring,我们,key,Data,public,Cassandra
From: https://www.cnblogs.com/web-666/p/17434264.html

相关文章

  • data.py里的各个文件的生成
    data.py里的各个文件的生成LoadforcingdataERA5_LAND_label_4_1990???哪生成的ERA5-Land_forcing{sr}spatialresolution{year}.npy??哪来的lat_{s}.npy#109lon_{s}.npy#110filterGlacialregionMaskwith{sr}spatialresolution.npy#204Determinewhethertoperform......
  • 实例讲解Spring boot动态切换数据源
    摘要:本文模拟一下在主库查询订单信息查询不到的时候,切换数据源去历史库里面查询。本文分享自华为云社区《springboot动态切换数据源》,作者:小陈没烦恼。前言在公司的系统里,由于数据量较大,所以配置了多个数据源,它会根据用户所在的地区去查询那一个数据库,这样就产生了动态切换数......
  • Spring Security 常见错误
    案例1:遗忘PasswordEncoder当我们第一次尝试使用SpringSecurity时,我们经常会忘记定义一个PasswordEncoder。因为这在SpringSecurity旧版本中是允许的。而一旦使用了新版本,则必须要提供一个PasswordEncoder。这里我们可以先写一个反例来感受下:(https://www.java567.com,搜......
  • idea显示springboot多服务启动界面service
    如果是多模块的微服务,idea提供了一个可以多服务启动的界面services,如果你的项目里没看到这个界面:那么你需要在顶级的maven工程中找到这个配置,然后找到componentname="RunDashboard"这个节点整个替换掉:<componentname="RunDashboard"><optionname="configurationTypes">......
  • SpringBoot2.0实现SpringCloud config自动刷新之坑点
    在使用rabbitmq之后并不能实现客户端的配置自动刷新,原因是我参考的资料都是springboot1.x的,Springboot2.0的改动较大,之前1.0的/bus/refresh全部整合到actuador里面了,所以之前1.x的management.security.enabled全部失效,不适用于2.0适用于2.0的配置是这样的:management:endpoin......
  • Springboot+Vue集成个人中心、修改头像、数据联动、修改密码
    源码:https://gitee.com/xqnode/pure-design/tree/master学习视频:https://www.bilibili.com/video/BV1U44y1W77D开始讲解个人信息的下拉菜单:<el-dropdownstyle="width:150px;cursor:pointer;text-align:right"><divstyle="display:inline-block">......
  • Springboot集成百度地图实现定位打卡功能
    打卡sign表sqlCREATETABLE`sign`(`id`int(11)NOTNULLAUTO_INCREMENT,`user`varchar(255)COLLATEutf8mb4_unicode_ciDEFAULTNULLCOMMENT'用户名称',`location`varchar(255)COLLATEutf8mb4_unicode_ciDEFAULTNULLCOMMENT'打卡位置',`......
  • SpringBoot集成swagger-ui以及swagger分组显示
    文章目录1.swagger配置类2.使用swagger3.额外的学习经历大家好,这篇文章展示下如何在springboot项目中集成swagger-ui。有人说,这都是老生常谈,网上的例子数不胜数。确实swagger诞生至今已经很久了,但是在使用过程中我遇到一个问题,下面给大家分享下我的使用心得吧。1.swagger配置类第......
  • Springboot集成支付宝沙箱支付补充版本(退款功能)
    接上一次讲解:B站视频讲解:https://www.bilibili.com/video/BV1w44y1379q/这次的讲解涉及到完整的支付流程,大家请仔细查看!包括:支付宝沙箱支付+异步通知+退款功能正式版本的sdk通用版本SDK文档:https://opendocs.alipay.com/open/02np94<dependency><groupId>com.alipay.sdk......
  • Spring Web 参数验证常见错误
    案例1:对象参数校验失效在构建Web服务时,我们一般都会对一个HTTP请求的Body内容进行校验,例如我们来看这样一个案例及对应代码。当开发一个学籍管理系统时,我们会提供了一个API接口去添加学生的相关信息,其对象定义参考下面的代码:(https://www.java567.com,搜"spring") importlo......