前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊Spring Cache的缓存抽象与JSR107缓存抽象JCache,并使用API方式使用Spring Cache【享学Spring】

聊聊Spring Cache的缓存抽象与JSR107缓存抽象JCache,并使用API方式使用Spring Cache【享学Spring】

作者头像
YourBatman
发布2019-09-03 16:19:05
1.5K0
发布2019-09-03 16:19:05
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦
前言

缓存(Cache)是计算机领域一个极其重要的概念,它是提高硬件(比如CPU、显卡)、软件运行效率非常重要且有效的一个手段,它的最大特点就一个字:速度非常快

缓存就是数据交换的缓冲区(称作:Cache),当要读取数据时,会首先从缓存汇总查询数据,有则直接执行返回,速度飞快。它被运用在计算机领域的各个方面,介绍如下:

  • 操作系统磁盘缓存 ——> 减少磁盘机械操作
  • Web服务器缓存——>减少应用服务器请求
  • 客户端浏览器缓存——>减少对网站的访问
  • 应用程序缓存——>减少对数据库的查询
  • 数据库缓存——>减少文件系统IO

本文讲解的缓存就是运用在我们应用程序(软件)上的缓存,并且主要指的是在Spring环境下对缓存的使用。随着Spring框架的普及和渗透,在Spring应用中使用缓存,应该成为了当下Java开发者必备的一个基础技能了~

本文主要讲解Spring对缓存的抽象,当然也会对JSR107缓存抽象进行概念性的介绍。

JSR107缓存抽象:JCache

说起JSR107或者说是JCache,估摸大多数小伙伴都会觉得非常的陌生,没用过且还没听过。

JSR107的草案提得其实是非常的早的,但是第一个Final Release版本却一直难产到了2014年,如图(本文截自JSR官网):

在这里插入图片描述
在这里插入图片描述

虽然最终它还是被作为JSR规范提出了,但那时已经4102年了,黄瓜菜早就凉凉~

在还没有缓存规范出来之前,作为Java市场标准制定的强有力竞争者:Spring框架动作频频,早在2011年就提供了它自己的缓存抽象(Spring3.1)。这一切依托于Spring的良好生态下,各大缓存厂商纷纷提供了实现产品。

因此目前而言,关于缓存这块业界有个通识:

  • Spring Cache缓存抽象已经成了业界实际的标准(几乎所有产品都支持)
  • JSR107仅仅只是官方的标准而已(支持的产品并不多)

因为JSR107使用得极少,因此此处对它只做比较简单的一个概念介绍即可。

若要使用JCache,首先我们得额外导包(API包):

代码语言:javascript
复制
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>

2019年5月发布的最新的1.1.1版本。1.0.0版本是2014年5月发布的

在这里插入图片描述
在这里插入图片描述

从这个jar的类里可以看到,它几乎所有都是接口,自己并不提供具体实现(第三方厂商自行实现)。

JCache的实现产品挺少的,Ehcache3.x有实现JSR107相关规范接口

它的核心类的层次结构图:

在这里插入图片描述
在这里插入图片描述
  • CachingProvider:创建、配置、获取、管理和控制多个CacheManager
  • CacheManager:创建、配置、获取、管理和控制多个唯一命名的Cache。(一个CacheManager仅被一个CachingProvider所拥有
  • Cache:一个类似Map的数据结构。(一个Cache仅被一个CacheManager所拥有
  • Entry:一个存储在Cache中的key-value对
  • Expiry每一个存储在Cache中的条目有一个定义的有效期,过期后不可访问、更新、删除。缓存有效期可以通过ExpiryPolicy设置

说实话,我个人认为JCache的这个设计太大而全了,导致我们使用它的复杂度是非常高的,因此难以流行起来。(其实JavaEE的很多设计都有这个通病,标准过于复杂,落地实操性很差~)

我看网上有小伙伴评论说:JSR107的设计简直莫名其妙。 其实啊,针对这种评论一定要辩证性的看待,毕竟JSR是全球顶级专家一起制定的,整体优秀性我觉得是毋庸置疑的,只是它作为标准,它不能对那20%的场景避而不谈,而Spring却可以,这就是差别~

Spring缓存抽象

上面说了JCache真正发布都到2014年了,而早在2011年Spring3.1版本就定义了它自己的缓存抽象,旨在帮助开发者简化缓存的开发,并且最终流行开来。

在这里插入图片描述
在这里插入图片描述

从截图中可以看到,它被定义在spring-context里面的,作为上下文的核心内容,并不需要额外导包

Spring的缓存抽象相关类的层次结构非常简单:

在这里插入图片描述
在这里插入图片描述
  • CacheManager:缓存管理器。管理各种缓存(Cache)组件
  • Cache:为缓存的组件规范定义,包含缓存的各种操作集合。比如它有很多实现:ConcurrentMapCacheRedisCacheEhCacheCache(额外导包)

说明:看到这个层次结构,很多小伙伴会问为何没有Expire的定义? 这里我想说:这也是我比较费解的地方之一。Expire作为缓存非常重要的能力,为何不抽象出来呢???这也是我们经常苦恼的地方:@Cacheable注解竟然不支持TTL过期时间的设置,着实让人很蛋疼~~~

我个人把Spring没有Expire这个理解为Spring缓存抽象的一个功能缺失,说不客气点就是Spring的一个Bug。不知各位对此是否和我相同意见呢?欢迎砸我讨论

至于为何它一直都没有“修复”?我感觉是因为Cache属于它对外公布的API,各大产品都自己实现了Expire,而且方式不尽相同,所以最终它想统一就很难了,很难做到最好的兼容性~

CacheManagerCache的使用示例

CacheManager简单描述就是用来存放Cache,Cache用于存放具体的key-value值。 比如一个名为"汽车厂"的Cache,那你就可以通过这个名字从CacheManager拿出这个Cache,然后往里面缓存汽车。

首先看看CacheManager这个接口:

代码语言:javascript
复制
// pring's central cache manager SPI.  它是个SPI接口
// @since 3.1
public interface CacheManager {
	@Nullable
	Cache getCache(String name);
	// 管理的所有的Cache的names~
	Collection<String> getCacheNames();
}

它的继承树如下(不进行任何额外导包的情况下):

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这些实现是Spring内置的最基础的缓存管理器类。

AbstractCacheManager
代码语言:javascript
复制
// @since 3.1 实现了InitializingBean接口~~
public abstract class AbstractCacheManager implements CacheManager, InitializingBean {
	
	// 保存着所有的Cache对象~~key为名字
	private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);	
	//  此处使用了volatile 关键字
	private volatile Set<String> cacheNames = Collections.emptySet();

	@Override
	public void afterPropertiesSet() {
		initializeCaches();
	}

	// @since 4.2.2  模版方法模式。   abstract方法loadCaches()交给子类实现~~~
	public void initializeCaches() {
		Collection<? extends Cache> caches = loadCaches();

		synchronized (this.cacheMap) {
			this.cacheNames = Collections.emptySet();
			this.cacheMap.clear();
			Set<String> cacheNames = new LinkedHashSet<>(caches.size());
			for (Cache cache : caches) {
				String name = cache.getName();
				// decorateCache是protected方法,交给子类  不复写也无所谓~~~
				this.cacheMap.put(name, decorateCache(cache));
				cacheNames.add(name);
			}
			// cacheNames是个只读视图~(框架设计中考虑读写特性~)
			this.cacheNames = Collections.unmodifiableSet(cacheNames);
		}
	}

	protected abstract Collection<? extends Cache> loadCaches();

	// 根据名称 获取Cache对象。若没有,就返回null
	@Override
	@Nullable
	public Cache getCache(String name) {
		Cache cache = this.cacheMap.get(name);
		if (cache != null) {
			return cache;
		} else {
			// Fully synchronize now for missing cache creation...
			synchronized (this.cacheMap) {
				cache = this.cacheMap.get(name);
				if (cache == null) {
					// getMissingCache默认直接返回null,交给子类复写~~~~
					// 将决定权交给实现者,你可以创建一个Cache,或者记录日志
					cache = getMissingCache(name);
					// cache  != null,主要靠getMissingCache这个方法了~~~向一个工厂一样创建一个新的~~~
					if (cache != null) {
						cache = decorateCache(cache);
						this.cacheMap.put(name, cache);
						// 向全局缓存里面再添加进去一个Cache~~~~
						updateCacheNames(name);
					}
				}
				return cache;
			}
		}
	}

	@Override
	public Collection<String> getCacheNames() {
		return this.cacheNames;
	}

	// @since 4.1
	protected final Cache lookupCache(String name) {
		return this.cacheMap.get(name);
	}
	...
}

这个抽象类其实还蛮重要的,它提供了基本的操作,如果已存的CacheManager们都无法满足你的要求,你可以自己通过继承AbstractCacheManager实现一个自己的CacheManager

比如Redis相关的RedisCacheManager就是继承自它的~

SimpleCacheManager
代码语言:javascript
复制
// @since 3.1
public class SimpleCacheManager extends AbstractCacheManager {
	private Collection<? extends Cache> caches = Collections.emptySet();
	public void setCaches(Collection<? extends Cache> caches) {
		this.caches = caches;
	}
	@Override
	protected Collection<? extends Cache> loadCaches() {
		return this.caches;
	}
}

最简单的一个CacheManager,它的caches都必须由调用者手动指定。若交给容器管理会自动执行afterPropertiesSet()方法,否则需要手动自己调用cacheManager.afterPropertiesSet();,自己放进去的setCaches才会生效~

NoOpCacheManager

一种基本的、无操作的CacheManager实现,适用于禁用缓存,通常用于在没有实际存储的情况下作为缓存声明。

ConcurrentMapCacheManager(重要)
代码语言:javascript
复制
// @since 3.1
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

	private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
	// true表示:若为null,就新创建一个缓存添加进去
	private boolean dynamic = true;
	// 是否允许value为null
	private boolean allowNullValues = true;
	// 指定此缓存管理器是存储每个条目的副本,还是存储其所有缓存的引用
	// 影响到ConcurrentMapCache对value值的存储~~~
	// false表示:存储它自己(引用)
	// true表示:存储一个副本(对序列化有要求~~~) 很少这么使用~~~
	private boolean storeByValue = false;
	

	public ConcurrentMapCacheManager() {
	}
	public ConcurrentMapCacheManager(String... cacheNames) {
		setCacheNames(Arrays.asList(cacheNames));
	}
	public void setCacheNames(@Nullable Collection<String> cacheNames) {
		if (cacheNames != null) {
			for (String name : cacheNames) {
				this.cacheMap.put(name, createConcurrentMapCache(name));
			}
			this.dynamic = false; // 手动设置了,就不允许在动态创建了
		} else {
			this.dynamic = true;
		}
	}
	protected Cache createConcurrentMapCache(String name) {
		// isStoreByValue=true需要存储值的副本的时候,才对序列化有要求~~~否则直接存引用即可
		SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
		return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization);

	}

	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.serialization = new SerializationDelegate(classLoader);
		if (isStoreByValue()) {
			// 重新创建Cache 因为要副本嘛
			recreateCaches();
		}
	}
	private void recreateCaches() {
		for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
			entry.setValue(createConcurrentMapCache(entry.getKey()));
		}
	}

	@Override
	@Nullable
	public Cache getCache(String name) {
		Cache cache = this.cacheMap.get(name);
		// dynamic=true  为null的时候会动态创建一个
		if (cache == null && this.dynamic) {
			synchronized (this.cacheMap) {
				cache = this.cacheMap.get(name);
				if (cache == null) {
					cache = createConcurrentMapCache(name);
					this.cacheMap.put(name, cache);
				}
			}
		}
		return cache;
	}
	@Override
	public Collection<String> getCacheNames() {
		return Collections.unmodifiableSet(this.cacheMap.keySet());
	}
	...
}

它的缓存存储是基于内存的,所以它的生命周期是与应用关联的,对于生产级别的大型企业级应用程序,这可能并不是理想的选择,但它用于本地自己测试是个很好的选择。

CompositeCacheManager
代码语言:javascript
复制
// @since 3.1  
public class CompositeCacheManager implements CacheManager, InitializingBean {
	// 内部聚合管理着一批CacheManager
	private final List<CacheManager> cacheManagers = new ArrayList<>();
	// 若这个为true,则可以结合NoOpCacheManager实现效果~~~
	private boolean fallbackToNoOpCache = false;

	public CompositeCacheManager() {
	}
	public CompositeCacheManager(CacheManager... cacheManagers) {
		setCacheManagers(Arrays.asList(cacheManagers));
	}
	// 也可以调用此方法,来自己往里面添加(注意是添加)CacheManager们
	public void setCacheManagers(Collection<CacheManager> cacheManagers) {
		this.cacheManagers.addAll(cacheManagers);
	}

	public void setFallbackToNoOpCache(boolean fallbackToNoOpCache) {
		this.fallbackToNoOpCache = fallbackToNoOpCache;
	}

	// 如果fallbackToNoOpCache=true,那么在这个Bean初始化完成后,也就是在末尾添加一个NoOpCacheManager
	// 当然fallbackToNoOpCache默认值是false
	@Override
	public void afterPropertiesSet() {
		if (this.fallbackToNoOpCache) {
			this.cacheManagers.add(new NoOpCacheManager());
		}
	}


	// 找到一个cache不为null的就return了~
	@Override
	@Nullable
	public Cache getCache(String name) {
		for (CacheManager cacheManager : this.cacheManagers) {
			Cache cache = cacheManager.getCache(name);
			if (cache != null) {
				return cache;
			}
		}
		return null;
	}

	// 可以看到返回的是所有names,并且用的set
	@Override
	public Collection<String> getCacheNames() {
		Set<String> names = new LinkedHashSet<>();
		for (CacheManager manager : this.cacheManagers) {
			names.addAll(manager.getCacheNames());
		}
		return Collections.unmodifiableSet(names);
	}
}

CompositeCacheManager要通过一个或更多的缓存管理器来进行配置,它会迭代这些缓存管理器,以查找之前所缓存的值。

看完CacheManager,再看看Cache这个接口:

代码语言:javascript
复制
public interface Cache {
	String getName();
	// 返回本地存储的那个。比如ConcurrentMapCache本地就是用的一个ConcurrentMap
	Object getNativeCache();
	
	// 就是用下面的ValueWrapper把值包装了一下而已~
	@Nullable
	ValueWrapper get(Object key);
	@Nullable
	<T> T get(Object key, @Nullable Class<T> type);
	@Nullable
	<T> T get(Object key, Callable<T> valueLoader);
	
	void put(Object key, @Nullable Object value);
	// @since 4.1
	// 不存在旧值直接put就先去了返回null,否则返回旧值(并且不会把新值put进去)
	@Nullable
	ValueWrapper putIfAbsent(Object key, @Nullable Object value);
	// 删除
	void evict(Object key);
	// 清空
	void clear();


	@FunctionalInterface
	interface ValueWrapper {
		@Nullable
		Object get();
	}
}

它的继承树非常简单,如下:

在这里插入图片描述
在这里插入图片描述
AbstractValueAdaptingCache
代码语言:javascript
复制
// @since 4.2.2 出现得还是挺晚的~~~
public abstract class AbstractValueAdaptingCache implements Cache {
	private final boolean allowNullValues;
	protected AbstractValueAdaptingCache(boolean allowNullValues) {
		this.allowNullValues = allowNullValues;
	}
	// lookup为抽象方法
	@Override
	@Nullable
	public ValueWrapper get(Object key) {
		Object value = lookup(key);
		return toValueWrapper(value);
	}
	@Nullable
	protected abstract Object lookup(Object key);

	// lookup出来的value继续交给fromStoreValue()处理~  其实就是对null值进行了处理
	// 若是null值就返回null,而不是具体的值了~~~
	@Override
	@SuppressWarnings("unchecked")
	@Nullable
	public <T> T get(Object key, @Nullable Class<T> type) {
		Object value = fromStoreValue(lookup(key));
		if (value != null && type != null && !type.isInstance(value)) {
			throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
		}
		return (T) value;
	}

	// 它是protected 方法  子类有复写
	@Nullable
	protected Object fromStoreValue(@Nullable Object storeValue) {
		if (this.allowNullValues && storeValue == NullValue.INSTANCE) {
			return null;
		}
		return storeValue;
	}

	// 提供给子类使用的方法,对null值进行转换~  子类有复写
	protected Object toStoreValue(@Nullable Object userValue) {
		if (userValue == null) {
			if (this.allowNullValues) {
				return NullValue.INSTANCE;
			}
			throw new IllegalArgumentException("Cache '" + getName() + "' is configured to not allow null values but null was provided");
		}
		return userValue;
	}

	// 把value进行了一层包装为SimpleValueWrapper
	@Nullable
	protected Cache.ValueWrapper toValueWrapper(@Nullable Object storeValue) {
		return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null);
	}
}

显然该类是后来(Spring4.2.2)插入进来的专门对null值进行的处理。它提供了通用实现,来适配null值的问题。若你自定义Cache的实现,建议继承自此抽象类。

ConcurrentMapCache
代码语言:javascript
复制
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
	private final String name;
	// 底层存储就是个java.util.concurrent.ConcurrentMap~~
	private final ConcurrentMap<Object, Object> store;
	...
	// 本处只写出这个最全的构造器  它是protected 的,其它的构造器是public的
	// 说白了serialization这个类不允许外部知道,子类知道即可~~~
	protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues, @Nullable SerializationDelegate serialization) {

		super(allowNullValues);
		Assert.notNull(name, "Name must not be null");
		Assert.notNull(store, "Store must not be null");
		this.name = name;
		this.store = store;
		this.serialization = serialization;
	}

	// 显然只有指定了序列化器,才有可能true:存储副本
	public final boolean isStoreByValue() {
		return (this.serialization != null);
	}
	...
	@Override
	public final ConcurrentMap<Object, Object> getNativeCache() {
		return this.store;
	}
	// 这是父类的抽象方法
	@Override
	@Nullable
	protected Object lookup(Object key) {
		return this.store.get(key);
	}

	@Override
	@Nullable
	public <T> T get(Object key, Callable<T> valueLoader) {
		return (T) fromStoreValue(this.store.computeIfAbsent(key, r -> {
			try {
				return toStoreValue(valueLoader.call());
			} catch (Throwable ex) {
				throw new ValueRetrievalException(key, valueLoader, ex);
			}
		}));
	}

	@Override
	public void put(Object key, @Nullable Object value) {
		this.store.put(key, toStoreValue(value));
	}
	@Override
	@Nullable
	public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
		Object existing = this.store.putIfAbsent(key, toStoreValue(value));
		return toValueWrapper(existing);
	}
	@Override
	public void evict(Object key) {
		this.store.remove(key);
	}
	@Override
	public void clear() {
		this.store.clear();
	}
	...
}

ConcurrentMapCache它是spring-context提供的内建唯一缓存实现,它是完全基于本地内存的。

Springboot默认使用的是SimpleCacheConfiguration,它配置的是ConcurrentMapCacheManager来实现缓存,因此对应Cache实现为ConcurrentMapCache

NoOpCache

NoOpCache配合NoOpCacheManager使用~

使用示例

上面介绍了spring-context自带的一些缓存管理器CacheManager实现以及缓存Cache实现。接下来是骡子是马,现在拉出来遛遛,用个案例介绍它的使用方式:

代码语言:javascript
复制
    public static void main(String[] args) {
        CacheManager cacheManager = new ConcurrentMapCacheManager(); //使用ConcurrentMapCacheManager可以不用初始化指定,可以get的时候动态创建Cache
        //CacheManager cacheManager = new ConcurrentMapCacheManager("car");

		// 即使我们上面没有放进去名字为car的Cache,此处也会帮我们自动生成~~~
        Cache carCache = cacheManager.getCache("car");
        // 向缓存里加数据
        carCache.put("benz", "奔驰");
        carCache.put("bmw", "宝马");
        carCache.put("audi", "奥迪");

        System.out.println(carCache.getClass()); //class org.springframework.cache.concurrent.ConcurrentMapCache
        // 从缓存里获取数据 
        System.out.println(carCache.get("benz").get()); //奔驰
        System.out.println(carCache.get("benz", String.class)); //奔驰

    }

此处我们用ConcurrentMapCacheManager作为实例,其实还可以使用SimpleCacheManager这个最为简单的缓存管理器:

代码语言:javascript
复制
    public static void main(String[] args) {
        CacheManager cacheManager = new SimpleCacheManager();

        Cache car = cacheManager.getCache("car");
        System.out.println(car); //null
    }

可以看到若我们使用SimpleCacheManager这个缓存管理器,它并不会给我们动态生成Cache对象,一切都变成手动档了:

代码语言:javascript
复制
    public static void main(String[] args) {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        ConcurrentMapCache carMapCache = new ConcurrentMapCache("car");
        cacheManager.setCaches(Collections.singleton(carMapCache));
        cacheManager.afterPropertiesSet(); // 这一步是必须的

        Cache car = cacheManager.getCache("car");
        System.out.println(car); //org.springframework.cache.concurrent.ConcurrentMapCache@19469ea2
    }

我们手动准备Cache、手动调用afterPropertiesSet()才有用~

本例只介绍了单元测试时的使用方式,若和Spring集成,一切就更简单了,各位小伙伴自行实践吧~


CompositeCacheManager + NoOpCacheManager

CompositeCacheManager主要用于集合多个CacheManager实例,在使用多种缓存容器(比如Redis+EhCache的组合)时特别有用。

设想这一个场景:当代码中使用@Cacheable注解指定的cacheNames中,却没有这个cacheManagers时,执行时便会报错。但是若此时我们使用的是CompositeCacheManager并且设置fallbackToNoOpCache=true,那么它就会没找到也最终进入到NoOpCacheManager里面去(用NoOpCache代替~),此时就相当于禁用掉了缓存,而不抛出相应的异常。

当你的应用中使用到了多个缓存的时候,强烈建议使用CompositeCacheManager管理(当然倘若是一个缓存也可以使用它,方便日后更加方便的扩展,这点在缓存注解章节里有深入讲解~)


最后需要注意的是:如果需要让Spring容器中的缓存可以正常工作,必须配置至少一个CacheManager。

总结

本文介绍了JSR107的缓存抽象JCache的概念和设计,以及重点介绍了Spring对缓存的抽象,希望各位看官在实操过程中,也需要注重一定概念性东西,更需要关注一下业界规范。需要注意的是,缓存不是Java世界中的一个概念~~~

本文重在概念的介绍、核心接口类的理解。至于如何防止缓存穿透、缓存击穿、缓存雪崩和缓存刷新等高级话题,后面也会加以论述~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年07月02日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • JSR107缓存抽象:JCache
  • Spring缓存抽象
    • CacheManager和Cache的使用示例
      • 使用示例
        • 总结
        相关产品与服务
        云数据库 Redis
        腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档