资讯动态

SpringBoot+MyCat生产级读写分离与分库分表实战

发布时间:2026/10/16 5:24:40 来源:尧图企业网站定制
1. 这不是“加个依赖就能跑”的玩具项目而是生产级数据架构的实战切片SpringBootMyCat 实现读写分离、分库分表、主从同步——看到这个标题很多刚接触高并发场景的开发者第一反应是“哦又一个整合教程”。但我要先泼一盆冷水如果你把它当成 Spring Boot 整合 MyCat 的配置清单来学十有八九会在上线前夜被数据库慢查询告警叫醒或者在流量高峰时眼睁睁看着主库 CPU 冲到 98% 却束手无策。这不是危言耸听而是我过去三年在多个中型业务系统中踩过的真实坑某次电商大促前我们按文档配好了 MyCat 的分片规则结果订单号按 user_id 分片后热门主播的粉丝集中下单导致单一分片库瞬间被打爆另一次金融类项目里读写分离没做事务一致性兜底用户刚充值成功就去查余额返回的却是旧值。这些都不是 MyCat 的 Bug而是对“读写分离”“分库分表”“主从同步”这三个词背后真实约束条件的理解偏差。所谓读写分离本质是把“写压力”和“读压力”在物理层面拆开让主库专注处理 insert/update/delete从库只扛 select分库分表则是当单库单表撑不住百万级数据或万级 QPS 时用水平拆分把数据打散到多个物理节点而主从同步是整个架构的基石——它决定了从库数据有多“新”也决定了读写分离能走多远。这三者不是并列关系而是环环相扣的链条主从同步延迟高读写分离就不可靠分片键选得不合理分库分表反而放大热点SpringBoot 层若没做好事务传播控制MyCat 的路由规则再漂亮也白搭。所以这篇内容不讲“怎么装 MyCat”也不贴一段 application.yml 就完事。我会带你从一张订单表开始推演它如何在日均 50 万单、峰值 3000 TPS 的压力下一步步演化成跨 4 个物理库、每个库含 8 张分表、读写请求自动分流的结构并告诉你每一步背后的取舍、参数怎么算、哪些配置必须改、哪些“最佳实践”其实是毒药。适合正在设计核心交易模块的后端工程师、负责数据库架构演进的 DBA以及想真正搞懂分布式数据层的同学——如果你只是想跑通 demo那建议先去官方文档抄一遍但如果你想让这套架构稳稳扛住真实业务流量那就继续往下看。2. 架构设计底层逻辑为什么是 MyCat 而不是 ShardingSphere为什么必须绕开 SpringBoot 的自动装配陷阱2.1 MyCat 的定位不是 ORM 框架而是数据库中间件的“交通警察”很多人一上来就问“ShardingSphere 和 MyCat 到底选哪个”这个问题本身就有陷阱。ShardingSphere 是以 JDBC/Proxy 形态嵌入应用或独立部署的分片引擎它更像一个“智能 SQL 解析器”能深度理解你的 SQL 语义支持复杂子查询、跨库 join需开启广播表、分布式事务Seata 集成。而 MyCat 是基于 MySQL 协议的代理层它把自己伪装成一个 MySQL Server应用完全无感所有 SQL 都发给它它再按规则转发到后端真实数据库。这就决定了它们的适用边界如果你的业务 SQL 极其复杂大量使用存储过程、自定义函数、嵌套子查询且团队对 JDBC 层改造接受度高ShardingSphere 更灵活如果你有一堆遗留系统Java 代码里全是JdbcTemplate或原生Connection甚至还有 PHP/Python 调用同一个数据库MyCat 的协议兼容性就是救命稻草——它对上层完全透明连连接池配置都不用动。我们最终选 MyCat核心原因有三个第一现有系统存在多个语言栈调用同一套数据库无法统一升级 JDBC 驱动第二DBA 团队对 MySQL 主从复制链路极其熟悉而 MyCat 的心跳检测、故障切换机制与 MySQL 原生生态无缝衔接第三也是最关键的一点MyCat 的分片路由逻辑是 SQL 解析后直接映射不经过 Java 字节码增强性能损耗极低。实测在同等硬件下MyCat 代理层平均增加 0.8ms 延迟而 ShardingSphere-JDBC 在复杂 SQL 下可达 3~5ms。别小看这几毫秒——在支付类场景端到端 P99 延迟要求 ≤200ms中间件多耗 3ms就意味着你少了一次重试机会。提示MyCat 不是银弹。它不支持跨库事务XA 仅限部分版本且稳定性存疑不支持全局二级索引对ORDER BY ... LIMIT分页需配合limitStart参数重写。这些限制不是缺陷而是为换取协议透明性和低延迟付出的合理代价。2.2 SpringBoot 的“便利性”反而是最大陷阱自动装配会悄悄覆盖关键配置SpringBoot 的spring-boot-starter-jdbc默认会加载HikariCP连接池并自动注入DataSourceBean。问题来了当你在application.yml里配了 MyCat 的地址如jdbc:mysql://mycat-host:8066/mydbSpringBoot 会把这个地址当成真实数据库直接创建连接池。这看似没问题但埋下了两个雷第一颗雷连接池健康检测失效。HikariCP 默认用SELECT 1做心跳检测。MyCat 收到这个 SQL 后会随机路由到某个后端节点执行。如果该节点宕机MyCat 会返回错误但 HikariCP 只知道“连接不可用”并不知道是后端节点故障还是 MyCat 自身问题。结果就是连接池不断尝试重建连接大量线程卡在getConnection()上应用雪崩。第二颗雷事务传播失控。Spring 的Transactional注解依赖DataSource的getConnection()获取物理连接。当 MyCat 地址被当作真实 DB 时Spring 认为所有操作都在“同一个数据库”上事务管理器会复用连接。但 MyCat 的事务其实是伪分布式事务——它只能保证单条 SQL 在单个节点上的原子性跨节点 update 无法回滚。一旦业务代码里写了类似update order set status2 where id?; update user set balancebalance-100 where id?;这样的语句MyCat 会把两条 SQL 发到不同分片第一条成功、第二条失败时事务管理器根本感知不到数据就永久不一致了。解决方案很直接必须手动接管 DataSource 创建禁用 SpringBoot 的自动装配。具体做法是在启动类上加SpringBootApplication(exclude {DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class})然后自己写Configuration类用DruidDataSource比 HikariCP 对 MyCat 兼容性更好创建连接池并显式设置validationQuery/* ping */ SELECT 1—— 这个特殊注释是 MyCat 识别心跳包的关键它会让 MyCat 直接返回成功不转发到后端从而避免因后端节点故障导致连接池瘫痪。2.3 三者协同的本质主从同步是地基读写分离是承重墙分库分表是屋顶很多人把这三个概念平级看待其实它们是严格的层级依赖关系主从同步地基决定从库数据的“新鲜度”。MySQL 原生的异步复制延迟通常在 50~200ms半同步复制可压到 20~50ms。MyCat 的读写分离策略如balance1会根据后端节点的writeHost/readHost状态和weight权重路由但它无法改变复制延迟本身。所以如果你的业务要求“写后立刻可读”主从同步延迟就是硬性天花板任何中间件都绕不开。读写分离承重墙建立在主从同步之上解决读多写少场景的扩展性。MyCat 通过schema.xml中的dataNode和dataHost配置将逻辑库映射到物理节点组。关键在于balance参数0表示只读主库1表示所有读操作随机分发到所有从库2表示读操作优先发给从库主库只在从库全挂时才承接。我们线上用balance2因为1在从库负载不均时会导致部分从库被打爆比如某个从库磁盘 IO 已达 90%MyCat 仍按权重均分请求。分库分表屋顶解决单库容量瓶颈但会引入新问题。比如分片键sharding key的选择用user_id分片用户维度查询快但跨用户统计如“全站昨日订单量”就得扫所有分片用order_id分片订单查询快但用户查自己全部订单就得并发请求多个分片。我们最终采用“双分片键”策略核心交易表用user_id % 4分库再用order_id % 8分表确保单用户订单落在同一库内同时通过 MyCat 的ER 分片规则让order和order_item表按order_id关联避免跨库 join。这三层不是孤立配置而是一个整体设计。比如主从同步延迟高你就得降低读写分离的balance级别甚至某些强一致性场景如支付结果页直接强制走主库分库数定为 4就决定了主从同步链路要至少维护 4 组主从对DBA 运维成本直线上升。所以架构决策从来不是技术炫技而是在性能、一致性、运维成本之间找平衡点。3. 核心细节解析从 schema.xml 到 rule.xml每一行配置都在解决一个真实痛点3.1 schema.xml不只是“把表映射到节点”而是定义数据拓扑的宪法MyCat 的schema.xml是整个数据路由的蓝图它的结构看似简单但每一处配置都对应着生产环境的血泪教训。我们以订单库trade_db为例展示一个经过压测验证的配置schema nametrade_db checkSQLschemafalse sqlMaxLimit100 table namet_order dataNodedn$0-3 rulesharding-by-userid / table namet_order_item dataNodedn$0-3 rulesharding-by-orderid / table namet_user dataNodedn0 typeglobal / /schema dataNode namedn0 dataHosthost1 databasetrade_db_0 / dataNode namedn1 dataHosthost1 databasetrade_db_1 / dataNode namedn2 dataHosthost2 databasetrade_db_2 / dataNode namedn3 dataHosthost2 databasetrade_db_3 / dataHost namehost1 maxCon1000 minCon10 balance2 writeType0 dbTypemysql dbDrivernative heartbeatselect user()/heartbeat writeHost hosthostM1 url192.168.1.10:3306 usermycat password123456 readHost hosthostS1 url192.168.1.11:3306 usermycat password123456 / readHost hosthostS2 url192.168.1.12:3306 usermycat password123456 / /writeHost /dataHost dataHost namehost2 maxCon1000 minCon10 balance2 writeType0 dbTypemysql dbDrivernative heartbeatselect user()/heartbeat writeHost hosthostM2 url192.168.1.20:3306 usermycat password123456 readHost hosthostS3 url192.168.1.21:3306 usermycat password123456 / readHost hosthostS4 url192.168.1.22:3306 usermycat password123456 / /writeHost /dataHost这段配置里藏着五个关键细节第一checkSQLschemafalse是保命开关。默认为true时MyCat 会校验 SQL 中的库名是否匹配schema名。但 SpringBoot 的 JPA/Hibernate 生成的 SQL 常带database_name.table_name前缀比如SELECT * FROM trade_db.t_order。如果checkSQLschematrueMyCat 会报错“schema not found”因为trade_db是逻辑库名不是物理库名。设为false后MyCat 直接忽略前缀只解析表名和字段。第二sqlMaxLimit100不是性能优化而是防雪崩熔断。没有 limit 的SELECT * FROM t_order在分库分表下会变成 32 条 SQL4 库 × 8 表并发执行结果集合并后可能超百兆内存。设为100后MyCat 会自动在所有分片查询上加上LIMIT 100再合并结果。虽然可能漏掉部分数据但总比 OOM 好。真正的分页需求必须用 MyCat 的limitStart,limitSize语法如SELECT * FROM t_order LIMIT 100,20。第三typeglobal的全局表必须谨慎。t_user表标记为global意味着它会在所有dn0-dn3四个物理库中完全复制一份。好处是t_order关联t_user时无需跨库 join坏处是每次INSERT/UPDATE t_userMyCat 都要向 4 个库并发写入写性能下降 4 倍。所以我们只把t_user中极少更新的字段如user_name,avatar_url放在这里高频更新的last_login_time等字段放在单独的t_user_profile表按user_id分片。第四dataHost的balance2和writeType0是读写分离的黄金组合。balance2如前所述优先读从库writeType0表示所有写操作只发给第一个writeHost即hostM1或hostM2不轮询。为什么不用writeType1轮询写因为 MySQL 主从复制是单线程回放 relay log轮询写会导致从库 SQL 线程竞争反而增大延迟。固定写一个主库让复制链路更稳定。第五heartbeat心跳语句必须用select user()而非SELECT 1。这是 MyCat 4.x 版本的坑SELECT 1在某些 MySQL 版本下会被优化掉导致心跳检测失效。select user()返回当前用户名强制走完整 SQL 解析流程确保心跳真实有效。3.2 rule.xml分片规则不是数学题而是业务场景的翻译器MyCat 的rule.xml定义了数据如何切分但网上教程常把它讲成“取模运算”或“范围分片”的数学游戏。实际上分片规则的核心是把业务查询模式翻译成数据库可高效执行的物理分布。我们以t_order表为例分析三种常见规则的取舍方案一sharding-by-userid取模分片tableRule namesharding-by-userid rule columnsuser_id/columns algorithmmod-long/algorithm /rule /tableRule function namemod-long classio.mycat.route.function.PartitionByMod property namecount4/property /function优势用户维度查询极致高效。SELECT * FROM t_order WHERE user_id 12345会被精准路由到dn112345 % 4 1不扫描其他分片。致命缺陷热点用户引发单点瓶颈。某次直播带货头部主播 ID 为10000001其粉丝集中下单dn1库的 CPU 瞬间拉满而dn0/dn2/dn3闲置。我们的补救在应用层加“用户 ID 扰动”——真实user_id存储时先hash(user_id) % 1000000再存这样相同业务用户的 ID 在哈希后分散到不同分片。虽然牺牲了少量查询便利性查用户所有订单需扫 4 个分片但换来了负载均衡。方案二sharding-by-date日期范围分片tableRule namesharding-by-date rule columnscreate_time/columns algorithmsharding-by-date/algorithm /rule /tableRule function namesharding-by-date classio.mycat.route.function.PartitionByDate property namedateFormatyyyy-MM-dd/property property namesBeginDate2023-01-01/property property namesEndDate2025-01-01/property property namesPartionDay30/property /function优势天然支持时间范围查询如WHERE create_time BETWEEN 2023-10-01 AND 2023-10-31只查 1~2 个分片。致命缺陷写入热点集中在最新分片。每天 0 点后所有订单都写入当天分片IO 压力陡增。我们的补救改用“月分片 日内哈希”先按YEAR(create_time)*100 MONTH(create_time)分库如 202310再按DAYOFMONTH(create_time) % 8分表。这样每月数据均匀分布在 8 张表中避免单表过大。方案三sharding-by-murmur一致性哈希function namemurmur classio.mycat.route.function.PartitionByMurmurHash property nameseed0/property property namecount64/property property namevirtualBucketInit20/property /function优势节点增减时仅少量数据迁移适合弹性扩缩容。致命缺陷无法支持范围查询BETWEEN或IN语句会广播到所有分片。我们的选择仅用于日志类表如t_operation_log这类表几乎只有INSERT和按id查询不需要范围扫描。注意分片键必须是WHERE条件中的确定值不能是函数或表达式。WHERE DATE(create_time) 2023-10-01无法路由因为 MyCat 解析不出具体日期必须写成WHERE create_time 2023-10-01 00:00:00 AND create_time 2023-10-02 00:00:00。3.3 server.xml被忽视的“安全阀”决定系统生死线server.xml控制 MyCat 的全局行为其中三个参数直接影响稳定性processors8MyCat 的 NIO 线程数默认为 CPU 核数。我们线上服务器 16 核但设为8。为什么因为 MyCat 的线程模型是“1 个 NIO 线程处理所有客户端连接的读写事件再将 SQL 请求分发给后端连接池”。如果设为 16NIO 线程过多会导致上下文切换开销剧增实测 QPS 反而下降 15%。8是经过压测找到的吞吐量拐点。processorBufferChunk4096每个 NIO 线程的缓冲区块大小。默认4096字节对于大字段如 JSON 订单详情容易触发buffer overflow。我们调大到8192并配合maxPacketSize64M在schema.xml的dataHost中设置确保单条 SQL 不被截断。bindIp0.0.0.0必须显式绑定 IP不能留空。否则 MyCat 启动后监听127.0.0.1外部应用连不上。我们生产环境严格设为内网 VIP如bindIp192.168.1.100并通过防火墙限制仅应用服务器 IP 段可访问。4. 实操全流程从本地调试到灰度上线每一步都附带避坑指南4.1 本地开发环境搭建用 Docker 三分钟启动 MyCat MySQL 主从本地调试最怕环境不一致。我们用 Docker Compose 一键拉起 MyCat 和两组 MySQL 主从共 4 个容器配置文件docker-compose.yml如下version: 3.8 services: mysql-master1: image: mysql:5.7 container_name: mysql-master1 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: trade_db_0 MYSQL_USER: mycat MYSQL_PASSWORD: 123456 command: --server-id1 --log-binmysql-bin --binlog-formatROW --max_connections1000 ports: - 3307:3306 volumes: - ./mysql/master1.cnf:/etc/mysql/conf.d/my.cnf mysql-slave1: image: mysql:5.7 container_name: mysql-slave1 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: trade_db_0 MYSQL_USER: mycat MYSQL_PASSWORD: 123456 command: --server-id2 --relay-logmysql-relay-bin --read_onlyON ports: - 3308:3306 volumes: - ./mysql/slave1.cnf:/etc/mysql/conf.d/my.cnf depends_on: - mysql-master1 # mysql-master2/mysql-slave2 配置类似略 mycat: image: mycatorg/mycat:1.6.7.4 container_name: mycat environment: MYCAT_HOME: /opt/mycat JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64 ports: - 8066:8066 # MyCat 服务端口 - 9066:9066 # MyCat 管理端口 volumes: - ./mycat/conf/schema.xml:/opt/mycat/conf/schema.xml - ./mycat/conf/rule.xml:/opt/mycat/conf/rule.xml - ./mycat/conf/server.xml:/opt/mycat/conf/server.xml depends_on: - mysql-master1 - mysql-slave1 - mysql-master2 - mysql-slave2关键避坑点MySQL 主从配置中--binlog-formatROW是必须的。MyCat 的show datasource命令依赖 row 格式解析 binlogstatement 格式会导致从库状态检测失败。mysql-slave1.cnf中必须加replicate-ignore-dbmysql否则 MySQL 系统库的变更也会被复制引发权限错误。启动顺序必须是mysql-master*→mysql-slave*→mycat因为 MyCat 启动时会主动连接后端数据库做心跳检测如果 MySQL 没起来MyCat 会报错退出。启动后用mysql -h127.0.0.1 -P8066 -umycat -p123456 -Dtrade_db连接 MyCat执行show databases;应看到trade_db执行show datasource;应看到所有dataHost状态为idle表示主从链路正常。4.2 SpringBoot 集成手写 DruidDataSource绕过所有自动装配陷阱如前所述必须禁用 SpringBoot 的DataSourceAutoConfiguration。完整配置如下Configuration MapperScan(basePackages com.example.trade.mapper, sqlSessionFactoryRef sqlSessionFactory) public class MyCatConfig { Bean(destroyMethod close) ConfigurationProperties(spring.datasource.mycat) public DataSource myCatDataSource() { DruidDataSource dataSource new DruidDataSource(); // 关键设置 validationQuery 为 MyCat 专用心跳 dataSource.setValidationQuery(/* ping */ SELECT 1); dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(false); dataSource.setTestOnReturn(false); // 连接泄漏检测MyCat 连接池易出现 dataSource.setRemoveAbandonedOnMaintenance(true); dataSource.setRemoveAbandonedOnBorrow(true); dataSource.setRemoveAbandonedTimeoutMillis(60000); return dataSource; } Bean public SqlSessionFactory sqlSessionFactory(Qualifier(myCatDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/*.xml)); return factoryBean.getObject(); } }对应的application.ymlspring: datasource: mycat: url: jdbc:mysql://localhost:8066/trade_db?useSSLfalseserverTimezoneAsia/Shanghai username: mycat password: 123456 driver-class-name: com.mysql.jdbc.Driver initial-size: 10 min-idle: 10 max-active: 100 # 关键关闭 Druid 的自动提交检测MyCat 不支持 default-auto-commit: false实操心得default-auto-commit: false是必须的。MyCat 的事务模型不支持 JDBC 的setAutoCommit(true)否则会报Cant execute statement with auto commit true。所有事务必须由 Spring 的Transactional显式管理。removeAbandonedTimeoutMillis设为6000060 秒因为 MyCat 的连接在长事务或网络抖动时容易被误判为泄漏。我们线上曾因一个未关闭的ResultSet导致连接池耗尽这个参数帮我们快速定位了问题。4.3 灰度上线四步法从只读流量到全量写入零事故迁移把 MyCat 接入生产环境绝不能“一刀切”。我们采用渐进式灰度全程 72 小时零回滚第一步只读流量镜像24 小时修改 DNS将trade-db-read.example.com解析到 MyCat 地址trade-db-write.example.com仍指向原主库。应用层所有SELECT语句的连接串切换为trade-db-readINSERT/UPDATE/DELETE仍走原库。监控 MyCat 的show sql.high慢 SQL和show heartbeat心跳延迟确认无异常。避坑此阶段发现GROUP BY语句在分片后结果不一致如COUNT(*)被各分片分别计算立即在应用层加Select(SELECT COUNT(*) FROM (SELECT 1 FROM t_order GROUP BY user_id) t)强制聚合。第二步读写分离验证12 小时将trade-db-write.example.com切到 MyCat但writeType0所有写仍发往原主库MyCat 仅做路由。开启 MyCat 的sql.log对比 MyCat 日志和原主库 binlog确认 SQL 完全一致。避坑发现NOW()函数在 MyCat 中被解析为本地时间导致订单创建时间比主库慢 8 小时时区不一致。解决方案在server.xml中加systemproperty nametimeZoneAsia/Shanghai/property/system。第三步分库分表预热24 小时将t_order表的rule从sharding-by-userid切换为实际分片规则但dataNode仍指向单库dn0即逻辑分片物理未拆。用影子库流量1% 生产流量复制验证分片路由正确性检查show datanode是否显示所有分片命中。避坑INSERT INTO t_order (...) VALUES (...),(...)批量插入时MyCat 默认按行分片。如果两行user_id不同会被路由到不同分片导致BatchUpdateException。解决方案在server.xml中加systemproperty nameuseHandshakeV10true/property/system启用 V10 握手协议支持批量插入按分片分组。第四步全量切流12 小时将dn0-dn3全部指向真实分库dataHost切换为生产主从集群。启用 MyCat 的show cache查看 SQL 结果缓存命中率初期应低于 20%说明路由正常若长期高于 80%说明大部分请求被路由到同一分片需检查分片键分布。终极验证执行SELECT COUNT(*) FROM t_order广播查询和SELECT COUNT(*) FROM t_order WHERE user_id ?单分片查询对比结果是否一致。不一致说明分片数据同步有问题。5. 常见问题与排查技巧实录那些文档不会写的“深夜报警电话”答案5.1 问题速查表从现象到根因的 5 分钟定位法现象可能根因快速验证命令解决方案应用连接 MyCat 超时但telnet mycat-host 8066通MyCat 连接池耗尽mysql -hmycat-host -P9066 -umycat -p123456 -e show connection检查server.xml的processors和processorBufferPool增大maxConSELECT * FROM t_order返回部分数据且show sql.high显示大量broadcast分片键未出现在 WHERE 条件中explain SELECT * FROM t_order WHERE user_id 12345确认 SQL 中user_id是字面量非?占位符MyCat 4.x 不支持预编译分片show datasource显示某readHost状态为down但 MySQL 从库实际正常MySQL 从库read_onlyOFFmysql -hslave-ip -e SELECT read_only在从库执行SET GLOBAL read_onlyONMyCat 日志频繁打印cant find tableschema.xml中table的name与物理表名不一致mysql -hmycat-host -P8066 -e show tables检查schema.xml的table name是否为小写MySQL 默认大小写敏感INSERT成功但SELECT查不到show heartbeat显示延迟 5sMySQL 主从复制延迟高mysql -hmaster-ip -e SHOW MASTER STATUS和mysql -hslave-ip -e SHOW SLAVE STATUS\G优化从库innodb_flush_log_at_trx_commit2增大relay_log_space_limit5.2 真实案例复盘一次“诡异”的 500ms 延迟现象某天下午 3 点订单创建接口 P95 延迟从 80ms 突增至 500msMyCat 监控显示query time平稳但backend time后端数据库响应时间飙升。排查过程先排除网络ping和tcping后端 MySQL 均正常查 MyCat 日志发现大量execute timeout但超时时间设为 30s500ms 不可能触发执行show sql.slow发现慢 SQL 都是SELECT LAST_INSERT_ID()—— 这

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑