溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離

發(fā)布時(shí)間:2021-07-20 18:12:26 來源:億速云 閱讀:165 作者:chen 欄目:web開發(fā)

這篇文章主要介紹“Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離”,在日常操作中,相信很多人在Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

寫在前面

很多小伙伴私聊我說:最近他們公司的業(yè)務(wù)涉及到多個(gè)數(shù)據(jù)源的問題,問我Spring如何實(shí)現(xiàn)多數(shù)據(jù)源的問題?;卮疬@個(gè)問題之前,首先需要弄懂什么是多數(shù)據(jù)源:多數(shù)據(jù)源就是在同一個(gè)項(xiàng)目中,會(huì)連接兩個(gè)甚至多個(gè)數(shù)據(jù)存儲(chǔ),這里的數(shù)據(jù)存儲(chǔ)可以是關(guān)系型數(shù)據(jù)庫(比如:MySQL、SQL  Server、Oracle),也可以非關(guān)系型數(shù)據(jù)庫,比如:HBase、MongoDB、ES等。那么,問題來了,Spring能夠?qū)崿F(xiàn)多數(shù)據(jù)源嗎?并且還要實(shí)現(xiàn)讀者分離?答案是:必須的,這么強(qiáng)大的Spring,肯定能實(shí)現(xiàn)啊!別急,我們就一點(diǎn)點(diǎn)剖析、解決這些問題!

背景

我們一般應(yīng)用對數(shù)據(jù)庫而言都是“讀多寫少”,也就說對數(shù)據(jù)庫讀取數(shù)據(jù)的壓力比較大,有一個(gè)思路就是說采用數(shù)據(jù)庫集群的方案,

其中一個(gè)是主庫,負(fù)責(zé)寫入數(shù)據(jù),我們稱之為:寫庫;其它都是從庫,負(fù)責(zé)讀取數(shù)據(jù),我們稱之為:讀庫;

那么,對我們的要求是:

  • 讀庫和寫庫的數(shù)據(jù)一致;

  • 寫數(shù)據(jù)必須寫到寫庫;

  • 讀數(shù)據(jù)必須到讀庫;

方案

解決讀寫分離的方案有兩種:應(yīng)用層解決和中間件解決。

應(yīng)用層解決

Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離

優(yōu)點(diǎn):

  • 多數(shù)據(jù)源切換方便,由程序自動(dòng)完成;

  • 不需要引入中間件;

  • 理論上支持任何數(shù)據(jù)庫;

缺點(diǎn):

  • 由程序員完成,運(yùn)維參與不到;

  • 不能做到動(dòng)態(tài)增加數(shù)據(jù)源;

中間件解決

Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離

優(yōu)點(diǎn):

  • 源程序不需要做任何改動(dòng)就可以實(shí)現(xiàn)讀寫分離;

  • 動(dòng)態(tài)添加數(shù)據(jù)源不需要重啟程序;

缺點(diǎn):

  • 程序依賴于中間件,會(huì)導(dǎo)致切換數(shù)據(jù)庫變得困難;

  • 由中間件做了中轉(zhuǎn)代理,性能有所下降;

Spring方案

原理

Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離

在進(jìn)入Service之前,使用AOP來做出判斷,是使用寫庫還是讀庫,判斷依據(jù)可以根據(jù)方法名判斷,比如說以query、find、get等開頭的就走讀庫,其他的走寫庫。

DynamicDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;   /**  * 定義動(dòng)態(tài)數(shù)據(jù)源,實(shí)現(xiàn)通過集成Spring提供的AbstractRoutingDataSource,只需要實(shí)現(xiàn)determineCurrentLookupKey方法即可  * 由于DynamicDataSource是單例的,線程不安全的,所以采用ThreadLocal保證線程安全,由DynamicDataSourceHolder完成。  * @author binghe  */ public class DynamicDataSource extends AbstractRoutingDataSource{       @Override     protected Object determineCurrentLookupKey() {         // 使用DynamicDataSourceHolder保證線程安全,并且得到當(dāng)前線程中的數(shù)據(jù)源key         return DynamicDataSourceHolder.getDataSourceKey();     }   }

DynamicDataSourceHolder

/**  * 使用ThreadLocal技術(shù)來記錄當(dāng)前線程中的數(shù)據(jù)源的key  * @author binghe  */ public class DynamicDataSourceHolder {          //寫庫對應(yīng)的數(shù)據(jù)源key     private static final String MASTER = "master";       //讀庫對應(yīng)的數(shù)據(jù)源key     private static final String SLAVE = "slave";          //使用ThreadLocal記錄當(dāng)前線程的數(shù)據(jù)源key     private static final ThreadLocal<String> holder = new ThreadLocal<String>();       /**      * 設(shè)置數(shù)據(jù)源key      * @param key      */     public static void putDataSourceKey(String key) {         holder.set(key);     }       /**      * 獲取數(shù)據(jù)源key      * @return      */     public static String getDataSourceKey() {         return holder.get();     }          /**      * 標(biāo)記寫庫      */     public static void markMaster(){         putDataSourceKey(MASTER);     }          /**      * 標(biāo)記讀庫      */     public static void markSlave(){         putDataSourceKey(SLAVE);     }   }

DataSourceAspect

import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint;   /**  * 定義數(shù)據(jù)源的AOP切面,通過該Service的方法名判斷是應(yīng)該走讀庫還是寫庫  * @author binghe  */ public class DataSourceAspect {       /**      * 在進(jìn)入Service方法之前執(zhí)行      * @param point 切面對象      */     public void before(JoinPoint point) {         // 獲取到當(dāng)前執(zhí)行的方法名         String methodName = point.getSignature().getName();         if (isSlave(methodName)) {             // 標(biāo)記為讀庫             DynamicDataSourceHolder.markSlave();         } else {             // 標(biāo)記為寫庫             DynamicDataSourceHolder.markMaster();         }     }       /**      * 判斷是否為讀庫      *       * @param methodName      * @return      */     private Boolean isSlave(String methodName) {         // 方法名以query、find、get開頭的方法名走從庫         return StringUtils.startsWithAny(methodName, "query", "find", "get");     }   }

配置2個(gè)數(shù)據(jù)源

jdbc.properties

jdbc.master.driver=com.mysql.jdbc.Driver jdbc.master.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true jdbc.master.username=root jdbc.master.password=123456   jdbc.slave01.driver=com.mysql.jdbc.Driver jdbc.slave01.url=jdbc:mysql://127.0.0.1:3307/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true jdbc.slave01.username=root jdbc.slave01.password=123456

定義連接池

<!-- 配置連接池 --> <bean id="masterDataSource" class="com.jolbox.bonecp.BoneCPDataSource"  destroy-method="close">  <!-- 數(shù)據(jù)庫驅(qū)動(dòng) -->  <property name="driverClass" value="${jdbc.master.driver}" />  <!-- 相應(yīng)驅(qū)動(dòng)的jdbcUrl -->  <property name="jdbcUrl" value="${jdbc.master.url}" />  <!-- 數(shù)據(jù)庫的用戶名 -->  <property name="username" value="${jdbc.master.username}" />  <!-- 數(shù)據(jù)庫的密碼 -->  <property name="password" value="${jdbc.master.password}" />  <!-- 檢查數(shù)據(jù)庫連接池中空閑連接的間隔時(shí)間,單位是分,默認(rèn)值:240,如果要取消則設(shè)置為0 -->  <property name="idleConnectionTestPeriod" value="60" />  <!-- 連接池中未使用的鏈接最大存活時(shí)間,單位是分,默認(rèn)值:60,如果要永遠(yuǎn)存活設(shè)置為0 -->  <property name="idleMaxAge" value="30" />  <!-- 每個(gè)分區(qū)最大的連接數(shù) -->  <property name="maxConnectionsPerPartition" value="150" />  <!-- 每個(gè)分區(qū)最小的連接數(shù) -->  <property name="minConnectionsPerPartition" value="5" /> </bean>   <!-- 配置連接池 --> <bean id="slave01DataSource" class="com.jolbox.bonecp.BoneCPDataSource"  destroy-method="close">  <!-- 數(shù)據(jù)庫驅(qū)動(dòng) -->  <property name="driverClass" value="${jdbc.slave01.driver}" />  <!-- 相應(yīng)驅(qū)動(dòng)的jdbcUrl -->  <property name="jdbcUrl" value="${jdbc.slave01.url}" />  <!-- 數(shù)據(jù)庫的用戶名 -->  <property name="username" value="${jdbc.slave01.username}" />  <!-- 數(shù)據(jù)庫的密碼 -->  <property name="password" value="${jdbc.slave01.password}" />  <!-- 檢查數(shù)據(jù)庫連接池中空閑連接的間隔時(shí)間,單位是分,默認(rèn)值:240,如果要取消則設(shè)置為0 -->  <property name="idleConnectionTestPeriod" value="60" />  <!-- 連接池中未使用的鏈接最大存活時(shí)間,單位是分,默認(rèn)值:60,如果要永遠(yuǎn)存活設(shè)置為0 -->  <property name="idleMaxAge" value="30" />  <!-- 每個(gè)分區(qū)最大的連接數(shù) -->  <property name="maxConnectionsPerPartition" value="150" />  <!-- 每個(gè)分區(qū)最小的連接數(shù) -->  <property name="minConnectionsPerPartition" value="5" /> </bean>

定義DataSource

<!-- 定義數(shù)據(jù)源,使用自己實(shí)現(xiàn)的數(shù)據(jù)源 --> <bean id="dataSource" class="cn.itcast.usermanage.spring.DynamicDataSource">  <!-- 設(shè)置多個(gè)數(shù)據(jù)源 -->  <property name="targetDataSources">   <map key-type="java.lang.String">    <!-- 這個(gè)key需要和程序中的key一致 -->    <entry key="master" value-ref="masterDataSource"/>    <entry key="slave" value-ref="slave01DataSource"/>   </map>  </property>  <!-- 設(shè)置默認(rèn)的數(shù)據(jù)源,這里默認(rèn)走寫庫 -->  <property name="defaultTargetDataSource" ref="masterDataSource"/> </bean>

配置事務(wù)管理與動(dòng)態(tài)切面

定義事務(wù)管理器

<!-- 定義事務(wù)管理器 --> <bean id="transactionManager"  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  <property name="dataSource" ref="dataSource" /> </bean>

定義事務(wù)策略

<!-- 定義事務(wù)策略 --> <tx:advice id="txAdvice" transaction-manager="transactionManager">  <tx:attributes>   <!--定義查詢方法都是只讀的 -->   <tx:method name="query*" read-only="true" />   <tx:method name="find*" read-only="true" />   <tx:method name="get*" read-only="true" />     <!-- 主庫執(zhí)行操作,事務(wù)傳播行為定義為默認(rèn)行為 -->   <tx:method name="save*" propagation="REQUIRED" />   <tx:method name="update*" propagation="REQUIRED" />   <tx:method name="delete*" propagation="REQUIRED" />     <!--其他方法使用默認(rèn)事務(wù)策略 -->   <tx:method name="*" />  </tx:attributes> </tx:advice>

定義切面

<!-- 定義AOP切面處理器 --> <bean class="cn.itcast.usermanage.spring.DataSourceAspect" id="dataSourceAspect" />   <aop:config>  <!-- 定義切面,所有的service的所有方法 -->  <aop:pointcut id="txPointcut" expression="execution(* xx.xxx.xxxxxxx.service.*.*(..))" />  <!-- 應(yīng)用事務(wù)策略到Service切面 -->  <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>    <!-- 將切面應(yīng)用到自定義的切面處理器上,-9999保證該切面優(yōu)先級最高執(zhí)行 -->  <aop:aspect ref="dataSourceAspect" order="-9999">   <aop:before method="before" pointcut-ref="txPointcut" />  </aop:aspect> </aop:config>

改進(jìn)切面實(shí)現(xiàn)

之前的實(shí)現(xiàn)我們是將通過方法名匹配,而不是使用事務(wù)策略中的定義,我們使用事務(wù)管理策略中的規(guī)則匹配。

改進(jìn)后的配置

<!-- 定義AOP切面處理器 --> <bean class="cn.itcast.usermanage.spring.DataSourceAspect" id="dataSourceAspect">  <!-- 指定事務(wù)策略 -->  <property name="txAdvice" ref="txAdvice"/>  <!-- 指定slave方法的前綴(非必須) -->  <property name="slaveMethodStart" value="query,find,get"/> </bean>

改進(jìn)后的實(shí)現(xiàn)

import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Map;   import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttributeSource; import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.PatternMatchUtils; import org.springframework.util.ReflectionUtils;   /**  * 定義數(shù)據(jù)源的AOP切面,該類控制了使用Master還是Slave。  * 如果事務(wù)管理中配置了事務(wù)策略,則采用配置的事務(wù)策略中的標(biāo)記了ReadOnly的方法是用Slave,其它使用Master。  * 如果沒有配置事務(wù)管理的策略,則采用方法名匹配的原則,以query、find、get開頭方法用Slave,其它用Master。  * @author binghe  *  */ public class DataSourceAspect {       private List<String> slaveMethodPattern = new ArrayList<String>();          private static final String[] defaultSlaveMethodStart = new String[]{ "query", "find", "get" };          private String[] slaveMethodStart;       /**      * 讀取事務(wù)管理中的策略      * @param txAdvice      * @throws Exception      */     @SuppressWarnings("unchecked")     public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception {         if (txAdvice == null) {             // 沒有配置事務(wù)管理策略             return;         }         //從txAdvice獲取到策略配置信息         TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource();         if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) {             return;         }         //使用反射技術(shù)獲取到NameMatchTransactionAttributeSource對象中的nameMap屬性值         NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource;         Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");         nameMapField.setAccessible(true); //設(shè)置該字段可訪問         //獲取nameMap的值         Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField.get(matchTransactionAttributeSource);           //遍歷nameMap         for (Map.Entry<String, TransactionAttribute> entry : map.entrySet()) {             if (!entry.getValue().isReadOnly()) {//判斷之后定義了ReadOnly的策略才加入到slaveMethodPattern                 continue;             }             slaveMethodPattern.add(entry.getKey());         }     }       /**      * 在進(jìn)入Service方法之前執(zhí)行      *       * @param point 切面對象      */     public void before(JoinPoint point) {         // 獲取到當(dāng)前執(zhí)行的方法名         String methodName = point.getSignature().getName();           boolean isSlave = false;           if (slaveMethodPattern.isEmpty()) {             // 當(dāng)前Spring容器中沒有配置事務(wù)策略,采用方法名匹配方式             isSlave = isSlave(methodName);         } else {             // 使用策略規(guī)則匹配             for (String mappedName : slaveMethodPattern) {                 if (isMatch(methodName, mappedName)) {                     isSlave = true;                     break;                 }             }         }           if (isSlave) {             // 標(biāo)記為讀庫             DynamicDataSourceHolder.markSlave();         } else {             // 標(biāo)記為寫庫             DynamicDataSourceHolder.markMaster();         }     }       /**      * 判斷是否為讀庫      *       * @param methodName      * @return      */     private Boolean isSlave(String methodName) {         // 方法名以query、find、get開頭的方法名走從庫         return StringUtils.startsWithAny(methodName, getSlaveMethodStart());     }       /**      * 通配符匹配      *       * Return if the given method name matches the mapped name.      * <p>      * The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, as well as direct      * equality. Can be overridden in subclasses.      *       * @param methodName the method name of the class      * @param mappedName the name in the descriptor      * @return if the names match      * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)      */     protected boolean isMatch(String methodName, String mappedName) {         return PatternMatchUtils.simpleMatch(mappedName, methodName);     }       /**      * 用戶指定slave的方法名前綴      * @param slaveMethodStart      */     public void setSlaveMethodStart(String[] slaveMethodStart) {         this.slaveMethodStart = slaveMethodStart;     }       public String[] getSlaveMethodStart() {         if(this.slaveMethodStart == null){             // 沒有指定,使用默認(rèn)             return defaultSlaveMethodStart;         }         return slaveMethodStart;     }      }

一主多從的實(shí)現(xiàn)

很多實(shí)際使用場景下都是采用“一主多從”的架構(gòu)的,所有我們現(xiàn)在對這種架構(gòu)做支持,目前只需要修改DynamicDataSource即可。

Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離

實(shí)現(xiàn)

import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger;   import javax.sql.DataSource;   import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.util.ReflectionUtils;   /**  * 定義動(dòng)態(tài)數(shù)據(jù)源,實(shí)現(xiàn)通過集成Spring提供的AbstractRoutingDataSource,只需要實(shí)現(xiàn)determineCurrentLookupKey方法即可  * 由于DynamicDataSource是單例的,線程不安全的,所以采用ThreadLocal保證線程安全,由DynamicDataSourceHolder完成。  * @author binghe  *  */ public class DynamicDataSource extends AbstractRoutingDataSource {       private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);       private Integer slaveCount;       // 輪詢計(jì)數(shù),初始為-1,AtomicInteger是線程安全的     private AtomicInteger counter = new AtomicInteger(-1);       // 記錄讀庫的key     private List<Object> slaveDataSources = new ArrayList<Object>(0);       @Override     protected Object determineCurrentLookupKey() {         // 使用DynamicDataSourceHolder保證線程安全,并且得到當(dāng)前線程中的數(shù)據(jù)源key         if (DynamicDataSourceHolder.isMaster()) {             Object key = DynamicDataSourceHolder.getDataSourceKey();              if (LOGGER.isDebugEnabled()) {                 LOGGER.debug("當(dāng)前DataSource的key為: " + key);             }             return key;         }         Object key = getSlaveKey();         if (LOGGER.isDebugEnabled()) {             LOGGER.debug("當(dāng)前DataSource的key為: " + key);         }         return key;       }       @SuppressWarnings("unchecked")     @Override     public void afterPropertiesSet() {         super.afterPropertiesSet();           // 由于父類的resolvedDataSources屬性是私有的子類獲取不到,需要使用反射獲取         Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources");         field.setAccessible(true); // 設(shè)置可訪問           try {             Map<Object, DataSource> resolvedDataSources = (Map<Object, DataSource>) field.get(this);             // 讀庫的數(shù)據(jù)量等于數(shù)據(jù)源總數(shù)減去寫庫的數(shù)量             this.slaveCount = resolvedDataSources.size() - 1;             for (Map.Entry<Object, DataSource> entry : resolvedDataSources.entrySet()) {                 if (DynamicDataSourceHolder.MASTER.equals(entry.getKey())) {                     continue;                 }                 slaveDataSources.add(entry.getKey());             }         } catch (Exception e) {             LOGGER.error("afterPropertiesSet error! ", e);         }     }       /**      * 輪詢算法實(shí)現(xiàn)      *       * @return      */     public Object getSlaveKey() {         // 得到的下標(biāo)為:0、1、2、3&hellip;&hellip;         Integer index = counter.incrementAndGet() % slaveCount;         if (counter.get() > 9999) { // 以免超出Integer范圍             counter.set(-1); // 還原         }         return slaveDataSources.get(index);     }   }

MySQL主從復(fù)制

原理

Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離

MySQL主(master)從(slave)復(fù)制的原理:

  • master將數(shù)據(jù)改變記錄到二進(jìn)制日志(binarylog)中,也即是配置文件log-bin指定的文件(這些記錄叫做二進(jìn)制日志事件,binary log  events)

  • slave將master的binary logevents拷貝到它的中繼日志(relay log)

  • slave重做中繼日志中的事件,將改變反映它自己的數(shù)據(jù)(數(shù)據(jù)重演)

主從配置需要注意的地方

  • 主DB server和從DB  server數(shù)據(jù)庫的版本一致

  • 主DB server和從DB server數(shù)據(jù)庫數(shù)據(jù)一致[  這里就會(huì)可以把主的備份在從上還原,也可以直接將主的數(shù)據(jù)目錄拷貝到從的相應(yīng)數(shù)據(jù)目錄]

  • 主DB server開啟二進(jìn)制日志,主DB server和從DB  server的server_id都必須唯一

主庫配置(windows,Linux下也類似)

在my.ini修改:

#開啟主從復(fù)制,主庫的配置 log-bin = mysql3306-bin #指定主庫serverid server-id=101 #指定同步的數(shù)據(jù)庫,如果不指定則同步全部數(shù)據(jù)庫 binlog-do-db=mybatis_1128

執(zhí)行SQL語句查詢狀態(tài):

SHOW MASTER STATUS

Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離

需要記錄下Position值,需要在從庫中設(shè)置同步起始值。

在主庫創(chuàng)建同步用戶

#授權(quán)用戶slave01使用123456密碼登錄mysql grant replication slave on *.* to 'slave01'@'127.0.0.1' identified by '123456'; flush privileges;

從庫配置

在my.ini修改

#指定serverid,只要不重復(fù)即可,從庫也只有這一個(gè)配置,其他都在SQL語句中操作 server-id=102

接下來,從從庫命令行執(zhí)行如下SQL語句。

CHANGE MASTER TO  master_host='127.0.0.1',  master_user='slave01',  master_password='123456',  master_port=3306,  master_log_file='mysql3306-bin.000006',  master_log_pos=1120;   #啟動(dòng)slave同步 START SLAVE;   #查看同步狀態(tài) SHOW SLAVE STATUS;

到此,關(guān)于“Spring如何實(shí)現(xiàn)多數(shù)據(jù)源讀寫分離”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI