canal 使用实例

canal 使用实例

摘要

canal 使用实例

为什么用canal

数据量大到一定程度,系统就会有分库分表. 而分片策略一般都是根据用户id来分的.
但是系统后台管理又可能是根据时间范围来查询的. 所以根本就没法查询了.

解决方法就是根据biglog实时同步数据库再写一份按照时间范围分片的数据库.

创建mysql用户

CREATE USER canal IDENTIFIED BY 'canal'; 
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';

FLUSH PRIVILEGES;  

SET PASSWORD FOR 'canal'@'%' = PASSWORD('123456');

mysql 开启binlog

vi /etc/my.cnf

[mysqld]
log-bin=mysql-bin #添加这一行就ok
binlog-format=ROW #选择row模式
server_id=1 #配置mysql replaction需要定义,不能和canal的slaveId重复

server mysqld restart

canal 配置

下载地址 https://github.com/alibaba/canal/releases

wget   https://github.com/alibaba/canal/releases/download/canal-1.0.24/canal.deployer-1.0.24.tar.gz

mkdir  canal
tar  zxvf  canal.deployer-1.0.24.tar.gz  -C canal

#example  代表 mysql 实例  ,可以拷贝example文件夹 ,比如 cp -r example  mysql-instance--241

编辑配置文件

vi canal/conf/mysql-instance-241/instance.properties


#################################################
## mysql serverId
canal.instance.mysql.slaveId = 1

# position info
canal.instance.master.address = 192.168.2.241:3306
canal.instance.master.journal.name = mysql-bin.000108
canal.instance.master.position = 0
canal.instance.master.timestamp =

#canal.instance.standby.address = 
#canal.instance.standby.journal.name =
#canal.instance.standby.position = 
#canal.instance.standby.timestamp =

# username/password
canal.instance.dbUsername = canal
canal.instance.dbPassword = 123456
canal.instance.defaultDatabaseName =
canal.instance.connectionCharset = UTF-8

# table regex
canal.instance.filter.regex = db_test_canal_master\\..*

启动

canal/bin/startup.sh

客户端代码

https://github.com/daodaovps/canal-client-test

代码备份

package com.alibaba.otter.canal.example;

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.lang.exception.ExceptionUtils;

import java.net.InetSocketAddress;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;


/**
 * 单机模式的测试例子
 *
 * @author jianghang 2013-4-15 下午04:19:20
 * @version 1.0.4
 */
public class SimpleCanalClientTest extends AbstractCanalClientTest {

    public SimpleCanalClientTest(String destination) {
        super(destination);
    }

    public static void main(String args[]) {
        // 根据ip,直接创建链接,无HA的功能
        String destination = "mysql-instance-241";
        //String ip = AddressUtils.getHostIp();
        String ip = "192.168.2.201";
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(ip, 11111),
                destination,
                "",
                "");

        final SimpleCanalClientTest clientTest = new SimpleCanalClientTest(destination);
        clientTest.setConnector(connector);
        clientTest.start();

        Runtime.getRuntime().addShutdownHook(new Thread() {

            public void run() {
                try {
                    logger.info("## stop the canal client");
                    clientTest.stop();
                } catch (Throwable e) {
                    logger.warn("##something goes wrong when stopping canal:\n{}", ExceptionUtils.getFullStackTrace(e));
                } finally {
                    logger.info("## canal client is down.");
                }

            }

        });
    }

    @Override
    protected void printEntry(List<CanalEntry.Entry> entrys) {

        for (CanalEntry.Entry entry : entrys) {
            long executeTime = entry.getHeader().getExecuteTime();
            long delayTime = new Date().getTime() - executeTime;

            if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
                if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN) {
                    TransactionBegin begin = null;
                    try {
                        begin = TransactionBegin.parseFrom(entry.getStoreValue());
                    } catch (InvalidProtocolBufferException e) {
                        throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
                    }
                    // 打印事务头信息,执行的线程id,事务耗时
                    logger.info(transaction_format,
                            new Object[]{entry.getHeader().getLogfileName(),
                                    String.valueOf(entry.getHeader().getLogfileOffset()),
                                    String.valueOf(entry.getHeader().getExecuteTime()), String.valueOf(delayTime)});
                    logger.info(" BEGIN ----> Thread id: {}", begin.getThreadId());
                } else if (entry.getEntryType() == EntryType.TRANSACTIONEND) {
                    TransactionEnd end = null;
                    try {
                        end = TransactionEnd.parseFrom(entry.getStoreValue());
                    } catch (InvalidProtocolBufferException e) {
                        throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
                    }
                    // 打印事务提交信息,事务id
                    logger.info("----------------\n");
                    logger.info(" END ----> transaction id: {}", end.getTransactionId());
                    logger.info(transaction_format,
                            new Object[]{entry.getHeader().getLogfileName(),
                                    String.valueOf(entry.getHeader().getLogfileOffset()),
                                    String.valueOf(entry.getHeader().getExecuteTime()), String.valueOf(delayTime)});
                }

                continue;
            }

            if (entry.getEntryType() == EntryType.ROWDATA) {
                RowChange rowChage = null;
                try {
                    rowChage = RowChange.parseFrom(entry.getStoreValue());
                } catch (Exception e) {
                    throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
                }

                CanalEntry.EventType eventType = rowChage.getEventType();

                logger.info(row_format,
                        new Object[]{entry.getHeader().getLogfileName(),
                                String.valueOf(entry.getHeader().getLogfileOffset()), entry.getHeader().getSchemaName(),
                                entry.getHeader().getTableName(), eventType,
                                String.valueOf(entry.getHeader().getExecuteTime()), String.valueOf(delayTime)});

                if (eventType == EventType.QUERY || rowChage.getIsDdl()) {
                    logger.info(" sql ----> " + rowChage.getSql() + SEP);
                    continue;
                }


                String tableName = entry.getHeader().getTableName();
                List sqls = new LinkedList();
                for (RowData rowData : rowChage.getRowDatasList()) {
                    if (eventType == EventType.DELETE) {
                        printColumn(rowData.getBeforeColumnsList());
                    } else if (eventType == CanalEntry.EventType.INSERT) {
                        printColumn(rowData.getAfterColumnsList());
                    } else {
                        printColumn(rowData.getAfterColumnsList());
                    }

                    String sql = buildSql(tableName, rowData, eventType);
                    if (sql != null) {
                        sqls.add(sql);
                        System.out.println(sql);
                    }

                }
                execSqls(sqls);

            }
        }
    }


    /**
     * 同步数据变更到从库 (admin查询使用的数据库)
     *
     * @param sqls
     */
    protected void execSqls(List sqls) {

    }


    final static String DELETE_SQL = "DELETE FROM _tn_ WHERE _cn_";
    final static String INSERT_SQL = "INSERT INTO _tn_(_cn_) VALUES (_vn_)";
    final static String UPDATE_SQL = "UPDATE _tn_ set _vn_ WHERE _cn_";


    /**
     * 构造sql语句
     *
     * @param tableName
     * @param rowData
     * @param eventType
     * @return
     */
    private String buildSql(String tableName, RowData rowData, EventType eventType) {

        tableName = "`" + tableName + "`";
        if (eventType == EventType.DELETE) {
            StringBuffer cn = new StringBuffer();
            StringBuffer cn2 = new StringBuffer();
            for (Column column : rowData.getBeforeColumnsList()) {
                if (column.getIsKey()) {
                    if (cn2.length() > 0) {
                        cn2.append(" and ");
                    }
                    cn2.append("`" + column.getName() + "`");
                    cn2.append("=");
                    if (!isNumberType(column.getMysqlType())) {
                        cn2.append("'" + column.getValue() + "'");
                    } else {
                        cn2.append(column.getValue());
                    }
                }
                if (column.getValue() == null || "".equals(column.getValue())) {
                    continue;
                }
                if (cn.length() > 0) {
                    cn.append(" and ");
                }
                cn.append("`" + column.getName() + "`");
                cn.append("=");
                if (!isNumberType(column.getMysqlType())) {
                    cn.append("'" + column.getValue() + "'");
                } else {
                    cn.append(column.getValue());
                }

            }
            return DELETE_SQL.replaceAll("_tn_", tableName).replaceAll("_cn_", cn2.length() > 0 ? cn2.toString() : cn.toString());
        } else if (eventType == EventType.INSERT) {
            StringBuffer cn = new StringBuffer();
            StringBuffer vn = new StringBuffer();
            for (Column column : rowData.getAfterColumnsList()) {
                if (cn.length() > 0) {
                    cn.append(",");
                    vn.append(",");
                }
                cn.append("`" + column.getName() + "`");
                if (!isNumberType(column.getMysqlType())) {
                    vn.append("'" + column.getValue() + "'");
                } else {
                    vn.append(column.getValue());
                }
            }
            return INSERT_SQL.replaceAll("_tn_", tableName).replaceAll("_cn_", cn.toString()).replaceAll("_vn_", vn.toString());
        } else {
            StringBuffer cn = new StringBuffer();
            StringBuffer cn2 = new StringBuffer();
            for (Column column : rowData.getBeforeColumnsList()) {
                if (column.getIsKey()) {
                    if (cn2.length() > 0) {
                        cn2.append(" and ");
                    }
                    cn2.append("`" + column.getName() + "`");
                    cn2.append("=");
                    if (!isNumberType(column.getMysqlType())) {
                        cn2.append("'" + column.getValue() + "'");
                    } else {
                        cn2.append(column.getValue());
                    }
                }
                if (column.getValue() == null || "".equals(column.getValue())) {
                    continue;
                }
                if (cn.length() > 0) {
                    cn.append(" and ");
                }
                cn.append("`" + column.getName() + "`");
                cn.append("=");
                if (!isNumberType(column.getMysqlType())) {

                    cn.append("'" + column.getValue() + "'");

                } else {

                    cn.append(column.getValue());

                }

            }

            StringBuffer vn = new StringBuffer();

            for (Column column : rowData.getAfterColumnsList()) {
                if (!column.getUpdated()) {
                    continue;
                }
                if (vn.length() > 0) {

                    vn.append(",");
                }
                vn.append("`" + column.getName() + "`");
                vn.append("=");
                if (!isNumberType(column.getMysqlType())) {
                    vn.append("'" + column.getValue() + "'");
                } else {
                    vn.append(column.getValue());
                }
            }
            return UPDATE_SQL.replaceAll("_tn_", tableName).replaceAll("_cn_", cn2.length() > 0 ? cn2.toString() : cn.toString()).replaceAll("_vn_", vn.toString());
        }

    }


    /**
     * 判断数据库列字段类型是否是数字,不是数字的值需要加''
     *
     * @param mysqlType
     * @return
     */

    private static boolean isNumberType(String mysqlType) {

        if (mysqlType.indexOf("int") == -1 &&
                mysqlType.indexOf("bigint") == -1 &&
                mysqlType.indexOf("float") == -1 &&
                mysqlType.indexOf("double") == -1 &&
                mysqlType.indexOf("decimal") == -1 &&
                mysqlType.indexOf("numeric") == -1) {
            return false;
        }
        return true;
    }


}

测试基类备份

package com.alibaba.otter.canal.example;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.alibaba.otter.canal.protocol.CanalEntry.TransactionBegin;
import com.alibaba.otter.canal.protocol.CanalEntry.TransactionEnd;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;

/**
 * 测试基类
 * 
 * @author jianghang 2013-4-15 下午04:17:12
 * @version 1.0.4
 */
public class AbstractCanalClientTest {

    protected final static Logger             logger             = LoggerFactory.getLogger(AbstractCanalClientTest.class);
    protected static final String             SEP                = SystemUtils.LINE_SEPARATOR;
    protected static final String             DATE_FORMAT        = "yyyy-MM-dd HH:mm:ss";
    protected volatile boolean                running            = false;
    protected Thread.UncaughtExceptionHandler handler            = new Thread.UncaughtExceptionHandler() {

                                                                     public void uncaughtException(Thread t, Throwable e) {
                                                                         logger.error("parse events has an error", e);
                                                                     }
                                                                 };
    protected Thread                          thread             = null;
    protected CanalConnector                  connector;
    protected static String                   context_format     = null;
    protected static String                   row_format         = null;
    protected static String                   transaction_format = null;
    protected String                          destination;

    static {
        context_format = SEP + "****************************************************" + SEP;
        context_format += "* Batch Id: [{}] ,count : [{}] , memsize : [{}] , Time : {}" + SEP;
        context_format += "* Start : [{}] " + SEP;
        context_format += "* End : [{}] " + SEP;
        context_format += "****************************************************" + SEP;

        row_format = SEP
                     + "----------------> binlog[{}:{}] , name[{},{}] , eventType : {} , executeTime : {} , delay : {}ms"
                     + SEP;

        transaction_format = SEP + "================> binlog[{}:{}] , executeTime : {} , delay : {}ms" + SEP;

    }

    public AbstractCanalClientTest(String destination){
        this(destination, null);
    }

    public AbstractCanalClientTest(String destination, CanalConnector connector){
        this.destination = destination;
        this.connector = connector;
    }

    protected void start() {
        Assert.notNull(connector, "connector is null");
        thread = new Thread(new Runnable() {

            public void run() {
                process();
            }
        });

        thread.setUncaughtExceptionHandler(handler);
        thread.start();
        running = true;
    }

    protected void stop() {
        if (!running) {
            return;
        }
        running = false;
        if (thread != null) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                // ignore
            }
        }

        MDC.remove("destination");
    }

    protected void process() {
        int batchSize = 5 * 1024;
        while (running) {
            try {
                MDC.put("destination", destination);
                connector.connect();
                connector.subscribe();
                while (running) {
                    Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
                    long batchId = message.getId();
                    int size = message.getEntries().size();
                    if (batchId == -1 || size == 0) {
                        // try {
                        // Thread.sleep(1000);
                        // } catch (InterruptedException e) {
                        // }
                    } else {
                        printSummary(message, batchId, size);
                        printEntry(message.getEntries());
                    }

                    connector.ack(batchId); // 提交确认
                    // connector.rollback(batchId); // 处理失败, 回滚数据
                }
            } catch (Exception e) {
                logger.error("process error!", e);
            } finally {
                connector.disconnect();
                MDC.remove("destination");
            }
        }
    }

    private void printSummary(Message message, long batchId, int size) {
        long memsize = 0;
        for (Entry entry : message.getEntries()) {
            memsize += entry.getHeader().getEventLength();
        }

        String startPosition = null;
        String endPosition = null;
        if (!CollectionUtils.isEmpty(message.getEntries())) {
            startPosition = buildPositionForDump(message.getEntries().get(0));
            endPosition = buildPositionForDump(message.getEntries().get(message.getEntries().size() - 1));
        }

        SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
        logger.info(context_format, new Object[] { batchId, size, memsize, format.format(new Date()), startPosition,
                endPosition });
    }

    protected String buildPositionForDump(Entry entry) {
        long time = entry.getHeader().getExecuteTime();
        Date date = new Date(time);
        SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
        return entry.getHeader().getLogfileName() + ":" + entry.getHeader().getLogfileOffset() + ":"
               + entry.getHeader().getExecuteTime() + "(" + format.format(date) + ")";
    }

    protected void printEntry(List<Entry> entrys) {
        for (Entry entry : entrys) {
            long executeTime = entry.getHeader().getExecuteTime();
            long delayTime = new Date().getTime() - executeTime;

            if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
                if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN) {
                    TransactionBegin begin = null;
                    try {
                        begin = TransactionBegin.parseFrom(entry.getStoreValue());
                    } catch (InvalidProtocolBufferException e) {
                        throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
                    }
                    // 打印事务头信息,执行的线程id,事务耗时
                    logger.info(transaction_format,
                        new Object[] { entry.getHeader().getLogfileName(),
                                String.valueOf(entry.getHeader().getLogfileOffset()),
                                String.valueOf(entry.getHeader().getExecuteTime()), String.valueOf(delayTime) });
                    logger.info(" BEGIN ----> Thread id: {}", begin.getThreadId());
                } else if (entry.getEntryType() == EntryType.TRANSACTIONEND) {
                    TransactionEnd end = null;
                    try {
                        end = TransactionEnd.parseFrom(entry.getStoreValue());
                    } catch (InvalidProtocolBufferException e) {
                        throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
                    }
                    // 打印事务提交信息,事务id
                    logger.info("----------------\n");
                    logger.info(" END ----> transaction id: {}", end.getTransactionId());
                    logger.info(transaction_format,
                        new Object[] { entry.getHeader().getLogfileName(),
                                String.valueOf(entry.getHeader().getLogfileOffset()),
                                String.valueOf(entry.getHeader().getExecuteTime()), String.valueOf(delayTime) });
                }

                continue;
            }

            if (entry.getEntryType() == EntryType.ROWDATA) {
                RowChange rowChage = null;
                try {
                    rowChage = RowChange.parseFrom(entry.getStoreValue());
                } catch (Exception e) {
                    throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
                }

                EventType eventType = rowChage.getEventType();

                logger.info(row_format,
                    new Object[] { entry.getHeader().getLogfileName(),
                            String.valueOf(entry.getHeader().getLogfileOffset()), entry.getHeader().getSchemaName(),
                            entry.getHeader().getTableName(), eventType,
                            String.valueOf(entry.getHeader().getExecuteTime()), String.valueOf(delayTime) });

                if (eventType == EventType.QUERY || rowChage.getIsDdl()) {
                    logger.info(" sql ----> " + rowChage.getSql() + SEP);
                    continue;
                }

                for (RowData rowData : rowChage.getRowDatasList()) {
                    if (eventType == EventType.DELETE) {
                        printColumn(rowData.getBeforeColumnsList());
                    } else if (eventType == EventType.INSERT) {
                        printColumn(rowData.getAfterColumnsList());
                    } else {
                        printColumn(rowData.getAfterColumnsList());
                    }
                }
            }
        }
    }

    protected void printColumn(List<Column> columns) {
        for (Column column : columns) {
            StringBuilder builder = new StringBuilder();
            builder.append(column.getName() + " : " + column.getValue());
            builder.append("    type=" + column.getMysqlType());
            if (column.getUpdated()) {
                builder.append("    update=" + column.getUpdated());
            }
            builder.append(SEP);
            logger.info(builder.toString());
        }
    }

    public void setConnector(CanalConnector connector) {
        this.connector = connector;
    }

}

POM.xml备份

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">


    <modelVersion>4.0.0</modelVersion>
    <version>1.0-SNAPSHOT</version>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.example</artifactId>
    <packaging>jar</packaging>
    <name>canal example module for otter ${project.version}</name>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.0.24</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.protocol</artifactId>
            <version>1.0.24</version>
        </dependency>

        <!-- test dependency -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- deploy模块的packaging通常是jar,如果项目中没有java 源代码或资源文件,加上这一段配置使项目能通过构建 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <archive>
                        <addMavenDescriptor>true</addMavenDescriptor>
                    </archive>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <!-- 这是最新版本,推荐使用这个版本 -->
                <version>2.2.1</version>
                <executions>
                    <execution>
                        <id>assemble</id>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
                <configuration>
                    <appendAssemblyId>false</appendAssemblyId>
                    <attach>false</attach>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
                <property>
                    <name>env</name>
                    <value>!release</value>
                </property>
            </activation>

            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-assembly-plugin</artifactId>
                        <configuration>
                            <!-- maven assembly插件需要一个描述文件 来告诉插件包的结构以及打包所需的文件来自哪里 -->
                            <descriptors>
                                <descriptor>${basedir}/src/main/assembly/dev.xml</descriptor>
                            </descriptors>
                            <!--<finalName>canal-example</finalName>-->
                            <outputDirectory>${project.build.directory}</outputDirectory>
                        </configuration>
                    </plugin>
                </plugins>
            </build>

        </profile>

        <profile>
            <id>release</id>
            <activation>
                <property>
                    <name>env</name>
                    <value>release</value>
                </property>
            </activation>

            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-assembly-plugin</artifactId>
                        <configuration>
                            <!-- 发布模式使用的maven assembly插件描述文件 -->
                            <descriptors>
                                <descriptor>${basedir}/src/main/assembly/release.xml</descriptor>
                            </descriptors>
                            <!-- 如果一个应用的包含多个deploy模块,如果使用同样的包名, 如果把它们复制的一个目录中可能会失败,所以包名加了 artifactId以示区分 -->
                            <!--<finalName>${project.artifactId}-${project.version}</finalName>-->
                            <!-- scm 要求 release 模式打出的包放到顶级目录下的target子目录中 -->
                            <outputDirectory>${project.parent.build.directory}</outputDirectory>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

高可用

HA模式只是新增了 zookeeper, 基本部署和使用步骤基本一样的.