设为首页 收藏本站
查看: 893|回复: 0

[经验分享] mybatis缓存原理

[复制链接]

尚未签到

发表于 2016-11-24 10:29:19 | 显示全部楼层 |阅读模式
缓存概述

  • 正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持;
  • 一级缓存基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。
  • 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache、Hazelcast等。
  • 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
  • MyBatis 的缓存采用了delegate机制 及 装饰器模式设计,当put、get、remove时,其中会经过多层 delegate cache 处理,其Cache类别有:BaseCache(基础缓存)、EvictionCache(排除算法缓存) 、DecoratorCache(装饰器缓存):BaseCache :为缓存数据最终存储的处理类,默认为 PerpetualCache,基于Map存储;可自定义存储处理,如基于EhCache、Memcached等;
    EvictionCache :当缓存数量达到一定大小后,将通过算法对缓存数据进行清除。默认采用 Lru 算法(LruCache),提供有 fifo 算法(FifoCache)等;
    DecoratorCache:缓存put/get处理前后的装饰器,如使用 LoggingCache 输出缓存命中日志信息、使用 SerializedCache 对 Cache的数据 put或get 进行序列化及反序列化处理、当设置flushInterval(默认1/h)后,则使用 ScheduledCache 对缓存数据进行定时刷新等。
  • 一般缓存框架的数据结构基本上都是 Key-Value 方式存储,MyBatis 对于其 Key 的生成采取规则为:[hashcode : checksum : mappedStementId : offset : limit : executeSql : queryParams]。
  • 对于并发 Read/Write 时缓存数据的同步问题,MyBatis 默认基于 JDK/concurrent中的ReadWriteLock,使用ReentrantReadWriteLock 的实现,从而通过 Lock 机制防止在并发 Write Cache 过程中线程安全问题。

源码剖解
接下来将结合 MyBatis 序列图进行源码分析。在分析其Cache前,先看看其整个处理过程。
执行过程:
DSC0000.png
① 通常情况下,我们需要在 Service 层调用 Mapper Interface 中的方法实现对数据库的操作,上述根据产品 ID 获取 Product 对象。
② 当调用 ProductMapper 时中的方法时,其实这里所调用的是 MapperProxy 中的方法,并且 MapperProxy已经将将所有方法拦截,其具体原理及分析,参考 MyBatis+Spring基于接口编程的原理分析,其 invoke 方法代码为:
Java代码 DSC0001.png


  • //当调用Mapper所有的方法时,将都交由Proxy中的invoke处理:
  • publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
  • try{
  • if(!OBJECT_METHODS.contains(method.getName())){
  • finalClassdeclaringInterface=findDeclaringInterface(proxy,method);
  • //最终交由MapperMethod类处理数据库操作,初始化MapperMethod对象
  • finalMapperMethodmapperMethod=newMapperMethod(declaringInterface,method,sqlSession);
  • //执行mappermethod,返回执行结果
  • finalObjectresult=mapperMethod.execute(args);
  • ....
  • returnresult;
  • }
  • }catch(SQLExceptione){
  • e.printStackTrace();
  • }
  • returnnull;
  • }


③其中的 mapperMethod 中的 execute 方法代码如下:
Java代码


  • publicObjectexecute(Object[]args)throwsSQLException{
  • Objectresult;
  • //根据不同的操作类别,调用DefaultSqlSession中的执行处理
  • if(SqlCommandType.INSERT==type){
  • Objectparam=getParam(args);
  • result=sqlSession.insert(commandName,param);
  • }elseif(SqlCommandType.UPDATE==type){
  • Objectparam=getParam(args);
  • result=sqlSession.update(commandName,param);
  • }elseif(SqlCommandType.DELETE==type){
  • Objectparam=getParam(args);
  • result=sqlSession.delete(commandName,param);
  • }elseif(SqlCommandType.SELECT==type){
  • if(returnsList){
  • result=executeForList(args);
  • }else{
  • Objectparam=getParam(args);
  • result=sqlSession.selectOne(commandName,param);
  • }
  • }else{
  • thrownewBindingException("Unkownexecutionmethodfor:"+commandName);
  • }
  • returnresult;
  • }

由于这里是根据 ID 进行查询,所以最终调用为 sqlSession.selectOne函数。也就是接下来的的 DefaultSqlSession.selectOne 执行;
④ ⑤ 可以在 DefaultSqlSession 看到,其 selectOne 调用了 selectList 方法:Java代码


  • publicObjectselectOne(Stringstatement,Objectparameter){
  • Listlist=selectList(statement,parameter);
  • if(list.size()==1){
  • returnlist.get(0);
  • }
  • ...
  • }

  • publicListselectList(Stringstatement,Objectparameter,RowBoundsrowBounds){
  • try{
  • MappedStatementms=configuration.getMappedStatement(statement);
  • //如果启动用了Cache才调用CachingExecutor.query,反之则使用BaseExcutor.query进行数据库查询
  • returnexecutor.query(ms,wrapCollection(parameter),rowBounds,Executor.NO_RESULT_HANDLER);
  • }catch(Exceptione){
  • throwExceptionFactory.wrapException("Errorqueryingdatabase.Cause:"+e,e);
  • }finally{
  • ErrorContext.instance().reset();
  • }
  • }

⑥到这里,已经执行到具体数据查询的流程,在分析 CachingExcutor.query 前,先看看 MyBatis 中 Executor 的结构及构建过程。


执行器(Executor):
Executor: 执行器接口。也是最终执行数据获取及更新的实例。其类结构如下:
DSC0002.png
BaseExecutor: 基础执行器抽象类。实现一些通用方法,如createCacheKey 之类。并且采用 模板模式 将具体的数据库操作逻辑(doUpdate、doQuery)交由子类实现。另外,可以看到变量localCache: PerpetualCache,在该类采用 PerpetualCache 实现基于 Map 存储的一级缓存,其 query 方法如下:Java代码


  • publicListquery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException{
  • ErrorContext.instance().resource(ms.getResource()).activity("executingaquery").object(ms.getId());
  • //执行器已关闭
  • if(closed)thrownewExecutorException("Executorwasclosed.");
  • Listlist;
  • try{
  • queryStack++;
  • //创建缓存Key
  • CacheKeykey=createCacheKey(ms,parameter,rowBounds);
  • //从本地缓存在中获取该key所对应的结果集
  • finalListcachedList=(List)localCache.getObject(key);
  • //在缓存中找到数据
  • if(cachedList!=null){
  • list=cachedList;
  • }else{//未从本地缓存中找到数据,开始调用数据库查询
  • //为该key添加一个占位标记
  • localCache.putObject(key,EXECUTION_PLACEHOLDER);
  • try{
  • //执行子类所实现的数据库查询操作
  • list=doQuery(ms,parameter,rowBounds,resultHandler);
  • }finally{
  • //删除该key的占位标记
  • localCache.removeObject(key);
  • }
  • //将db中的数据添加至本地缓存中
  • localCache.putObject(key,list);
  • }
  • }finally{
  • queryStack--;
  • }
  • //刷新当前队列中的所有DeferredLoad实例,更新MateObject
  • if(queryStack==0){
  • for(DeferredLoaddeferredLoad:deferredLoads){
  • deferredLoad.load();
  • }
  • }
  • returnlist;
  • }

BatchExcutor、ReuseExcutor、SimpleExcutor: 这几个就没什么好说的了,继承了 BaseExcutor 的实现其 doQuery、doUpdate 等方法,同样都是采用 JDBC 对数据库进行操作;三者区别在于,批量执行、重用 Statement 执行、普通方式执行。具体应用及场景在Mybatis 的文档上都有详细说明。

CachingExecutor: 二级缓存执行器。个人觉得这里设计的不错,灵活地使用 delegate机制。其委托执行的类是 BaseExcutor。 当无法从二级缓存获取数据时,同样需要从DB 中进行查询,于是在这里可以直接委托给 BaseExcutor 进行查询。其大概流程为:
DSC0003.png
流程为: 从二级缓存中进行查询 -> [如果缓存中没有,委托给 BaseExecutor] -> 进入一级缓存中查询 -> [如果也没有] -> 则执行 JDBC 查询,其 query 代码如下:Java代码


  • publicListquery(MappedStatementms,ObjectparameterObject,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException{
  • if(ms!=null){
  • //获取二级缓存实例
  • Cachecache=ms.getCache();
  • if(cache!=null){
  • flushCacheIfRequired(ms);
  • //获取读锁(Read锁可由多个Read线程同时保持)
  • cache.getReadWriteLock().readLock().lock();
  • try{
  • //当前Statement是否启用了二级缓存
  • if(ms.isUseCache()){
  • //将创建cachekey委托给BaseExecutor创建
  • CacheKeykey=createCacheKey(ms,parameterObject,rowBounds);
  • finalListcachedList=(List)cache.getObject(key);
  • //从二级缓存中找到缓存数据
  • if(cachedList!=null){
  • returncachedList;
  • }else{
  • //未找到缓存,很委托给BaseExecutor执行查询
  • Listlist=delegate.query(ms,parameterObject,rowBounds,resultHandler);
  • tcm.putObject(cache,key,list);
  • returnlist;
  • }
  • }else{//没有启动用二级缓存,直接委托给BaseExecutor执行查询
  • returndelegate.query(ms,parameterObject,rowBounds,resultHandler);
  • }
  • }finally{
  • //当前线程释放Read锁
  • cache.getReadWriteLock().readLock().unlock();
  • }
  • }
  • }
  • returndelegate.query(ms,parameterObject,rowBounds,resultHandler);
  • }

至此,已经完完了整个缓存执行器的整个流程分析,接下来是对缓存的 缓存数据管理实例进行分析,也就是其 Cache 接口,用于对缓存数据 put 、get及remove的实例对象。


Cache 委托链构建:
正如最开始的缓存概述所描述道,其缓存类的设计采用 装饰模式,基于委托的调用机制。
缓存实例构建:
缓存实例的构建 ,Mybatis 在解析其 Mapper 配置文件时就已经将该实现初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 类中可以看到:
Java代码


  • privatevoidcacheElement(XNodecontext)throwsException{
  • if(context!=null){
  • //基础缓存类型
  • Stringtype=context.getStringAttribute("type","PERPETUAL");
  • ClasstypeClass=typeAliasRegistry.resolveAlias(type);
  • //排除算法缓存类型
  • Stringeviction=context.getStringAttribute("eviction","LRU");
  • ClassevictionClass=typeAliasRegistry.resolveAlias(eviction);
  • //缓存自动刷新时间
  • LongflushInterval=context.getLongAttribute("flushInterval");
  • //缓存存储实例引用的大小
  • Integersize=context.getIntAttribute("size");
  • //是否是只读缓存
  • booleanreadWrite=!context.getBooleanAttribute("readOnly",false);
  • Propertiesprops=context.getChildrenAsProperties();
  • //初始化缓存实现
  • builderAssistant.useNewCache(typeClass,evictionClass,flushInterval,size,readWrite,props);
  • }
  • }

以下是 useNewCache 方法实现:
Java代码


  • publicCacheuseNewCache(ClasstypeClass,
  • ClassevictionClass,
  • LongflushInterval,
  • Integersize,
  • booleanreadWrite,
  • Propertiesprops){
  • typeClass=valueOrDefault(typeClass,PerpetualCache.class);
  • evictionClass=valueOrDefault(evictionClass,LruCache.class);
  • //这里构建Cache实例采用Builder模式,每一个Namespace生成一个Cache实例
  • Cachecache=newCacheBuilder(currentNamespace)
  • //Builder前设置一些从XML中解析过来的参数
  • .implementation(typeClass)
  • .addDecorator(evictionClass)
  • .clearInterval(flushInterval)
  • .size(size)
  • .readWrite(readWrite)
  • .properties(props)
  • //再看下面的build方法实现
  • .build();
  • configuration.addCache(cache);
  • currentCache=cache;
  • returncache;
  • }

  • publicCachebuild(){
  • setDefaultImplementations();
  • //创建基础缓存实例
  • Cachecache=newBaseCacheInstance(implementation,id);
  • setCacheProperties(cache);
  • //缓存排除算法初始化,并将其委托至基础缓存中
  • for(Class<?extendsCache>decorator:decorators){
  • cache=newCacheDecoratorInstance(decorator,cache);
  • setCacheProperties(cache);
  • }
  • //标准装饰器缓存设置,如LoggingCache之类,同样将其委托至基础缓存中
  • cache=setStandardDecorators(cache);
  • //返回最终缓存的责任链对象
  • returncache;
  • }

最终生成后的缓存实例对象结构:
DSC0004.png
可见,所有构建的缓存实例已经通过责任链方式将其串连在一起,各 Cache 各负其责、依次调用,直到缓存数据被 Put 至 基础缓存实例中存储。


Cache 实例解剖:
实例类:SynchronizedCache
说 明:用于控制 ReadWriteLock,避免并发时所产生的线程安全问题。
解 剖:
对于 Lock 机制来说,其分为 Read 和 Write 锁,其 Read 锁允许多个线程同时持有,而 Write 锁,一次能被一个线程持有,如果当 Write 锁没有释放,其它需要 Write 的线程只能等待其释放才能去持有。
其代码实现:Java代码


  • publicvoidputObject(Objectkey,Objectobject){
  • acquireWriteLock();//获取Write锁
  • try{
  • delegate.putObject(key,object);//委托给下一个Cache执行put操作
  • }finally{
  • releaseWriteLock();//释放Write锁
  • }
  • }

对于 Read 数据来说,也是如此,不同的是 Read 锁允许多线程同时持有 :
Java代码


  • publicObjectgetObject(Objectkey){
  • acquireReadLock();
  • try{
  • returndelegate.getObject(key);
  • }finally{
  • releaseReadLock();
  • }
  • }

其具体原理可以看看 jdk concurrent 中的 ReadWriteLock 实现。


实例类:LoggingCache
说 明:用于日志记录处理,主要输出缓存命中率信息。
解 剖:
说到缓存命中信息的统计,只有在 get 的时候才需要统计命中率:
Java代码


  • publicObjectgetObject(Objectkey){
  • requests++;//每调用一次该方法,则获取次数+1
  • finalObjectvalue=delegate.getObject(key);
  • if(value!=null){//命中!命中+1
  • hits++;
  • }
  • if(log.isDebugEnabled()){
  • //输出命中率。计算方法为:hits/requets则为命中率
  • log.debug("CacheHitRatio["+getId()+"]:"+getHitRatio());
  • }
  • returnvalue;
  • }




实例类:SerializedCache
说 明:向缓存中 put 或 get 数据时的序列化及反序列化处理。
解 剖:
序列化在Java里面已经是最基础的东西了,这里也没有什么特殊之处:
Java代码


  • publicvoidputObject(Objectkey,Objectobject){
  • //PO类需要实现Serializable接口
  • if(object==null||objectinstanceofSerializable){
  • delegate.putObject(key,serialize((Serializable)object));
  • }else{
  • thrownewCacheException("SharedCachefailedtomakeacopyofanon-serializableobject:"+object);
  • }
  • }

  • publicObjectgetObject(Objectkey){
  • Objectobject=delegate.getObject(key);
  • //获取数据时对byte数据进行反序列化
  • returnobject==null?null:deserialize((byte[])object);
  • }

其 serialize 及 deserialize 代码就不必要贴了。


实例类:LruCache
说 明:最近最少使用的:移除最长时间不被使用的对象,基于LRU算法。
解 剖:
这里的 LRU 算法基于 LinkedHashMap 覆盖其 removeEldestEntry 方法实现。好象之前看过 XMemcached 的 LRU 算法也是这样实现的。
初始化 LinkedHashMap,默认为大小为 1024 个元素:
Java代码


  • publicLruCache(Cachedelegate){
  • this.delegate=delegate;
  • setSize(1024);//设置map默认大小
  • }
  • publicvoidsetSize(finalintsize){
  • //设置其capacity为size,其factor为.75F
  • keyMap=newLinkedHashMap(size,.75F,true){
  • //覆盖该方法,当每次往该map中put时数据时,如该方法返回True,便移除该map中使用最少的Entry
  • //其参数eldest为当前最老的Entry
  • protectedbooleanremoveEldestEntry(Map.Entryeldest){
  • booleantooBig=size()>size;
  • if(tooBig){
  • eldestKey=eldest.getKey();//记录当前最老的缓存数据的Key值,因为要委托给下一个Cache实现删除
  • }
  • returntooBig;
  • }
  • };
  • }

  • publicvoidputObject(Objectkey,Objectvalue){
  • delegate.putObject(key,value);
  • cycleKeyList(key);//每次put后,调用移除最老的key
  • }
  • //看看当前实现是否有eldestKey,有的话就调用removeObject,将该key从cache中移除
  • privatevoidcycleKeyList(Objectkey){
  • keyMap.put(key,key);//存储当前put到cache中的key值
  • if(eldestKey!=null){
  • delegate.removeObject(eldestKey);
  • eldestKey=null;
  • }
  • }

  • publicObjectgetObject(Objectkey){
  • keyMap.get(key);//便于该Map统计get该key的次数
  • returndelegate.getObject(key);
  • }



实例类:PerpetualCache
说 明:这个比较简单,直接通过一个 HashMap 来存储缓存数据。所以没什么说的,直接看下面的 MemcachedCache 吧。


自定义二级缓存/Memcached
其自定义二级缓存也较为简单,它本身默认提供了对 Ehcache 及 Hazelcast 的缓存支持:Mybatis-Cache,我这里参考它们的实现,自定义了针对 Memcached 的缓存支持,其代码如下:
Java代码


  • packagecom.xx.core.plugin.mybatis;

  • importjava.util.LinkedList;
  • importjava.util.concurrent.locks.ReadWriteLock;
  • importjava.util.concurrent.locks.ReentrantReadWriteLock;

  • importorg.apache.ibatis.cache.Cache;
  • importorg.slf4j.Logger;
  • importorg.slf4j.LoggerFactory;

  • importcom.xx.core.memcached.JMemcachedClientAdapter;
  • importcom.xx.core.memcached.service.CacheService;
  • importcom.xx.core.memcached.service.MemcachedService;

  • /**
  • *CacheadapterforMemcached.
  • *
  • *@authordenger
  • */
  • publicclassMemcachedCacheimplementsCache{

  • //Sf4jloggerreference
  • privatestaticLoggerlogger=LoggerFactory.getLogger(MemcachedCache.class);

  • /**Thecacheservicereference.*/
  • protectedstaticfinalCacheServiceCACHE_SERVICE=createMemcachedService();

  • /**TheReadWriteLock.*/
  • privatefinalReadWriteLockreadWriteLock=newReentrantReadWriteLock();

  • privateStringid;
  • privateLinkedList<String>cacheKeys=newLinkedList<String>();

  • publicMemcachedCache(Stringid){
  • this.id=id;
  • }
  • //创建缓存服务类,基于java-memcached-client
  • protectedstaticCacheServicecreateMemcachedService(){
  • JMemcachedClientAdaptermemcachedAdapter;

  • try{
  • memcachedAdapter=newJMemcachedClientAdapter();
  • }catch(Exceptione){
  • Stringmsg="InitialtheJMmemcachedClientAdapterError.";
  • logger.error(msg,e);
  • thrownewRuntimeException(msg);
  • }
  • returnnewMemcachedService(memcachedAdapter);
  • }

  • @Override
  • publicStringgetId(){
  • returnthis.id;
  • }

  • //根据key从缓存中获取数据
  • @Override
  • publicObjectgetObject(Objectkey){
  • StringcacheKey=String.valueOf(key.hashCode());
  • Objectvalue=CACHE_SERVICE.get(cacheKey);
  • if(!cacheKeys.contains(cacheKey)){
  • cacheKeys.add(cacheKey);
  • }
  • returnvalue;
  • }

  • @Override
  • publicReadWriteLockgetReadWriteLock(){
  • returnthis.readWriteLock;
  • }

  • //设置数据至缓存中
  • @Override
  • publicvoidputObject(Objectkey,Objectvalue){
  • StringcacheKey=String.valueOf(key.hashCode());

  • if(!cacheKeys.contains(cacheKey)){
  • cacheKeys.add(cacheKey);
  • }
  • CACHE_SERVICE.put(cacheKey,value);
  • }
  • //从缓存中删除指定key数据
  • @Override
  • publicObjectremoveObject(Objectkey){
  • StringcacheKey=String.valueOf(key.hashCode());

  • cacheKeys.remove(cacheKey);
  • returnCACHE_SERVICE.delete(cacheKey);
  • }
  • //清空当前Cache实例中的所有缓存数据
  • @Override
  • publicvoidclear(){
  • for(inti=0;i<cacheKeys.size();i++){
  • StringcacheKey=cacheKeys.get(i);
  • CACHE_SERVICE.delete(cacheKey);
  • }
  • cacheKeys.clear();
  • }

  • @Override
  • publicintgetSize(){
  • returncacheKeys.size();
  • }
  • }


在 ProductMapper 中增加配置:
Xml代码


  • <cacheeviction="LRU"type="com.xx.core.plugin.mybatis.MemcachedCache"/>


启动Memcached:
Shell代码


  • memcached-c2000-p11211-vv-U0-l192.168.1.2-v


执行Mapper 中的查询、修改等操作,Test:
Java代码


  • @Test
  • publicvoidtestSelectById(){
  • Longpid=100L;

  • ProductdbProduct=productMapper.selectByID(pid);
  • Assert.assertNotNull(dbProduct);

  • ProductcacheProduct=productMapper.selectByID(pid);
  • Assert.assertNotNull(cacheProduct);

  • productMapper.updateName("IPad",pid);

  • Productproduct=productMapper.selectByID(pid);
  • Assert.assertEquals(product.getName(),"IPad");
  • }


Memcached Loging:
DSC0005.png
看上去没什么问题~ OK了。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-304884-1-1.html 上篇帖子: Hibernate与 MyBatis的比较 下篇帖子: mybatis的paramType
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表