目录
前言
在数据量非常大的情况下,在读写操作频繁的情况下,如果服务器硬件设施不够NB,就很容易出现性能低下的问题。最常见的做法有升级硬件设施、分库分表或者读写分离等。
硬件设施昂贵,项目资金有限不允许,怎么办?当然,可以从数据库访问操作方面入手咯,通过分库分表或者读写分离来提高数据库的性能,以提高系统整体的性能。
一般实现分库分表或者读写分离的实现是通过数据库中间件来实现的,常见的数据库中间件有:ShardingSphere、MyCat、DRDS等,本篇文章主要就MyCat的配置作详细介绍,为后的文章作铺垫。
为什么要出这篇文章?有很多人想实现分库分表又或者读写分离等,于是乎就到网上去收罗了一篇,一饨操作猛如虎,结果怎么也运行不起来,问博主,我这个怎启动不起来???,博主一看,配置错了!!!于是乎就有了这篇文章,知其然又知其所以然后,天高任我飞咯。
MyCat配置参数详解
一、MyCat中常用的三个配置
配置文件 | 说明 |
---|---|
schema.xml
|
Schema.xml作为MyCat中重要的配置文件之一,管理着MyCat的逻辑库、表、分片规则、DataNode以及DataSource。弄懂这些配置,是正确使用MyCat的前提。这里就一层层对该文件进行解析。
|
server.xml
|
server.xml几乎保存了所有mycat需要的系统配置信息。其在代码内直接的映射类为
SystemConfig
类。
|
rule.xml
|
rule.xml里面就定义了我们对表进行拆分所涉及到的规则定义。我们可以灵活的对表使用不同的分片算法,或者对表使用相同的算法但具体的参数不同。这个文件里面主要有tableRule
和
function
这两个标签。
|
二、rule.xml
rule.xml 里面就定义了对表进行拆分所涉及到的规则定义。通过此配置可以灵活的对表使用不同的分片算法,或者对表使用相同的算法但具体的参数不同。这个文件里面主要有 tableRule 和 function 这两个标签。在具体使用过程中可以按照需求添加 tableRule 和 function。
1、tableRule 标签
这个标签主要用来定义表规则。
<tableRule name="rule1">
<rule>
<columns>id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
属性名 | 类型 | 说明 |
---|---|---|
name
| String |
属性指定唯一的名字,用于标识不同的表规则。
内嵌的
rule
标签则指定对物理表中的哪一列进行拆分和使用什么路由算法。
|
columns
| String |
内指定要拆分的列名字。
|
algorithm
| String |
使用
function
标签中的
name
属性。连接表规则和具体路由算法。当然,多个表规则可以连接到同一个路由算法上。 标签内使用。让逻辑表使用这个规则进行分片。
|
2、function 标签
<function name="hash-int"
class="io.mycat.route.function.PartitionByFileMap">
<property name="mapFile">partition-hash-int.txt</property>
</function>
<function name="rang-long"
class="io.mycat.route.function.AutoPartitionByLong">
<property name="mapFile">autopartition-long.txt</property>
</function>
属性名 | 类型 | 说明 |
---|---|---|
name
| String |
指定算法的名字。
|
class
| String |
制定路由算法具体的类名字。
|
property
| String |
为具体算法需要用到的一些属性。
|
三、schema.xml
1、schema 标签
schema 标签用于定义MyCat实例中的逻辑库,MyCat可以有多个逻辑库,每个逻辑库都有自己的相关配置。可以使用 schema 标签来划分这些不同的逻辑库。
如果不配置 schema 标签,所有的表配置,会属于同一个默认的逻辑库。
<schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100" randomDataNode="dn1">
<!-- auto sharding by id (long) -->
<!-- splitTableNames 启用<table name 属性使用逗号分割配置多个表,即多个表使用这个配置 -->
<table name="travelrecord,address" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" splitTableNames ="true"/>
<!-- <table name="oc_call" primaryKey="ID" dataNode="dn1$0-743" rule="latest-month-calldate"/> -->
</schema>
schema 标签属性:
属性名 | 类型 | 说明 |
---|---|---|
dataNode
|
String
|
该属性用于绑定逻辑库到某个具体的database上,如果定义了这个属性,那么这个逻辑库就不能工作在分库分表模式下了。也就
是说对这个逻辑库的所有操作会直接作用到绑定的dataNode上,这个schema就可以用作读写分离和主从切换。
注意,该属性只能配置绑定到一个 database 上,不能绑定多个dn。
|
checkSQLschema
|
Boolean
|
当该值设置为
true
时,如果执行语句 “ select * from TESTDB.travelrecord; ” 则MyCat会把语句修改为 “ select * from travelrecord; ”。即把表示schema的字符去掉,避免发送到后端数据库执行时报 “ (ERROR 1146 (42S02): Table ‘testdb.travelrecord’ doesn’t exist)。” 不过,即使设置该值为 true
,如果语句所带的是并非是 schema 指定的名字,例如:“ select * from db1.travelrecord; ” 那么 MyCat并不会删除db1这个字段,如果没有定义该库的话则会报错,所以在提供SQL语句的最好是不带这个字段。
|
sqlMaxLimit
|
Integer
|
当该值设置为某个数值时。每条执行的SQL语句,如果没有加上limit语句,MyCat也会自动的加上所对应的值。例如设置值为 100,执行 “ select * from TESTDB.travelrecord; ” 的效果为和执行 “ select * from TESTDB.travelrecord limit 100; ”相同。 不设置该值的话,MyCat默认会把查询到的信息全部都展示出来,造成过多的输出。所以,在正常使用中,还是建议加上一个值,用于减少过多的数据返回。
当然SQL语句中也显式的指定limit的大小,不受该属性的约束。
注意,如果运行的schema为非拆分库的,那么该属性不会生效。需要手动添加limit语句。
|
2、table 标签
Table 标签定义了MyCat中的逻辑表,所有需要拆分的表都需要在这个标签中定义。
<!-- auto sharding by id (long) -->
<!--splitTableNames 启用<table name 属性使用逗号分割配置多个表,即多个表使用这个配置-->
<table name="travelrecord,address" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" splitTableNames ="true"/>
<!-- <table name="oc_call" primaryKey="ID" dataNode="dn1$0-743" rule="latest-month-calldate"/> -->
如果需要定义多个dn时,可以使用如下的方法来减少配置:
<table name="travelrecord" dataNode="multipleDn$0-99,multipleDn2$100-199" rule="auto-sharding-long" ></table>
<dataNode name="multipleDn" dataHost="localhost1" database="db$0-99" ></dataNode>
<dataNode name="multipleDn2" dataHost="localhost1" database=" db$0-99" ></dataNode>
这里需要注意的是 database 属性所指定的真实 database name 需要在后面添加一个,例如上面的例子中,需要在真实的mysql上 建立名称为dbs0到dbs99的 database。
table 标签属性:
属性名 | 类型 | 说明 |
---|---|---|
name | String |
定义逻辑表的表名,这个名字就如同我在数据库中执行create table命令指定的名字一样,同个schema标签中定义的名字必须唯一。
|
dataNode
| String |
定义这个逻辑表所属的dataNode, 该属性的值需要和dataNode标签中name属性的值相互对应。
|
rule | String |
该属性用于指定逻辑表要使用的规则名字,规则名字在
rule.xml
中定义,必须与
tableRule
标签中
name
属性属性值一一对应。
|
ruleRequired
| Boolean |
该属性用于指定表是否绑定分片规则,如果配置为true,但没有配置具体rule的话 ,程序会报错。
|
primaryKey
| String |
该逻辑表对应真实表的主键,例如:分片的规则是使用非主键进行分片的,那么在使用主键查询的时候,就会发送查询语句到所有配置的dn上,如果使用该属性配置真实表的主键。难么MyCat会缓存主键与具体dn的信息,那么再次使用非主键进行查询的时候就不会进行广播式的查询,就会直接发送语句给具体的dn,但是尽管配置该属性,如果缓存并没有命中的话,还是会发送语句给具体的dn,来获得数据。
|
type
| String |
该属性定义了逻辑表的类型,目前逻辑表只有“全局表”和”普通表”两种类型。
对应的配置:
全局表:global。
普通表:不指定该值为globla的所有表。
|
autoIncrement
| Boolean | mysql对非自增长主键,使用last_insert_id()是不会返回结果的,只会返回0。所以,只有定义了自增长主键的表才可以用last_insert_id()返回主键值。 mycat目前提供了自增长主键功能,但是如果对应的mysql节点上数据表,没有定义auto_increment,那么在mycat层调用last_insert_id()也是不会返回结果的。 由于insert操作的时候没有带入分片键,mycat会先取下这个表对应的全局序列,然后赋值给分片键。这样才能正常的插入到数据库中,最后使用last_insert_id()才会返回插入的分片键值。 如果要使用这个功能最好配合使用数据库模式的全局序列。 使用autoIncrement=“true” 指定这个表有使用自增长主键,这样mycat才会不抛出分片键找不到的异常。 使用autoIncrement=“false” 来禁用这个功能,当然你也可以直接删除掉这个属性。默认就是禁用的。 |
needAddLimit
| Boolean | 指定表是否需要自动的在每个语句后面加上limit限制。由于使用了分库分表,数据量有时会特别巨大。这时候执行查询语句,如果恰巧又忘记了加上数量限制的话。那么查询所有的数据出来,也够等上一小会儿的。 所以,mycat就自动的为我们加上LIMIT 100。当然,如果语句中有limit,就不会在次添加了。这个属性默认为true,你也可以设置成false禁用掉默认行为。 |
3、childTable 标签
属性名 | 类型 | 说明 |
---|---|---|
name
| String |
定义子表的表名。
|
joinKey
| String |
插入子表的时候会使用这个列的值查找父表存储的数据节点。
|
parentKey
| String | 属性指定的值一般为与父表建立关联关系的列名。程序首先获取joinkey的值,再通过 “ parentKey ”属性指定的列名产生查询语句,通过执行该语句得到父表存储在哪个分片上。从而确定子表存储的位置。 |
primaryKey
| String |
同 table 标签
primaryKey 属性。
|
needAddLimit
| String |
同 table 标签的
needAddLimit 属性。
|
4、dataNode 标签
<dataNode name="dn1" dataHost="localhost1" database="db1" />
<dataNode name="dn2" dataHost="localhost1" database="db2" />
<dataNode name="dn3" dataHost="localhost1" database="db3" />
dataNode 标签定义了MyCat中的数据节点,也就是我们通常所说的数据分片。一个 “ dataNode ” 标签就是一个独立的数据分片。
dataNode 标签中的属性:
属性名 | 类型 | 说明 |
---|---|---|
name
| String |
定义数据节点的名字,这个名字需要是唯一的,需要在table标签上应用这个名字,来建立表与分片对应的关系。
|
dataHost
| String |
该属性用于定义该分片属于哪个数据库实例的,属性值是引用 dataHost 标签上定义的name属性。
|
database
| String |
该属性用于定义该分片属性哪个具体数据库实例上的具体库,因为这里使用两个纬度来定义分片,就是:实例 + 具体的库。因为每个库上建立的表和表结构是一样的。所以这样做就可以轻松的对表进行水平拆分。
|
5、dataHost 标签
作为 Schema.xml 中最后的一个标签,该标签在 mycat 逻辑库中也是作为最底层的标签存在,直接定义了具体的数据库实例、读写分离配置和心跳语句。配置示例如下:
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0"
dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<!-- can have multi write hosts -->
<writeHost host="hostM1" url="localhost:3306"
user="root" password="123456">
</writeHost>
<!-- <writeHost host="hostM2" url="localhost:3316"
user="root" password="123456"/> -->
</dataHost>
dataHost 标签属性:
属性名 | 类型 | 说明 |
---|---|---|
name
| String |
唯一标识 dataHost 标签,供上层的标签使用。
|
maxCon
| Integer | 指定每个读写实例连接池的最大连接。也就是说,标签内嵌套的 writeHost、readHost 标签都会使用这个属性的值来实例化出连接池的最大连接数。 |
minCon
| Integer |
指定每个读写实例连接池的最小连接,初始化连接池的大小。
|
balance
| Integer |
负载均衡类型,目前的取值有3种:
1)、balance=“0”,所有读操作都发送到当前可用的 writeHost上。
2)、balance=“1”,所有读操作都随机的发送到 readHost。
3)、balance=“2”,所有读操作都随机的在 writeHost、readhost上分发。
|
writeType
| Integer |
写库的负载均衡类型,目前的取值有3种:
1)、writeType=“0”,所有写操作都发送到可用的 writeHost上。
2)、writeType=“1”,所有写操作都随机的发送到 readHost。
3)、writeType=“2”,所有写操作都随机的在 writeHost、readhost分上发。
|
dbType
| String | 指定后端连接的数据库类型,目前支持二进制的mysql协议,还有其他使用JDBC连接的数据库。例如:mongodb、oracle、spark等。 |
dbDriver
| String | 指定连接后端数据库使用的 Driver,目前可选的值有 native 和 JDBC。使用 native的话,因为这个值执行的是二进制的 mysql协议,所以可以使用 mysql 和 maridb。其他类型的数据库则需要使用 JDBC 驱动来支持。 如果使用JDBC的话需要将符合JDBC 4标准的驱动JAR包放到MYCAT\lib目录下,并检查驱动JAR包中包括如下目录结构的文件:META-INF\services\java.sql.Driver。在这个文件内写上具体的Driver类名,例如:com.mysql.jdbc.Driver。 |
6、heartbeat 标签
这个标签内指明用于和后端数据库进行心跳检查的语句。例如,MYSQL可以使用 select user(),Oracle 可以使用 select 1 from dual 等。
这个标签还有一个 connectionInitSql 属性,主要是当使用 Oracla 数据库时,需要执行的初始化SQL语句就这个放到这里面来。例如:alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss'
7、writeHost 标签、readHost 标签
这两个标签都指定后端数据库的相关配置给mycat,用于实例化后端连接池。唯一不同的是,writeHost 指定写实例、readHost 指定读实例,组着这些读写实例来满足系统的要求。
在一个 dataHost 内可以定义多个 writeHost 和 readHost。但是,如果 writeHost指定的后端数据库宕机,那么这个writeHost绑定的所有readHost都将不可用。另一方面,由于这个writeHost宕机系统会自动的检测到,并切换到备用的writeHost上去。
这两个标签的属性相同
属性名 | 类型 | 说明 |
---|---|---|
host
|
String
|
用于标识不同实例,一般 writeHost 一般使用 ->M1,readHost 一般使用 ->S1。
|
url
|
String
| 后端实例连接地址,如果是使用 native 的 dbDriver,则一般为 address:port 这种形式。用JDBC或其他的dbDriver,则需要特殊指定。当使用JDBC时则可以这么写:jdbc:mysql://localhost:3306/。 |
password
|
String
|
后端存储实例需要的密码。
|
user
|
String
|
后端存储实例需要的用户名字。
|
四、server.xml
server.xml 几乎保存了所有mycat需要的系统配置信息。
1、user 标签
<user name="root" defaultAccount="true">
<property name="password">123456</property>
<property name="schemas">TESTDB</property>
<property name="defaultSchema">TESTDB</property>
<!--No MyCAT Database selected 错误前会尝试使用该schema作为schema,不设置则为null,报错 -->
<!-- 表级 DML 权限设置 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<user name="user">
<property name="password">user</property>
<property name="schemas">TESTDB</property>
<property name="readOnly">true</property>
<property name="defaultSchema">TESTDB</property>
</user>
server.xml 中的标签本就不多,这个标签主要用于定义登录mycat的用户和权限。例如上面的例子中,定义了一个用户,用户名为root、密码为123456,用户名为user,密码也为user,可访问的schema也只有TESTDB一个。
如果在schema.xml中定义了多个schema,那么这个用户是无法访问其他的schema。在mysql客户端看来则是无法使用use切换到这个其他的数据库。如果使用了use命令,则mycat会报出这样的错误提示:
“ ERROR 1044 (HY000): Access denied for user 'test' to database 'xxx' ”
这个标签嵌套的 property标签则是具体声明的属性值,正如上面的例子。可以修改user标签的 name属性来指定用户名;修改 password内的文本来修改密码;修改 readOnly为 true 或 false 来限制用户是否只是可读的;修改 schemas内的文本来控制用户可放问的 schema;修改 schemas内的文本来控制用户可访问的 schema,同时访问多个 schema的话使用 , 隔开,例如:
<property name="schemas">TESTDB,db1,db2,db3</property>
2、system 标签
这个标签内嵌套的所有property标签都与系统配置有关,配置示例如下:
<system>
<property name="nonePasswordLogin">0</property> <!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0,设置为1则需要指定默认账户-->
<property name="ignoreUnknownCommand">0</property><!-- 0遇上没有实现的报文(Unknown command:),就会报错、1为忽略该报文,返回ok报文。
在某些mysql客户端存在客户端已经登录的时候还会继续发送登录报文,mycat会报错,该设置可以绕过这个错误-->
<property name="useHandshakeV10">1</property>
<property name="removeGraveAccent">1</property>
<property name="useSqlStat">0</property> <!-- 1为开启实时统计、0为关闭 -->
<property name="useGlobleTableCheck">0</property> <!-- 1为开启全加班一致性检测、0为关闭 -->
<property name="sqlExecuteTimeout">300</property> <!-- SQL 执行超时 单位:秒-->
<property name="sequnceHandlerType">1</property>
<!--<property name="sequnceHandlerPattern">(?:(\s*next\s+value\s+for\s*MYCATSEQ_(\w+))(,|\)|\s)*)+</property>
INSERT INTO `travelrecord` (`id`,user_id) VALUES ('next value for MYCATSEQ_GLOBAL',"xxx");
-->
<!--必须带有MYCATSEQ_或者 mycatseq_进入序列匹配流程 注意MYCATSEQ_有空格的情况-->
<property name="sequnceHandlerPattern">(?:(\s*next\s+value\s+for\s*MYCATSEQ_(\w+))(,|\)|\s)*)+</property>
<property name="subqueryRelationshipCheck">false</property> <!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false -->
<property name="sequenceHanlderClass">io.mycat.route.sequence.handler.HttpIncrSequenceHandler</property>
<!-- <property name="useCompression">1</property>--> <!--1为开启mysql压缩协议-->
<!-- <property name="fakeMySQLVersion">5.6.20</property>--> <!--设置模拟的MySQL版本号-->
<!-- <property name="processorBufferChunk">40960</property> -->
<!--
<property name="processors">1</property>
<property name="processorExecutor">32</property>
-->
<!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool -->
<property name="processorBufferPoolType">0</property>
<!--默认是65535 64K 用于sql解析时最大文本长度 -->
<!--<property name="maxStringLiteralLength">65535</property>-->
<!--<property name="sequnceHandlerType">0</property>-->
<!--<property name="backSocketNoDelay">1</property>-->
<!--<property name="frontSocketNoDelay">1</property>-->
<!--<property name="processorExecutor">16</property>-->
<!--
<property name="serverPort">8066</property> <property name="managerPort">9066</property>
<property name="idleTimeout">300000</property> <property name="bindIp">0.0.0.0</property>
<property name="dataNodeIdleCheckPeriod">300000</property> 5 * 60 * 1000L; //连接空闲检查
<property name="frontWriteQueueSize">4096</property> <property name="processors">32</property> -->
<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,>但是记录分布式事务日志-->
<property name="handleDistributedTransactions">0</property>
<!--
off heap for merge/order/group/limit 1开启 0关闭
-->
<property name="useOffHeapForMerge">0</property>
<!--
单位为m
-->
<property name="memoryPageSize">64k</property>
<!--
单位为k
-->
<property name="spillsFileBufferSize">1k</property>
<property name="useStreamOutput">0</property>
<!--
单位为m
-->
<property name="systemReserveMemorySize">384m</property>
<!--是否采用zookeeper协调切换 -->
<property name="useZKSwitch">false</property>
<!-- XA Recovery Log日志路径 -->
<!--<property name="XARecoveryLogBaseDir">./</property>-->
<!-- XA Recovery Log日志名称 -->
<!--<property name="XARecoveryLogBaseName">tmlog</property>-->
<!--如果为 true的话 严格遵守隔离级别,不会在仅仅只有select语句的时候在事务中切换连接-->
<property name="strictTxIsolation">false</property>
<property name="useZKSwitch">true</property>
<!--如果为0的话,涉及多个DataNode的catlet任务不会跨线程执行-->
<property name="parallExecute">0</property>
</system>
这儿介绍部分属性,其余数据在同上或已在配置中注释存在的:
属性名 | 类型 | 说明 |
---|---|---|
defaultSqlParser
| String | 由于mycat最初是时候Foundation DB的sql解析器,而后才添加的Druid的解析器。所以这个属性用来指定默认的解析器。目前的可用的取值有:druidparser 和 fdbparser。使用的时候可以选择其中的一种,目前一般都使用 druidparser。 |
processors
| String |
这个属性主要用于指定系统可用的线程数,默认值为
Runtime.getRuntime().availableProcessors()
方法返回的值。主要影响 processorBufferPool、
processorBufferLocalPercent
、
processorExecutor
属性。
NIOProcessor
的个数也是由这个属性定义的, 所以调优的时候可以适当的调高这个属性。
|
processorBufferChunk
| Byte |
这个属性指定每次分配 Socket Direct Buffer 的大小,默认是 4096个字节。这个属性也影响 buffer pool的长度。
|
processorBufferPool
| Byte | 这个属性指定 bufferPool 计算比例值。由于每次执行 NIO 读、写操作都需要使用到buffer,系统初始化的时候会建立一定长度的 buffer 池来加快读、写的效率,减少建立 buffer的时间。 |
processorBufferLocalPercent
| Byte |
前面提到了
ThreadLocalPool
。这个属性就是用来控制分配这个 pool 的大小用的,但其也并不是一个准确的值,也是一个比例值。这个属性默认值为100。
|
processorExecutor
| Byte | 这个属性主要用于指定 NIOProcessor 上共享的 businessExecutor 固定线程池大小。mycat 在需要处理一些异步逻辑的时候会把任务提交到这个线程池中。新版本中这个连接池的使用频率不是很大了,可以设置一个较小的值。 |
sequnceHandlerType
| String | 指定使用 Mycat 全局序列的类型。0为本地文件方式,1为数据库方式。默认是使用本地文件方式,文件方式主要只是用于测试使用。 |
* 注:
Mycat 中有两个主要的 buffer池:
- BufferPool
- ThreadLocalPool
BufferPool 由 ThreadLocalPool 组合而成,每次从 BufferPool 中获取 buffer 都会优先获取 ThreadLocalPool 中的buffer,未命中之后才会去获取 BufferPool 中的 buffer。也就是说 ThreadLocalPool 是作为 BufferPool 的二级缓存,每个线程内部自己使用的。当然,这其中还有一些限制条件需要线程的名字是由 $_ 开头。然而,BufferPool 上的 buffer 则是每个NIOProcessor都共享的。
默认这个属性的值为:
默认 bufferChunkSize(4096) * processors 属性 * 1000
BufferPool 的总长度 = bufferPool / bufferChunk
若 bufferPool 不是 bufferChunk 的整数倍,则总长度为前面计算得出的商 + 1,假设系统线程数为 4,其他都为属性的默认值,则:
bufferPool = 4096 * 4 * 1000
BufferPool 的总长度:4000 = 16384000 / 4096
参考文献:
MyCat官网:【MyCat官方网站】
GitHub:【MyCATApache】
Issues:【Mycat-Server-issues】
MyCat指南:【MyCat指南CSDN】
好了,关于 MySQL中间件MyCat配置参数列表,schema.xml ,server.xml,rule.xml,配置参数详情,Mycat中的两个主要Buffer池,BufferPool的二级缓存 就写到这儿了,如果还有什么疑问或遇到什么问题欢迎扫码提问,也可以给我留言哦,我会一一详细的解答的。
歇后语:“ 共同学习,共同进步 ”,也希望大家多多关注CSND的IT社区。
作 者: | 华 仔 |
联系作者: | who.seek.me@java98k.vip |
来 源: | CSDN (Chinese Software Developer Network) |
原 文: | https://blog.csdn.net/Hello_World_QWP/article/details/105050152 |
版权声明: | 本文为博主原创文章,请在转载时务必注明博文出处! |