尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多数据源与分库分表实战:从路由原理到ShardingSphere配置

多数据源与分库分表实战:从路由原理到ShardingSphere配置 简介一份基于Spring Boot的多数据源与分库分表实战代码包面向需要处理高并发读写、水平拆表扩库的Java后端开发者。项目采用MyBatis-Plus的dynamic-datasource统一管理多数据源引入Sharding-JDBC完成分库分表配合Druid连接池监控数据库连接并通过Lombok简化实体类整体工程结构清晰适合作为微服务数据层架构的参考实现。压缩包共162个文件以115个XML映射文件为主另有14个Java核心类与14个Class编译产物、6套YML配置声明数据源和分片规则同时附带JAR依赖、Maven构建脚本、properties与README等文档包体仅164KB下载后即可快速导入本地复现并在测试用例中验证分库分表是否生效。内容预览显示资源内含完整测试用例与示例接口读者可结合JUnit运行结果观察分片逻辑的执行路径。目前已吸引623人学习下载适合想深入理解Sharding-JDBC分片策略并需要可运行示例的Spring Boot开发者。1. 多数据源分库分表单库到瓶颈时别急着上缓存“多数据源数据库分库分表”这个组合解决的不是慢查询优化那一层的问题而是数据库结构性瓶颈当核心表数据量到了千万级、连接数被占满、磁盘 IO 和锁竞争把单实例压到极限加缓存只能延缓没法治本。多数据源负责让同一个应用按业务域或读写角色连不同的数据库实例分库分表负责把一张大表按分片键拆到多个物理节点上两者通常一起出现因为拆完库之后必然要面对多数据源切换。这套方案适合订单、流水、用户、消息这类持续增长的 OLTP 业务也适合正在做微服务拆分、需要把老单体里的库按域隔离出去的团队。下面这篇按“先想清楚、再落地、后避坑”的顺序讲透。2. 分库分表前先做容量评估分片键、拆分策略与数据源边界2.1 先别引入框架把容量账算明白不少团队是被慢查询逼着上分库分表的结果框架引入了一堆分片键随便选个时间字段三个月后热点分片又把单库打爆。我一般的做法是动手前先把当前库的真实容量算出来包括表行数、日增长量、单行平均长度、高峰期 QPS、读写比例以及连接数峰值。算完之后你会得到一个关键数字——单一 MySQL 实例在业务高峰期到底还能撑多久。有个粗略但实用的经验值单表行数超过 2000 万或者单表容量超过 50GB或者单实例连接数长期超过 70%就值得认真考虑分库分表了。这个数字不是绝对红线MyISAM 和 InnoDB、机械盘和 SSD 的表现差异很大关键在于“当前增长速度是否会在一个业务周期内击穿单实例上限”。如果按日增 10 万行估算2000 万行的表只需要半年就到临界点这时候应该提前规划分片方案而不是等报警了再熬夜迁移。算清楚容量之后还要区分两个概念分库解决的是连接数和 IO 瓶颈分表解决的是单表锁竞争和索引深度问题。如果一个实例的连接数还没打满只是单表查询慢先做分表就够了反过来如果每张表都不大但实例连接被打满那要优先分库。两者混在一起改排查问题时变量太多容易翻车。2.2 分片键与拆分策略hash、range、时间分片怎么选分片键是整个分库分表方案里最不能拍脑袋决定的参数因为它直接决定数据是否均匀分布、后续扩容要不要重排数据。常见分片键有两类业务主键类订单 ID、用户 ID。适合用 hash 取模数据均匀但扩容时要变更模数。时间类创建时间、订单时间。适合用 range 分片按月或按年切分天然支持按时间归档但容易产生热点分区——当天的数据永远集中在最新分片上。策略数据分布扩容难度典型场景hash 取模MOD均匀难mod 数变化要重分布订单表、用户表按 ID 分range 范围不均匀按业务量分布易新增分区即可日志表、流水表按时间分一致性 hash较均匀相对易只迁移少量数据分布式缓存映射到库按业务租户分天然隔离中SaaS 多租户场景实际项目中订单这类核心业务表我基本不用纯时间分片因为热点太集中下单高峰期所有写请求都打到最后一个月的那几张表上分片的意义就打了折扣。常见做法是“user_id 或 order_id 取模定库定表”时间字段只作为二级索引。如果业务一定要按时间维度查可以用“用户维度分片 时间字段索引”的组合牺牲一部分跨月查询性能换写入均衡。分片键还必须是查询条件里稳定出现的字段。如果 SQL 经常不带分片键查询比如“查最近十分钟所有用户的订单”这个分片键就不合格因为无法路由到具体分片只能全库广播。选分片键前把线上 TOP 20 高频 SQL 拉出来确认其中的 WHERE 条件、JOIN 关联字段、事务里的查询路径都落在同一个分片键上才算过关。2.3 多数据源的概念边界异构数据库与同构分片实例多数据源这个词容易让人误解成“连多个 MySQL 实例”或者“同时连 MySQL 和 Oracle”。按我的理解多数据源在工程上有两种形态混着用经常出问题。第一种是读写分离型多源同一个业务库主库负责写从库负责读数据源配置里定义 primary 和 replica 两套连接通过路由在代码层切换。这种“多源”数据是一份逻辑上仍是同一个库。第二种是业务域隔离型多源一个应用同时连接订单库、用户库、商品库甚至同时连 MySQL 和 PostgreSQL、达梦这样的异构数据库。这种“多源”各管一份数据数据模型和业务边界都不同。分库分表属于第二类的延伸——拆分后得到的多个分片实例在应用层看起来是多个数据源但它们持有的是同一张逻辑表的数据。这里最常见的误区是把它们当成普通的多数据源去手工切换SQL 路由逻辑散落在业务代码里时间一长根本维护不动。所以行业里才会出现 ShardingSphere 这类中间件把分库分表的多源路由收敛在配置层业务代码尽量无感。下一章先讲不依赖中间件、用 Spring 自身能力实现多数据源的最小方案把它理解透了你才能看懂那些中间件帮你做了什么。3. 用 AbstractRoutingDataSource 实现多数据源路由最小配置与切换规则3.1 多数据源切换的核心机制路由发生在拿到连接的那一刻Spring 里实现多数据源绕不开 AbstractRoutingDataSource 这个抽象类。它本身不自带任何数据源而是持有一个 targetDataSources 映射表和一个默认数据源在对外的 getConnection 被调用时通过 determineCurrentLookupKey 来决定返回哪个真实数据源的连接。理解这个时机很重要路由发生在“拿连接”的瞬间而不是 SQL 执行的瞬间。这意味着只要你在进入 DAO 或 Service 之前把路由 key 放进一个上下文数据源切换就能做到对 MyBatis、JdbcTemplate 透明。常见的落地方式是用 ThreadLocal 存当前线程的路由 key再用一个注解加 AOP 切面在方法调用前设值、调用后清理。事务边界要特别小心如果 Transactional 在方法进入时已经拿到了连接你再切换路由 key 是无效的这一点到第 5 章展开讲。3.2 最小配置application.yml 定义数据源与路由规则先看一份可用的最小配置。这里以两个订单库分片为例ds0 和 ds1都是 MySQL 实例。生产环境连接池我一般用 HikariCPSpring Boot 2.x 和 3.x 都默认集成不用额外引依赖。spring: datasource: hikari: pool-name: HikariCP minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 3000 idle-timeout: 600000 dynamic: # 自定义的扩展配置不是 Spring Boot 原生配置 datasource: ds0: jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0?useSSLfalseserverTimezoneAsia/Shanghai username: order_app password: change_me driver-class-name: com.mysql.cj.jdbc.Driver ds1: jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_1?useSSLfalseserverTimezoneAsia/Shanghai username: order_app password: change_me driver-class-name: com.mysql.cj.jdbc.Driver注意这里 dynamic.datasource 是我自定义的配置段Spring Boot 原生并不认识它需要你写一个配置类去读取。很多团队直接用它配合 baomidou 的 dynamic-datasource-spring-boot-starter省去自己写配置类。不过我还是建议先自己写一次理解背后的机制后面出问题才知道从哪排查。上面的 jdbc-url 是 HikariCP 的标准写法driver-class-name 对应 MySQL 8 的驱动包。连接池参数里maximum-pool-size 是最容易拍脑袋的。单实例 20 个连接看起来不多但如果你有 8 个数据源每个默认 20高峰期就是 160 个连接同时挂在各库上。先按业务并发估算每个事务占用一个连接事务平均执行时间 50ms并发 200 个请求同时进来理论需要 200×0.0510 个连接再算上慢查询和连接等待重试的余量单库 20 是合理的起步值。3.3 路由类、注解与 AOP把切换动作收敛成一行注解有了数据源定义下一步是让代码能切换。先写路由 key 的持有者再写真正的路由器然后写注解和切面。public class DataSourceContextHolder { private static final ThreadLocalString CONTEXT new ThreadLocal(); public static void setDataSource(String key) { CONTEXT.set(key); } public static String getDataSource() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }ThreadLocal 的作用是隔离线程间的路由状态。这里必须用 remove 而不是简单的 set null因为 Web 容器会复用线程如果线程执行完不清理下一个请求复用这个线程时会读到上一个请求的路由 key造成数据源错乱。这个坑非常隐蔽排查一次至少半天。public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DataSource { String value() default ds0; }Aspect Component public class DataSourceAspect { Around(annotation(dataSource)) public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { DataSourceContextHolder.setDataSource(dataSource.value()); try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clear(); } } }切面的作用域放在 finally 里清理保证即使业务方法抛异常路由状态也能复位。注解可以直接标在 Service 方法上DataSource(ds1)底层 DAO 执行 SQL 时会自动拿到 ds1 的连接。如果方法上没有注解AbstractRoutingDataSource 会回落到默认数据源所以一定要在配置里指定默认源。Bean public DataSource dataSource() { MapObject, Object targetDataSources new HashMap(); targetDataSources.put(ds0, buildDataSource(ds0)); targetDataSources.put(ds1, buildDataSource(ds1)); DynamicDataSource dynamicDataSource new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(targetDataSources.get(ds0)); return dynamicDataSource; }默认数据源建议选只读压力最小的库或者统一指向一个专门用于兜底的实例。很多团队默认指向分片 0结果所有没标注解的 SQL 全打到 ds0时间一长 ds0 的连接和 IO 比 ds1 高一大截从监控上看像是数据倾斜其实是默认路由设计疏忽。3.3 读写分离场景下的路由规则key 不只有数据源名还可以是角色多数据源如果是为了读写分离路由 key 的定义思路就不同了。此时不再是“订单走 ds0用户走 ds1”的业务域逻辑而是“读走 slave写走 master”。常见做法是在路由 key 里带上读写角色query 开头的方法走从库save/update/delete 开头的方法走主库。这里有两个容易踩的细节。一是主从延迟刚写入的数据立即读可能读不到业务上对强一致有要求的场景必须走主库或者用“先写主库再查主库”的兜底逻辑。二是从库之间要负载均衡AbstractRoutingDataSource 本身只支持按 key 选择一个源你需要在路由上下文里自己做轮询或随机分配比如用 AtomicLong 计数取模。更省事的做法是换成熟中间件但理解原理之后你会发现这些框架做的事就是这套模型的工程化封装。4. ShardingSphere 做分库分表从分片算法到全局表的一步步配置4.1 选 ShardingSphere-JDBC 还是 Proxy两种形态的取舍分库分表到了落地阶段几乎没人还用手写路由手工拼接表名。业界最常见的方案是 ShardingSphere它有两种形态ShardingSphere-JDBC 以 jar 包方式嵌入应用SQL 经过它解析、改写、路由、执行对应用来说像连了一个普通数据源ShardingSphere-Proxy 则是一个独立服务应用通过 MySQL 协议连它它再把 SQL 分发到底层分片。对比项ShardingSphere-JDBCShardingSphere-Proxy部署方式嵌入应用进程独立进程性能损耗较低本地解析多一跳网络损耗略高改造成本改数据源配置即可改连接地址兼容任意语言运维复杂度随应用发布独立部署与升级适用场景Java 应用、原生日志多语言混部、需要集中管控我一般建议 Java 服务优先用 JDBC 形态理由只有一个少一个故障节点。Proxy 在管控能力和多语言接入上有优势但生产环境里 Proxy 自身的连接管理和 SQL 解析性能也是要额外维护的成本。如果你的团队连 MySQL 主从都还没理清先别上 Proxy把 JDBC 形态跑通再说。4.2 分片配置实例按用户 ID 哈希拆成 2 库 × 4 表用 Spring Boot ShardingSphere-JDBC 5.x 的 YAML 配置做实例目标是实现用户订单表 t_order 按 user_id 取模分成 2 个库ds0、ds1每库 4 张表t_order_0 到 t_order_3共 8 张物理表。spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0 username: order_app password: change_me ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_1 username: order_app password: change_me rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..3} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db_mod table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_mod key-generate-strategy: column: order_id key-generator-name: snowflake sharding-algorithms: db_mod: type: MOD props: sharding-count: 2 table_mod: type: MOD props: sharding-count: 4 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: trueactual-data-nodes 这一段是关键它用表达式声明了 8 张物理表的完整位置。database-strategy 里的 sharding-column 是 user_id算法名指向 db_mod算法类型 MOD、取模数 2意思是 user_id % 2 决定进哪个库table_strategy 同理user_id % 4 决定进哪张表。库和表都用同一个分片键保证同一条订单数据在路由时能精确落到唯一的物理表。为什么要用 MOD 而不是 hash 后再模MOD 的实现简单直接适用于分片键本身就是数字的场景。如果分片键是字符串比如用户手机号或 UUID通常先对字符串做 hash 再取模否则字符串转数字后取模的分布性很难保证。ShardingSphere 的 HASH_MOD 算法内部会先用 MD5 计算 hash 再做取模字符串分片键我建议直接用它。key-generate-strategy 里的 snowflake 是分库分表绕不开的环节数据库自增主键在分片环境下无法保证全局唯一业务主键建议用分布式 ID。雪花算法生成的 ID 是 64 位长整型包含时间戳、机器位、序号趋势递增且全局唯一。要注意的是雪花 ID 作为表主键没问题但作为分片键时别拿它取模——时间戳高位的影响会让取模分布产生倾斜。4.3 全局表与绑定表分片后 JOIN 查询的两个关键配置分库分表之后SQL 的 JOIN 查询会遇到两种情况需要单独配置才能正常路由。第一种是广播表全局表比如省份、字典、商品类目这类数据量小、几乎不更新的表。它们不需要分片每个库里都放一份全量数据。配置上把它声明为 broadcast-table查询时 ShardingSphere 会在当前路由到的库内直接 JOIN不会因为表不在当前分片里而报错。代价是每次更新这个表时所有分片都要同步更新所以这类表必须严格控制更新频率。第二种是绑定表指的是分片键完全一致、分片规则也一致的两张业务表。比如 t_order 和 t_order_item两张表都按 user_id 分片那么 user_id100 的订单和它的明细一定在同一个物理库、同一张逻辑表上。把两者配置成 binding-tables 后JOIN 查询会强制在两个分片规则对应的表之间做关联不会出现跨库 JOIN。rules: sharding: binding-tables: - t_order,t_order_item broadcast-tables: - t_dict_province这两个配置的价值体现在查询性能上没有绑定关系时ShardingSphere 只能做笛卡尔积式路由——把 order 的 8 个分片和 order_item 的 8 个分片两两组合理论上要查 64 组配置绑定后只查 user_id 对应模数一致的 8 组order_0 配 item_0order_1 配 item_1……。业务表建模时如果主表、明细表分片键不一致分库分表后所有关联查询都是灾难所以库表设计阶段就要强制这一点。这也解释了为什么“先设计好分片规则再拆库”比“拆完再补规则”靠谱得多。4.4 跨分片查询与分布式事务能规避就规避不能规避再上中间件分库分表后不带分片键的查询会退化成全分片广播性能取决于最慢的那个分片这是物理规律任何中间件都优化不了。常见的解决思路是“分片键找不到就再建一个索引表”。比如订单表按 user_id 分片但运营后台要按商户 merchant_id 查订单怎么办单独建一张 merchant_order_index 表字段是 merchant_id 和 order_id这张表也分片分片键换成 merchant_id。查询时先查索引表拿到 order_id再用 order_id 对应的 user_id 回订单表。这是以额外的存储换取路由确定性。跨分片事务则是另一个复杂话题。分片后一个事务可能涉及多个库普通事务的 ACID 语义无法跨库保证。轻量业务可以用本地消息表最终一致性方案强一致场景需要引入类似 SEATA 的分布式事务框架。这里不展开细节只提醒一句分库分表后事务范围会被物理拆散排查问题时要习惯把“分布式事务未提交/回滚失败”列入怀疑列表。5. 多数据源与分库分表的坑清单路由、事务、DDL 与连接池5.1 分片字段没进 WHERESQL 被广播到全部分片现象一条单表查询平时 20ms分片后偶尔 400ms 甚至超时慢日志里同一个 SQL 出现在所有分片节点上。原因分片键没有出现在 WHERE 条件里ShardingSphere 无法推断目标分片只能把 SQL 下发到所有物理表然后合并结果。比如 t_order 按 user_id 分片你按 order_id 精确查询这条 SQL 只能全分片扫描。解决两条路——要么把查询条件改写成分片键业务键组合先查出 user_id 再回表要么像上面说的建一张按 order_id 分片的索引映射表。开发规范上应当强制规定访问分片表的 SQL 必须携带分片键上线前用 sql-show 打开路由日志人工抽查 SQL 的路由结果。5.2 多数据源下事务失效回滚只回滚了默认数据源现象方法上标了 DataSource(ds1) 和 Transactional业务在 ds1 上写入成功但后面的异常回滚后ds1 的数据还是写进去了。原因Spring 事务管理器在事务开始时就从数据源上下文拿连接。如果事务切面先于数据源切面执行连接在路由 key 设置之前就被绑定到了默认库你的 DataSource 注解晚了事务连接已经和默认数据源绑死。解决控制切面执行顺序让数据源切面的 order 值小于事务切面的 order 值。用 Order 或 AutoConfigureOrder 显式声明DataSourceAspect 的 order 设为 0Spring 事务切面的默认 order 是 Integer.MAX_VALUE 附近前者先执行路由 key 先设置事务再拿连接就能拿到正确的数据源。这种时序问题在单数据源项目里根本不存在多源改造后务必检查切面顺序。5.3 分片后修改表结构一次 DDL 发布要逐库逐表执行现象发布脚本只对 order_db_0.t_order_0 执行了 ALTER TABLE业务流量被路由到 order_db_1.t_order_3 时直接报 column not found或者在主库改了表结构从库没同步引发随机性报错。原因分库分表后逻辑表背后是 N 个物理表研发习惯性地把单库的 DDL 直接粘贴执行脑子里的表模型还是“一张表”。解决改成脚本化 DDL遍历所有物理节点执行执行完做一次 schema 差异校验。常见做法是把数据源地址、分片规则写进一个变量表用 Python 或 Bash 脚本遍历连接逐个执行同一个 DDL然后在所有节点上跑 information_schema.COLUMNS 查询比对字段版本。发布清单里必须包含这个校验步骤不能只靠“执行成功”就宣称发布完成。5.4 连接池给了默认值多数据源叠加后线程池被打满现象业务高峰期应用日志频繁出现 connectionTimeout、HikariPool 连接获取超时但数据库侧负载并不高CPU 和 IO 都很空闲。原因每个数据源都按单库经验配置了 maximum-pool-size208 个分片就是 160 个连接。如果线程池核心线程数是 50每个线程的一次调用只持有 1 个连接理论上够但如果有嵌套事务——一个 Service 里调多个 DAO每个 DAO 又切了不同的数据源连接会被占更多50 个线程请求 160 个连接的池子也会挤爆。解决连接池参数必须按“数据源数量同时被一个线程占用的最大可能值”来估算。事务方法里如果会串行访问 ds0 和 ds1那么一个线程最多占 2 个连接核心线程数×单线程最大占用连接数就是连接池下限。还要注意 ShardingSphere-JDBC 下连接是逻辑连接对应的物理连接路由到 8 张表时可能同时打开 8 个物理连接这个场景要单独压测确认。5.5 扩容与数据迁移hash 分片不是一锤子买卖现象分片数定成 2 库×4 表后业务半年涨了一倍需要扩容到 4 库×8 表结果发现老数据全要按新模数重算位置迁移期间写入和查询互相打架。原因MOD 取模分片天然和分片数强绑定mod 从 2 变成 4老数据的归属全部变化必须全量重分布。这是方案设计阶段留下的债很多团队把分片数当成静态配置等到扩容时才发现是“数据重洗”级别的工程。解决设计阶段就预留分片余量按未来三年的数据量定分片数一次性给足比如 32 库×64 表避免 1 年后扩容。如果一定要扩容用“双倍数扩容”或一致性 hash 的方案只迁移部分数据。上线前把扩容演练加入应急预案别把首次扩容放在生产事故当天做。6. 分片验证与灰度迁移用 EXPLAIN 看路由用双写保回滚6.1 用 EXPLAIN 验证路由结果确认 SQL 落在哪张物理表ShardingSphere 的 sql-show 日志能看到改写后的 SQL但生产环境不会一直开着。跑分片验证时我习惯直接把 sql-show 打开在测试环境对每个核心 SQL 执行 EXPLAIN确认改写后的 SQL 落到预期物理表。EXPLAIN SELECT * FROM t_order WHERE user_id 100 AND order_id 20250701001;ShardingSphere 会把这段 SQL 改写为SELECT * FROM t_order_0 WHERE user_id 100 AND order_id 20250701001;只要 user_id100 时 user_id % 20、user_id % 40路由到 ds0.t_order_0 就是正确的。这类验证要和自动化测试绑定不然每个分片键的余数组合都要人工算一遍漏掉user_id101这个奇数分片就会在线上翻车。6.2 迁移上线的顺序影子表双写、对账、再切读分库分表上线不推荐“一把梭”我见过最稳的顺序是先在新分片上建立影子表应用侧写双份——老单库写一份、新分片写一份跑一段时间对账确认两边数据一致再切读流量最后把老库降级只读。双写期间的失败要记录到补偿队列定时任务把失败数据重新同步到新分片。对账脚本按主键分批拉数据比对行数和关键字段的 hash这一步不能省因为“新表结构和旧表完全一致”这个假设往往在字段默认值、时区转换上破功。6.3 切流后的 watch 清单切到分片后的前三天重点盯着这几个指标分片间数据增长是否均匀、各库连接数是否有单个被压满、慢查询日志里有没有“无分片键广播”的 SQL。踩坑记录里最常见的还是 5.1——历史代码里藏着一条没带分片键的报表查询平时压测压不到凌晨跑批时把所有分片全部打满。我会把这类 SQL 汇总成黑名单在监控里设置单独告警一冒头就有人处理。分库分表这个方向真正考验人的不是配置写法而是对数据分布和路由时序的敏感度。我早期交过的学费都集中在 ThreadLocal 清理和事务切面顺序这些“代码之外的细节”上只能说多数据源这种设计天然把复杂度从数据库转移到了应用层。希望这份从评估到落地的路径能帮你在动手前把账算清楚上线后少熬几个夜。本文还有配套的精品资源点击获取
返回列表