案例 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 时必须注意的一些关键点:
-
一定要注意一致性,例如读写的序列化方法需要一致;
-
一定要重新检查下所有的默认配置是什么,是否符合当前的需求,例如在 Spring Data Cassandra 中,默认的一致性级别在大多情况下都不适合;
-
如果你自定义自己的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