海量数据使用dbms_parallel_execute执行并行更新

0    109    1

👉 本文共约10456个字,系统预计阅读时间或需40分钟。

海量数据处理,是很多系统开发人员,有时候甚至是运维人员,经常面对的需求。接口海量数据文件加载入库、批量数据更新、阶段数据归档删除是我们经常遇到的应用需求。针对不同的实际情况,包括软硬件、运维环境、SLA窗口期要求,我们需要采用不同的策略和方法进行问题解决。

在笔者之前文章《Oracle中如何更新一张大表记录》(http://blog.itpub.net/17203031/viewspace-1061065/)中,介绍了以Oracle数据库端为中心,进行大表数据处理过程中的一些方法和考虑因素。简单的说,海量数据处理难点不在语句层面,而在如何平衡各种需求因素。比较常见的因素有如下

ü 业务系统正常生产冲击。大数据操作绝大多数场景是在生产环境。在7*24可用性需求日益强化的今天,业务系统一个SQL运行之后,影响减慢核心操作速度,严重甚至系统崩溃,绝对不是我们运维人员希望见到的;

ü 操作窗口期长短。在相同的业务操作量的情况下,平缓化操作负载一定是以增加操作时间作为前提的。增加延长操作时间是否能够在维护窗口内完成,也是需要考量的问题;

ü 对数据一致性的影响。一些“流言”方法(如nologging),虽然可以减少操作负载,但是潜在会给系统备份连续性带来灾难影响;

此外,SQL语句本身优化,操作策略也会有一些可以提高的空间。但是,一些问题还是需要单纯的大量数据处理。当其他常规手段出尽的时候,在硬件条件允许下,并行、并发操作往往是不错的选择。

在11gR2中,Oracle为海量数据处理提供了很多方便的支持。工具包dbms_parallel_execute可以支持将海量数据分拆为独立的chunk任务,并行执行作业。本篇就详细介绍这个新特性的使用。

1、环境准备

实验环境为11.2.0.3。

SQL> select * from v$version;

BANNER

------------------------------------------

Oracle Database 11g Enterprise Edition Release 11.2.0.3.0 - Production

PL/SQL Release 11.2.0.3.0 - Production

CORE 11.2.0.3.0 Production

TNS for Linux: Version 11.2.0.3.0 - Production

NLSRTL Version 11.2.0.3.0 – Production

构造一张大表。说明:条件所限,笔者环境比较简单,一些性能方面的优势比较难体现出来。先创建出一个单独表空间。

SQL> create tablespace test datafile size 2G autoextend on

2 extent management local uniform size 1m

3 segment space management auto;

Tablespace created

SQL> create table t as select * from dba_objects;

Table created

SQL> insert into t select * from t;

75586 rows inserted

(一系列的insert操作……)

SQL> commit;

Commit complete

数据表T包括大约2千万条记录,占用空间体积在2G左右。

SQL> select count(*) from t;

COUNT(*)

----------

19350016

SQL> select bytes/1024/1024/1024, tablespace_name from dba_segments where owner='SYS' and segment_name='T';

本人提供Oracle、MySQL、PG等数据库的培训和考证业务,私聊QQ646634621或微信db_bao,谢谢!

BYTES/1024/1024/1024 TABLESPACE_NAME


​ 2.0986328125 TEST

Dbms_parallel_execute并不是传统的多进程并行操作,本质上是通过作业管理器Schedule来完成系列作业的(在后文中会详细证明)。所以前提要求job_queue_processes参数设置不能为0。

SQL> show parameter job

NAME TYPE VALUE


job_queue_processes integer 1000

2、dbms_parallel_execute包执行介绍

Dbms_parallel_execute是Oracle 11g推出的一个全新并行操作接口。它的原理为:当Oracle需要处理一个大量数据处理,特别是update操作的时候,可以将其拆分为若干各chunk分块,以多进程作业(Schedule Job)分块执行操作。从而降低一次性undo的使用,更进一步的便于断点续作。

Dbms_parallel_execute包使用要满足两个条件:

ü 执行程序用户需要拥有create job系统权限;

ü Dbms_parallel_execute程序包执行中需要调用dbms_sql包的一些方法,所以也需要该程序包执行权限;

并行包的执行有两个问题需要调用者确定:chunk分割方法和并行作业进程个数。

传统的单线程执行策略中,无论任务多大,都是对应一个Server Process进行处理。如果调用了并行,会有对应的协调进程和工作进程存在(v$px_process)。

如果启用了并行执行,一个关键问题在于如何划分任务,将一个数据表更新操作划分为多个小数据集合操作。Dbms_parallel_execute包支持三种任务划分方法。

ü By_rowid方法:依据rowid将操作数据进行划分;

ü By_number_col方法:输入定义一个数字列名称,依据这个列的取值进行划分;

ü By_SQL语句方法:给一个SQL语句,用户可以帮助定义出每次chunk的起始和终止id取值;

在三种方法中,笔者比较推荐rowid方法,理由是条件要求低、操作速度快。如果操作过程中没有明确的对数据表作业,这种策略是首选。具体比较可以从下面的实验中看出。

确定了划分方法,还要确定每个chunk的大小。注意:这个chunk设置大小并不一定是每个chunk操作数据行的数量。针对不同的分区类型,有不同的策略。这个在下面实验中笔者也会给出明确的解析。

并行进程个数表示的是当“一块”任务被划分为“一堆”相互独立的任务集合之后,准备多少个工作进程进行工作。这个是并行包使用的关键,类似于并行度,是需要依据实际软硬件资源负载情况综合考虑。

长时间作业存在一个问题,就是调用用户希望随时了解执行情况。Oracle提供了两个数据视图user_parallel_execute_tasks和user_parallel_execute_chunks,分别查看Task执行情况和各个chunk执行完成情况。

在Oracle官方文档中,给出了调用dbms_parallel_execute包的方法流程,本文使用的也就是这个脚本的变种,特此说明。下面,我们先看第一种by rowid方法。

3、By Rowid划分chunk方法

Oracle中的rowid是数据实际物理位置的表示。借助rowid直接定位数据,是目前Oracle获取数据最快的方法。所以在RBO中,第一执行计划被确定为rowid访问方式。

依据Oracle文档提供的PL/SQL匿名块,修改处我们第一个rowid范围查询。

declare

vc_task varchar2(100);

vc_sql varchar2(1000);

n_try number;

n_status number;

begin

--Define the Task

vc_task := 'Task 1: By Rowid'; --Task名称

dbms_parallel_execute.create_task(task_name => vc_task); --手工定义一个Task任务;

--Define the Spilt

dbms_parallel_execute.create_chunks_by_rowid(task_name => vc_task,

​ table_owner => 'SYS',

​ table_name => 'T',

​ by_row => true,

​ chunk_size => 1000); --定义Chunk

vc_sql := 'update /+ ROWID(dda) /t set DATA_OBJECT_ID=object_id+1 where rowid between :start_id and :end_id';

--Run the task

dbms_parallel_execute.run_task(task_name => vc_task,

​ sql_stmt => vc_sql,

​ language_flag => dbms_sql.native,

​ parallel_level => 2); --执行任务,确定并行度

--Controller

n_try := 0;

n_status := dbms_parallel_execute.task_status(task_name => vc_task);

while (n_try<2 and n_status != dbms_parallel_execute.FINISHED) loop

dbms_parallel_execute.resume_task(task_name => vc_task);

n_status := dbms_parallel_execute.task_status(task_name => vc_task);

end loop;

--Deal with Result

dbms_parallel_execute.drop_task(task_name => vc_task);

end;

/

从调用过程来看,这个并行操作包括下面几个步骤:

ü 定义Task;

ü 确定chunk划分方法,定义每个chunk的范围信息;

ü 执行作业,确定并行作业进程数量;

这个调用过程和我们常见的并行方式有很大差异,类似于Oracle的Job Schedule机制。由于执行过程比较长,我们可以有比较从容的查看并行执行包的情况。

从user_parallel_execute_tasks中,看到当前作业的关键信息。注意:chunk_type表示的是采用什么样的划分方法。JOB_PREFIX对应的则是Schedule中的内容。

SQL> select task_name, chunk_type, JOB_PREFIX from user_parallel_execute_tasks;

TASK_NAME CHUNK_TYPE JOB_PREFIX


Task 1: By Rowid ROWID_RANGE TASK$_4

在user_parallel_execute_chunks中,作业的所有chunk划分,每个chunk对应的一行数据。其中包括这个chunk的起始和截止rowid。对应的chunk取值对应的就是每个chunk的数据行数。

SQL> select chunk_id, task_name, status, start_rowid, end_rowid from user_parallel_execute_chunks where rownum<10;

CHUNK_ID TASK_NAME STATUS START_ROWID END_ROWID


​ 1 Task 1: By Rowid PROCESSED AAATLKAAHAAAACAAAA AAATLKAAHAAAACxCcP

​ 2 Task 1: By Rowid PROCESSED AAATLKAAHAAAACyAAA AAATLKAAHAAAADjCcP

​ 3 Task 1: By Rowid PROCESSED AAATLKAAHAAAADkAAA AAATLKAAHAAAAD/CcP

​ 4 Task 1: By Rowid PROCESSED AAATLKAAHAAAAEAAAA AAATLKAAHAAAAExCcP

​ 5 Task 1: By Rowid PROCESSED AAATLKAAHAAAAEyAAA AAATLKAAHAAAAFjCcP

​ 6 Task 1: By Rowid PROCESSED AAATLKAAHAAAAFkAAA AAATLKAAHAAAAF/CcP

​ 7 Task 1: By Rowid PROCESSED AAATLKAAHAAAAGAAAA AAATLKAAHAAAAGxCcP

​ 8 Task 1: By Rowid PROCESSED AAATLKAAHAAAAGyAAA AAATLKAAHAAAAHjCcP

​ 9 Task 1: By Rowid PROCESSED AAATLKAAHAAAAHkAAA AAATLKAAHAAAAH/CcP

9 rows selected

作为user_parallel_execute_chunks,一个很重要的字段就是status状态列,用于标注每个chunk的处理情况。我们可以依据这个字段来判断任务完成情况。

SQL> select status, count(*) from user_parallel_execute_chunks group by status;

STATUS COUNT(*)


ASSIGNED 2

UNASSIGNED 5507

PROCESSED 938

(过一会之后…….)

SQL> select status, count(*) from user_parallel_execute_chunks group by status;

STATUS COUNT(*)


ASSIGNED 2

UNASSIGNED 5441

PROCESSED 1004

从status字段,我们可以分析出并行作业工作的原理。每一个chunk记录在划分之后,都是设置为unassiged状态,包括起始和终止的id信息(rowid或者column_range)。每次处理的chunk是assigned状态,实验程序中我们设置parallel_level为2,所以每次都是2个chunk是assigned状态。处理结束之后,设置为processed状态。

海量数据更新最大的问题在于undo拓展的量,我们检查一下执行过程中的undo size情况。

SQL> select sum(bytes)/1024/1024 from dba_undo_extents where status='ACTIVE';

SUM(BYTES)/1024/1024

--------------------

SQL> select sum(bytes)/1024/1024 from dba_undo_extents where status='ACTIVE';

SUM(BYTES)/1024/1024

--------------------

​ 16

SQL> select sum(bytes)/1024/1024 from dba_undo_extents where status='ACTIVE';

SUM(BYTES)/1024/1024

--------------------

​ 10

每次的数据量都不大,说明每次都是一小块chunk的操作。也确定使用parallel执行的过程,是分步小块commit的过程。在job视图中,我们也可以明确的看出作为作业的信息。

SQL> select owner, job_name, JOB_ACTION, SCHEDULE_TYPE, state, last_start_date from dba_scheduler_jobs where job_name like 'TASK$_4%';

OWNER JOB_NAME JOB_ACTION SCHEDULE_TYPE STATE LAST_START_DATE


SYS TASK$_4_2 DBMS_PARALLEL_EXECUTE.RUN_INTERNAL_WORKER IMMEDIATE RUNNING 10-2月 -14 01.48.34.947417 下午 PRC

SYS TASK$_4_1 DBMS_PARALLEL_EXECUTE.RUN_INTERNAL_WORKER IMMEDIATE RUNNING 10-2月 -14 01.48.34.730487 下午 PRC

注意:传统的并行进程v$px_process中没有看到数据信息,说明并行程序包并不是Oracle传统的数据库并行方案。

SQL> select * from v$px_process;

SERVER_NAME STATUS PID SPID SID SERIAL#


执行结束信息:

25 --Controller

26 n_try := 0;

27 n_status := dbms_parallel_execute.task_status(task_name => vc_task);

28 while (n_try<2 and n_status != dbms_parallel_execute.FINISHED) loop

29 dbms_parallel_execute.resume_task(task_name => vc_task);

30 n_status := dbms_parallel_execute.task_status(task_name => vc_task);

31 end loop;

32

33 --Deal with Result

34 dbms_parallel_execute.drop_task(task_name => vc_task);

35 end;

36 /

PL/SQL procedure successfully completed

Executed in 944.453 seconds

更新2G数据一共使用945s,合计约16分钟。

从上面的数据视图和调用过程,我们可以得到如下结论:

对dbms_parallel_execute执行包而言,通过确定chunk方法和chunk size,可以将一个很大的数据集合划分为若干各小chunk集合,分步进行操作处理。代码中设置的parallel_level,体现在设置Job的个数上。启动作业任务后,Oracle并不是启动传统的并行机制,而是在Job Schedule的基础上创建parallel_level个数的作业,类型为立即执行。多个作业分别执行各个chunk的小块工作。使用Job Schedule的一个好处在于可以方便的进行作业resume和start过程。

下面我们讨论by number col和by SQL两种执行方法。

上篇我们讨论了dbms_parallel_execute的工作方法、使用流程和特点。本篇继续来讨论其他两种划分Chunk方式。说明:对每种划分策略执行过程中,笔者都进行了不同的实验,来说明其工作特点。

4、By Number Col划分Chunk方法

应该说,使用rowid进行数据表划分可以带来很多好处。每个chunk数据获取过程,本质上就是执行一个范围Range操作。对于rowid而言,直接通过范围检索的效率是相当高的。

与Rowid方法对应两种策略都是依据“数据表列范围”进行chunk划分。By Number Col的方法顾名思义,需要我们指定出一个数字类型列名称。Oracle会依据这个列取值进行划分。每个chunk实际上都是通过数字类型检索到的结果集合进行处理。

当然,这个过程必然伴随着我们对于“地势”条件的依赖。每次从上千万条记录中,FTS的检索出一个chunk数据显然是很费力的操作过程。最直接的优化手段就是索引和分区。注意:如果我们没有特殊的条件进行chunk划分辅助,一定要考虑by number col方式是否适合。

SQL> create index idx_t_id on t(object_id);

Index created

Executed in 107.282 seconds

SQL> exec dbms_stats.gather_table_stats(user,'T',cascade => true,degree => 2);

PL/SQL procedure successfully completed

Executed in 87.453 seconds

修改的脚本如下:

SQL> declare

2 vc_task varchar2(100);

3 vc_sql varchar2(1000);

4 n_try number;

5 n_status number;

6 begin

7 --Define the Task

8 vc_task := 'Task 2: By Number Col';

9 dbms_parallel_execute.create_task(task_name => vc_task);

10

11 --Define the Spilt

12 dbms_parallel_execute.create_chunks_by_number_col(task_name => vc_task,

13 table_owner => 'SYS',

14 table_name => 'T',

15 table_column => 'OBJECT_ID',

16 chunk_size => 1000); --定义chunk

17

18 vc_sql := 'update /+ ROWID(dda) /t set DATA_OBJECT_ID=object_id+1 where object_id between :start_id and :end_id';

19 --Run the task

20 dbms_parallel_execute.run_task(task_name => vc_task,

21 sql_stmt => vc_sql,

22 language_flag => dbms_sql.native,

23 parallel_level => 1);

24

25 --Controller

26 n_try := 0;

27 n_status := dbms_parallel_execute.task_status(task_name => vc_task);

28 while (n_try<2 and n_status != dbms_parallel_execute.FINISHED) loop

29 dbms_parallel_execute.resume_task(task_name => vc_task);

30 n_status := dbms_parallel_execute.task_status(task_name => vc_task);

31 end loop;

32

33 --Deal with Result

34 dbms_parallel_execute.drop_task(task_name => vc_task);

35 end;

36 /

从执行流程上看,上面脚本和by rowid方式没有显著地差异。最大的区别在于定义chunk时调用的方法,参数包括指定的数据表、列名和chunk size。注意:我们这里定义了chunk size是1000,但是在执行过程中,我们不能保证每个chunk的大小是1000。这个结论我们在后面的阐述实验中可以证明。

执行脚本的速度显著的比by rowid的慢了很多。但是我们也能发现很多技术细节。首先,我们会有一个时期,在chunk视图中没有结果返回。

SQL> select task_name, chunk_type, status from user_parallel_execute_tasks;

TASK_NAME CHUNK_TYPE STATUS


Task 2: By Number Col NUMBER_RANGE CHUNKING

Executed in 0.61 seconds

SQL> select status, count(*) from user_parallel_execute_chunks group by status;

STATUS COUNT(*)


在之后,我们才能查看到chunk处理情况。

SQL> select task_name, chunk_type, status from user_parallel_execute_tasks;

TASK_NAME CHUNK_TYPE STATUS


Task 2: By Number Col NUMBER_RANGE PROCESSING

SQL> select status, count(*) from user_parallel_execute_chunks group by status;

STATUS COUNT(*)


ASSIGNED 1

UNASSIGNED 1557

PROCESSED 13

这个现象说明:对dbms_parallel_execute包处理过程来说,包括两个重要的步骤,Chunk分块步骤和Chunk处理步骤。无论是哪种分块方法,Oracle都是首先依据分割原则,将任务拆分开来,规划在任务视图里面。之后再进行分作业Job的Processing处理过程。

同by rowid方式中的rowid Range信息一样,我们在chunk视图中也是可以看到数字列范围的信息。

SQL> select task_name, status, start_id, end_id, job_name from user_parallel_execute_chunks where rownum<5;

TASK_NAME STATUS START_ID END_ID JOB_NAME


Task 2: By Number Col PROCESSED 25002 26001 TASK$_5_2

Task 2: By Number Col ASSIGNED 26002 27001 TASK$_5_1

Task 2: By Number Col ASSIGNED 27002 28001 TASK$_5_2

Task 2: By Number Col UNASSIGNED 28002 29001

注意:我们此处看到的chunk范围是1000,由于数据准备过程,范围1000绝对不意味着每个chunk的大小是1000。所以,我们也就可以推断出,调用方法中的chunk size在number col方式中,是取值范围的大小。

直观的想,Oracle选取这样的策略也是有依据的:Oracle可以直接选取一个最小和最大的数据列值,依次chunk取值范围进行分割。这样做可减少对数据检索的压力。

在执行过程中,我们跟踪了执行会话的SQL语句,从shared pool中抽取出执行计划。

SQL> select * from table(dbms_xplan.display_cursor(sql_id=>'f2z147unc1n3q'));

PLAN_TABLE_OUTPUT

--------------------------------------------------------------------------------

SQL_ID f2z147unc1n3q, child number 0

-------------------------------------

update /+ ROWID(dda) /t set DATA_OBJECT_ID=object_id+1 where

object_id between :start_id and :end_id

Plan hash value: 538090111

-------------------------------------------------------------------------------

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

-------------------------------------------------------------------------------

| 0 | UPDATE STATEMENT | | | | 74397 (100)| |

| 1 | UPDATE | T | | | | |

|* 2 | FILTER | | | | | |

|* 3 | INDEX RANGE SCAN| IDX_T_ID | 48375 | 472K| 197 (1)| 00:00:03 |

-------------------------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

2 - filter(:START_ID<=:END_ID)

PLAN_TABLE_OUTPUT

--------------------------------------------------------------------------------

3 - access("OBJECT_ID">=:START_ID AND "OBJECT_ID"<=:END_ID)

匿名块执行完毕。

32

33 --Deal with Result

34 dbms_parallel_execute.drop_task(task_name => vc_task);

35 end;

36 /

PL/SQL procedure successfully completed

Executed in 11350.421 seconds

完成时间大大增加,折合3个小时左右。这个实验告诉我们:在三种方法选取如果不合适,性能会大大降低。

下面我们来看最后一种方法by SQL。

5、by SQL方法进行chunk划分

By SQL方法是用户自己定义SQL语句,获取column的start id和end id作为划分chunk的内容。代码如下:

SQL> declare

2 vc_task varchar2(100);

3 vc_sql varchar2(1000);

4 vc_sql_mt varchar2(1000);

5 n_try number;

6 n_status number;

7 begin

8 --Define the Task

9 vc_task := 'Task 3: By SQL';

10 dbms_parallel_execute.create_task(task_name => vc_task);

11

12 --Define the Spilt

13 vc_sql_mt := 'select distinct object_id, object_id from t';

14 dbms_parallel_execute.create_chunks_by_SQL(task_name => vc_task,

15 sql_stmt => vc_sql_mt,

16 by_rowid => false);

17

18 vc_sql := 'update /+ ROWID(dda) /t set DATA_OBJECT_ID=object_id+1 where object_id between :start_id and :end_id';

19 --Run the task

20 dbms_parallel_execute.run_task(task_name => vc_task,

21 sql_stmt => vc_sql,

22 language_flag => dbms_sql.native,

23 parallel_level => 2);

24

25 --Controller

26 n_try := 0;

27 n_status := dbms_parallel_execute.task_status(task_name => vc_task);

28 while (n_try<2 and n_status != dbms_parallel_execute.FINISHED) loop

29 dbms_parallel_execute.resume_task(task_name => vc_task);

30 n_status := dbms_parallel_execute.task_status(task_name => vc_task);

31 end loop;

32

33 --Deal with Result

34 dbms_parallel_execute.drop_task(task_name => vc_task);

35 end;

36 /

在定义chunk的过程中,我们指定出单独的SQL语句来确定start id和end id。这也就让我们不需要定义所谓的chunk size了。

执行过程依然进行chunking和processing过程。相关视图信息如下:

--chunking过程

SQL> select task_name, chunk_type, status from user_parallel_execute_tasks;

TASK_NAME CHUNK_TYPE STATUS


Task 3: By SQL NUMBER_RANGE CHUNKING

--Processing过程

SQL> select task_name, chunk_type, status from user_parallel_execute_tasks;

TASK_NAME CHUNK_TYPE STATUS


Task 3: By SQL NUMBER_RANGE PROCESSING

SQL> select status, count(*) from user_parallel_execute_chunks group by status;

STATUS COUNT(*)


ASSIGNED 2

UNASSIGNED 75559

PROCESSED 25

--执行作业情况

SQL> select saddr, sid, serial#, PROGRAM from v$session where username='SYS' and status='ACTIVE' and osuser='oracle';

SADDR SID SERIAL# PROGRAM


35ECE400 31 103 oracle@SimpleLinux.localdomain (J000)

35EA8300 45 29 oracle@SimpleLinux.localdomain (J001)

在chunk范围信息中,我们可以印证对于chunk size的理解。

SQL> select chunk_id, task_name, status, start_id, end_id from user_parallel_execute_chunks where rownum<10;

CHUNK_ID TASK_NAME STATUS START_ID END_ID


20052 Task 3: By SQL PROCESSED 17427 17427

20053 Task 3: By SQL PROCESSED 17439 17439

20054 Task 3: By SQL PROCESSED 17442 17442

20055 Task 3: By SQL PROCESSED 17458 17458

20056 Task 3: By SQL PROCESSED 37321 37321

20057 Task 3: By SQL PROCESSED 37322 37322

20058 Task 3: By SQL PROCESSED 17465 17465

20059 Task 3: By SQL PROCESSED 37323 37323

20060 Task 3: By SQL PROCESSED 17468 17468

9 rows selected

由于条件的限制,本次执行时间较长。

32

33 --Deal with Result

34 dbms_parallel_execute.drop_task(task_name => vc_task);

35 end;

36 /

PL/SQL procedure successfully completed

Executed in 47522.328 seconds

总执行时间为13个小时。

6、结论

从上面的实验,我们可以了解dbms_parallel_execute新功能包的使用和功能特点。比较显著的就是区别与传统的并行设置,parallel_execute包的方法是依托于10g以来的job schedule机制。并行、多线程转化为多个后台作业自主运行完成。

应该说,这样的策略让并行变的更加简单易用。我们将关注点转移到如何进行chunk划分和设置多少并行度的问题上。Chunk的划分影响到的是每次处理的数据量,而并行度取决于实际系统的资源富裕程度。

标签:

头像

小麦苗

学习或考证,均可联系麦老师,请加微信db_bao或QQ646634621

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

5 × 4 =

 

嘿,我是小麦,需要帮助随时找我哦
  • 18509239930
  • 个人微信

  • 麦老师QQ聊天
  • 个人邮箱
  • 点击加入QQ群
  • 个人微店

  • 回到顶部
返回顶部