ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Session存储选型:内存还是Redis?原理对比与Spring Boot迁移实操

Session存储选型:内存还是Redis?原理对比与Spring Boot迁移实操 做 Java Web 开发久了session 这个问题迟早会撞到你脸上。我刚入行那会儿项目还是个单体应用所有 session 都老老实实躺在 Tomcat 内存里一个用户登录了就有一份会话数据简单直接根本不用操心别的。直到后来服务拆成多台、用户量上来每次重启都得让所有人重新登录线上偶发“诡异掉线”我才被迫把 session 从内存迁到 Redis也才真正把“session 放内存”和“session 放 Redis”这两条路的底细摸清楚。这篇文章不绕弯子直接聊这个话题。我会先讲 session 到底存了什么、为什么有存储位置的问题再分别拆内存方案和 Redis 方案的工作原理、优劣和适用场景最后给出从内存迁到 Redis 的完整实操配置和我在生产环境里踩过的一些坑。适合正在做 Web 开发、被 session 共享和持久化问题困扰的开发者也适合准备面试、想系统理解 session 存储原理的同学。1. session 的本质以及为什么会有“放哪里”的问题1.1 session 到底存了什么HTTP 协议本身是无状态的。用户登录成功后服务端怎么知道后面的请求还是同一个用户最常见的做法就是服务端在内存里创建一份会话数据生成一个唯一的 sessionId通过响应头里的Set-Cookie: JSESSIONIDxxx交给浏览器浏览器后续请求都会自动带上这个 Cookie服务端再根据 sessionId 找到对应的数据。这份“对应的数据”就是 session。它本质上是一个键值对容器里面可以放 userId、用户角色、购物车内容、临时校验标记等任何你需要跨请求保留的信息。你可以把 session 理解成服务员手里的“客人档案卡”客人报出桌号JSESSIONID服务员就能找到对应的菜单偏好和忌口记录。1.2 为什么 session 不能只放在客户端有同学可能会问既然 Cookie 能存放数据为什么还要在服务端开一块空间存 session原因很简单Cookie 在客户端数据可以被用户随意查看和篡改而且大小有限制通常 4KB 左右。如果登录凭证、权限角色这些都放 Cookie安全防线基本等于没有。服务端 session 的好处是数据留在自己的可控范围内客户端只持有一个不可猜测的 ID服务器拿到 ID 后再去查真正的数据避免把核心信息暴露给用户。既然要在服务端存就得选地方。最自然的选择是内存——毕竟对象本来就在 JVM 里直接一个 Map 存下来读写速度极快。但随着系统变大“服务端”可能不止一台机器session 放内存的短板就逐渐暴露了。这就是这篇文章里内存方案与 Redis 方案所有分歧的根源。1.3 会话数据在内存和 Redis 中本质上的差异是什么一句话概括内存方案把 session 当成“进程内临时对象”来管理Redis 方案把 session 当成“独立存储中的键值数据”来管理。前者生命周期绑定在 JVM 进程上进程死了 session 就没了后者生命周期绑定在 Redis 中通过 key 和 TTL 控制和具体哪一个应用节点无关。一句话的差异背后的设计取舍和运维成本差出十万八千里。2. session 放内存默认方案的真实面貌2.1 Tomcat 默认是怎么管理内存 session 的绝大多数 Java Web 开发者第一次接触的就是这个方案。以 Tomcat 为例默认的StandardManager内部维护了一个ConcurrentHashMapkey 是 sessionIdvalue 是StandardSession对象。每次请求进来Tomcat 从 Cookie 里解析出 JSESSIONID到这个 Map 里查一下存在就返回同一个 session 对象不存在就新建一个。这个 Map 是有淘汰机制的。Tomcat 后台有个定时任务周期性地扫描所有 session检查lastAccessedTime和当前时间的差值超过最大未访问时间就移除。默认超时时间是 30 分钟也就是web.xml里session-configsession-timeout30/session-timeout/session-config的来历。用户持续频繁操作lastAccessedTime 会被不断刷新session 就不会“过期”。这套机制对单机应用完全够用实现也简单。2.2 为什么内存 session 会让人头疼我在生产里见过三种典型的坑都和“把 session 放内存”直接相关。第一重启丢会话。不管你用kill -9还是优雅停机JVM 进程一旦退出堆内存里所有 session 对象立刻灰飞烟灭。你发布一次代码用户就集体掉线一次。有些项目为了缓解这个问题用 Tomcat 的PersistentManager把 session 落盘或存到数据库先不说性能配置复杂度和恢复延迟就够喝一壶了。第二多实例无法共享。用户第一次请求打到 A 机器session 存在 A 的内存里下一次请求被负载均衡转发到 B 机器B 的内存里当然没有这个 session。常见临时方案是负载均衡开启“粘性会话”sticky session让同一个用户固定打到同一台机器。但代价是某台机器故障时还是会让该节点上的用户全部掉线弹性扩容和缩容也极其痛苦。第三内存压力不可控。session 数量取决于在线用户数和每个 session 的大小编码但它占用的堆内存是动态增长的。如果业务方在一个 session 里塞了大量对象比如把某个很重的查询结果直接丢进去堆内存会迅速攀升GC 变频繁甚至触发 Full GC 长停顿。我见过一个老系统内存 session 里存了用户权限表几万人一登录堆直接涨了 2GB整站卡成幻灯片。2.3 内存 session 就没有优点吗不能一棍子打死。内存方案最大的优点就是快——读取 session 不需要走网络、不需要序列化直接取对象引用性能下限极高。而且它零额外组件依赖Tomcat 开箱即用连运维都不用操心。对单机部署、用户量小比如内部管理系统、低并发后台、可以接受重启丢会话的场景用内存 session 反而是最优解。加一个 Redis 进去不是不行但属于无谓的复杂度。3. session 放 Redis解决共享和重启问题的正经方案3.1 Redis 里的 session 长什么样把 session 放 Redis本质上就是用 Redis 替代 JVM 内部那个 Map。sessionId 作为 keysession 数据作为 valueTTL 设置为会话超时时间。用户请求进来时应用从 Cookie 取到 sessionId去 Redis 查一下数据还在就恢复会话数据已过期就新建。用 Spring Session 的例子来说一个 session 在 Redis 里往往不是单条记录而是有几类 keyspring:session:sessions:sessionId主键hash 类型存放当前 session 的属性、创建时间、最后访问时间、过期时间。spring:session:expirations:时间戳辅助键集合类型用来定时清理过期的 session避免依赖 Redis 自身的惰性删除导致残留。spring:session:index:索引名如果开启了按 userName 等字段索引查询还会额外生成索引键。实际拿redis-cli看你会发现 value 可能是一串 JSON也可能是二进制乱码这取决于你配置的序列化方案。后面实操部分我会详细说怎么让它变得可读、可排查。3.2 为什么 Redis 能解决“多实例不共享”理解这一点是全文的核心。内存 session 的数据归属在某个 JVM 进程内部别的进程看不见也摸不着。Redis 则是一个独立的存储服务所有应用节点通过 TCP 连接访问同一个 Redis 实例或集群。session 数据不再属于任何特定应用节点而是属于统一的存储层。于是负载均衡不再需要粘性会话了。用户第一次请求登录打到 Asession 存入 Redis第二次请求打到 BB 同样从 Redis 里按 sessionId 查到了这份数据。应用节点可以任意横向扩容和缩容哪台机器挂了都不影响其他节点的用户。这也是微服务架构、网关层无状态化常用的基础能力之一。3.3 Spring Session 是怎么做到“无缝替换”的你可能会问换成 Redis 存 session业务代码是不是要大改Spring Session 做得比较巧妙它通过 Servlet 规范里的HttpSession接口把“会话实现”整体替换掉了。应用层拿到的不再是 Tomcat 的StandardSession而是一个基于 Redis 数据构造的RedisSession对象。你照样request.getSession().setAttribute(...)底层写操作却已经透传到 Redis。原理上Spring Session 在 Servlet 容器里注册了一个SessionRepositoryFilter这个过滤器会把原生的HttpServletRequest包装成SessionRepositoryRequestWrapper然后重写getSession()方法。真正干活的是RedisSessionRepository它负责把 session 对象序列化写入 Redis、按 ID 反序列化读回、设置 TTL。这个过程对绝大多数业务代码是透明的你甚至不需要改动 Service 层和 Controller 层。3.4 Redis session 的“序列化”是一个大坑把 session 放进 Redis意味着数据要跨越 JVM 进程边界必须变成字节流再存进去读取时再还原成对象。序列化方案选错了表面看不出问题但排查起来能让人怀疑人生。常见序列化方式有这三种JDK 原生序列化直接把ObjectOutputStream写出来的二进制存进 Redis。缺点很明显内容体积大、不可读、有反序列化安全风险构造恶意数据可能触发漏洞。很多“重启 Redis 之后用户全部掉线”的诡异问题追根究底就是新旧代码用的序列化方式不一致导致旧数据还原失败。JSON 序列化Jackson推荐方案可读性强方便用redis-cli直接观察数据跨语言友好。但要注意对象的全限定类名会被写进 JSON 里重命包名或类名后旧数据也无法读取。自定义压缩序列化对超大 session 数据比如塞了对象列表能显著节省 Redis 内存但调试更麻烦。我现在默认用GenericJackson2JsonRedisSerializer原因只有一个线上出问题时我能直接看到 session 里到底存了什么东西不需要拿着二进制串去解析。4. 内存方案与 Redis 方案的硬核对比4.1 关键指标对比表下面这张表是我自己整理的项目选型速查表后面每一项我都会展开解释。维度内存 sessionRedis session读写性能最快走 JVM 堆内引用需要网络 RTT 序列化通常多 1-5ms容量上限受 JVM 堆限制过大会加重 GC受 Redis 内存限制可随集群扩展进程重启直接丢失Redis 有 RDB/AOF数据基本不丢多实例共享不支持需要粘性会话天然支持所有节点读写同一存储数据可观测性需借助 dump 堆栈可直接redis-cli查看 key 和 value运维复杂度无额外组件需要维护 Redis 集群或保证高可用会话过期方式容器定时扫描 lastAccessedTimeTTL 定期清理依赖 Redis 删除机制对小流量系统最合适偏重引入依赖反而增加故障点4.2 性能差距并没有想象中那么大很多同学一听 Redis 方案第一反应是“每次读 session 都走网络肯定慢”。真实情况要冷静看待。如果你和 Redis 都部署在同一内网一次GET的往返延迟通常在 0.2-1ms 左右加上 JSON 反序列化时间撑死几个毫秒。而内存 session 虽然对象引用拿到就能用但当堆内存被大量 session 塞满时频繁 GC 造成的毛刺也许比 Redis 一次网络 IO 还夸张。举个例子我压过某个电商后台内存 session 方案在并发 2000 时 GC 暂停时间平均 80ms最坏接近 300ms切到 Redis session 之后每次 session 操作增加 2ms但堆内存稳住了GC 暂停掉到 20ms 以内。整体用户体验反而更平滑。当然如果你的每个请求里有十几次 session 访问且 Redis 网络波动较大这个结论要重测。关键是用数据说话不要凭感觉。4.3 过期机制的本质区别这里必须细讲因为很多线上掉线问题都和它有关。内存方案里Tomcat 的StandardManager是周期扫描判断“最后访问时间距离当前时间是否超过超时阈值”。用户连续操作session 会被不断续期用户离开session 撑到超时阈值后由后台任务清除。这种机制是“应用层主动控制”的。Redis 方案里session 过期依赖 Redis 的 key 过期机制包括惰性删除和定期删除。Spring Session 还会额外维护一个expirations集合配合定时任务确保过期 session 会被明确清理而不只是挂个 TTL 等 Redis 自己想起来了再删。但需要注意Redis 的定期删除默认每秒 10 次扫描一定比例的 key如果某个 key 的 TTL 刚一到也不会立刻在物理上消失而是被标记为逻辑过期。客户端访问时惰性删除才会真正触发。所以线上偶尔能看到 Redis 里还残留着“已过期”的 session key这是正常的不意味着 session 还能被正常使用。4.4 安全层面的差异内存 session 的数据在 JVM 内部外部无法直接读取Redis session 是独立存储如果 Redis 端口直接暴露或未设置认证局域网内任何能连上 Redis 的人都可能直接顺着 sessionId 读出用户数据。所以用 Redis session 之后Redis 本身的访问控制反而成了安全重点必须设requirepass、绑定非公网地址、最好启用 TLS。另外敏感字段不要直接塞进 session即使 Redis 里也要考虑加密存储。我在项目里习惯只往 session 放“非敏感的上下文信息”像真实的令牌这类高危数据另外放专门的存储。4.5 什么时候你必须用 Redis session我不会摆“万金油”结论只告诉你哪些硬指标一出现内存 session 就该淘汰了应用部署了 2 个以上实例且不方便开启粘性会话。发布频率高接受不了每次发布用户集体掉线。有弹性扩容诉求希望新节点启动后能立刻接管用户流量。多个应用需要共享同一份登录态比如网关 多个后端服务。用户量虽不大但单机堆内存非常紧张不希望被 session 占用太多。只要命中其中两条直接上 Redis session 是正确选择。反之就继续用内存别给自己找事。5. 实操把 Spring Boot 的 session 从内存迁到 Redis5.1 环境准备与依赖我下面以 Spring Boot 2.7 / 3.x 为例Redis 用 6.x 或 7.x 都行。先确认你已经有可用的 Redis 实例本地可以用 Docker 快速起一个docker run -d --name redis-session -p 6379:6379 redis:7-alpine项目里需要引入两个关键依赖spring-boot-starter-data-redis提供 Redis 操作能力spring-session-data-redis提供 HttpSession 与 Redis 的桥接。如果你是 Maven 项目在pom.xml里加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.session/groupId artifactIdspring-session-data-redis/artifactId /dependency引入之后Spring Boot 会尝试自动配置 Redis 连接和 Session 仓库。理论上你只需要加依赖和配置就能跑但实际生产中序列化和连接池参数必须手动调。5.2 配置 Redis 连接与会话超时在application.yml中做如下配置spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 2000ms lettuce: pool: max-active: 50 max-idle: 10 min-idle: 5 max-wait: 3000ms session: store-type: redis timeout: 30m这里有几个容易忽略的点。spring.session.store-type: redis是显式声明 session 存储类型不加也能通过依赖自动推断但显式写出来更可控。spring.session.timeout是 session 的过期时间默认 30 分钟生产环境要根据业务自己定比如“记住我”类需求可能需要 7 天纯登录态管理可能 2 小时就够了。连池参数要结合请求峰值估算太小会导致 Redis 连接等待超时太大又会占用内存和连接数。5.3 配置 JSON 序列化器避免乱码这是我最想让你注意的一步。默认情况下 Spring Session 会把 session 属性用 JDK 序列化存储Redis 里全是\xAC\xED\x00\x05开头的二进制。调试时你根本看不出来里面是什么反序列化出错时也难排查。我推荐直接换成GenericJackson2JsonRedisSerializerimport org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; Configuration public class SessionRedisConfig { Bean(name springSessionDefaultRedisSerializer) public RedisSerializerObject springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }注意 bean 名字必须叫springSessionDefaultRedisSerializerSpring Session 会优先使用这个名字注入它默认的 RedisSerializer。如果你自己另起一个名字配置可能不生效session 照样走 Java 原生序列化。这是 Spring Session 设计上比较隐蔽的一点网上很多教程都没提实际踩坑的人不少。配置好之后session.setAttribute(loginUser, user)里的User对象必须保证能被 Jackson 序列化类需要有默认构造器字段要有 getter/setter或者类上用JsonIgnoreProperties(ignoreUnknown true)兜底。否则写入时会直接抛序列化异常。5.4 写一个最简单的登录接口验证效果为了验证迁移是否成功我通常会在 Controller 里加一个非常简单的接口RestController public class SessionController { PostMapping(/login) public String login(HttpServletRequest request, String username) { request.getSession().setAttribute(username, username); return login success, sessionId request.getSession().getId(); } GetMapping(/whoami) public String whoami(HttpServletRequest request) { Object username request.getSession().getAttribute(username); return username null ? anonymous : (String) username; } PostMapping(/logout) public String logout(HttpServletRequest request) { request.getSession().invalidate(); return logout success; } }先调用/login登录再用redis-cli查 Redisredis-cli keys spring:session:*你会看到类似这样的结果spring:session:sessions:5f2c1a... spring:session:expirations:1710000000000再执行 type spring:session:sessions:5f2c1a... hash hgetall spring:session:sessions:5f2c1a... 1) creationTime 2) 1710000000000 3) lastAccessedTime 4) 1710000001000 5) maxInactiveInterval 6) 1800 7) sessionAttr:username 8) \zhangsan\能看到sessionAttr:username的值是zhangsan说明 session 已经干净地写入 Redis并且是 JSON 格式没有乱码。5.5 关键参数与淘汰策略的联动Redis session 的 TTL 虽然由 Spring Session 写入但你别忘了 Redis 本身还有内存淘汰策略在下面看着。如果你的 Redismaxmemory很小且maxmemory-policy是allkeys-lru那么在内存吃紧的时候Redis 会优先淘汰最近最少使用的 session key。这会导致明明用户还在活跃但因为其他大量 key 占内存把 session key 挤掉了用户莫名其妙掉线。所以用了 Redis session 之后要么把maxmemory-policy设置成volatile-ttl只淘汰设置了 TTL 的 key且优先淘汰剩余寿命短的要么给 Redis 规划足够的内存并在监控里盯住evicted_keys指标。如果是集群方案还要考虑不同分片之间的内存均衡。6. 常见问题与排查实录6.1 重启后用户全部掉线但已经用的是 Redis这类问题通常不是“没用 Redis”而是“Redis 根本没接上”或“Spring Session 没有接管容器 session”。排查路径先看启动日志有没有出现SessionRepositoryFilter相关的初始化信息再看启动时是否报 Redis 连接失败最后看 Redis 里到底有没有 session key。如果 Redis 里根本没 key说明 session 还是落在了 Tomcat 内存里多半是依赖没引全或者配置里spring.session.store-type没生效。还有一种情况是你以前用内存 session 时负载均衡开了 sticky session换 Redis session 后 sticky 也解除了但有些老浏览器的 Cookie 里同时存了多个 JSESSIONID请求时带上旧 ID 找不到 session也会表现为掉线。遇到这种清一下 Cookie 往往就好了。6.2 Redis 里的 session key 全是二进制乱码如果你在redis-cli里看到 key 或 value 是\xAC\xED\x00\x05开头的内容大概率是 JDK 原生序列化在起作用。排查思路是把SessionRedisConfig里那个 bean 名确认一遍确认名字是springSessionDefaultRedisSerializer而不是redisSerializer。改完配置后重启应用再登录一次新的 session 就会是 JSON 格式。旧数据不会自动转换线上切换时可以先清除旧 session key或等 TTL 自然过期。6.3 Redis 连接闪断导致接口报错、session 疑似丢失Redis session 方案引出一个新问题Redis 成了关键依赖一旦连接异常所有需要 session 的接口都可能 5xx。我遇到过 Redis 实例因为 AOF 重写导致短暂阻塞结果线上大量请求在等待连接超时后抛 RedisConnectionFailureException。应对措施分三层第一层是给 Lettuce 连接池足够上限避免瞬时流量把连接打满第二层是用 Redis Sentinel 或 Cluster 保证单节点故障时可切换第三层是在代码里对 session 读取异常做降级比如允许部分只读请求在 session 缺失时以匿名身份继续而不是直接抛错。注意降级逻辑不要误放行需要登录态的写操作安全优先级永远高于可用性。6.4 内存和 Redis 双份 session 串场有些团队迁移到一半好奇“两者能不能共存”结果发现 Cookie 里的 JSESSIONID 在 Tomcat 内存和 Redis 里都能查到有的人访问到旧的有的人访问到新的。这是因为你同时保留了容器默认 session 管理和 Spring Session 过滤器两者各自创建会话极易串场。我的建议是不要做这种混搭。要迁就一次性迁干净移除容器默认 session 持久化配置只用 Spring Session 管 Redis。如果你的老代码里手动依赖session.getServletContext()这类容器 API迁到 Spring Session 后虽然兼容但很多容器特有方法已经变成空实现业务层千万别再去依赖 Tomcat 私有 API。这是迁移中的隐藏雷区。6.5 排查思路速查表下面是我平时排查 session 问题的一整套顺序先控制变量再动手。现象可能原因排查动作登录后又变匿名session 过期太快检查spring.session.timeout与 CookieMax-Age重启后全量掉线session 还在内存里登录后查 Redis 有无 session key多实例间时而登录时而没登录粘性会话未关闭看负载均衡策略Redis 里 key 是乱码默认 JDK 序列化按上文配置 JSON 序列化器Redis 内存不断上涨过期 key 清理延迟看expired_keys和evicted_keys某节点 Redis 连接池占满峰值流量超过 pool调大 max-active看 Redis 端慢日志此外注意不要把数据库 session 的概念和 HTTP session 混为一谈。像某些数据库日志里的session unused timeout, terminating connection那是数据库连接或 SQL 会话层面的超时跟 Web 登录态没有任何关系。排查时先搞清楚是哪个“session”别看见关键词就往上套容易白忙活半天。6.6 Cookie 与 sessionId 的联动问题另一个高频坑是 Cookie 的Path和Domain设置不对。如果应用部署在/app这个 context path 下但 Cookie 的 Path 被设成/那么其他应用也可能会把这个 JSESSIONID 发过来造成 Redis 里查不到对应 session白增加一次查询。反过来如果多个子域名需要共享登录态得把 Cookie Domain 设置成顶级域名并把 Path 设置成/。Spring Session 里可以通过server.servlet.session.cookie.path和domain来调动手前先理清域名拓扑。7. 我的选型经验以及一点扩展建议7.1 三个典型场景我分别怎么选我先给一套我自己在项目里反复用的判断标准。内部管理系统、工单平台这类 200 人以内的小应用单机部署我直接用内存 session连 Redis 都不碰。理由很简单部署简单出了岔子也好排查重启丢登录这件事对低频率用户几乎没有感知。电商业务或面向 C 端的门户只要是两个以上实例我会第一优先级上 Redis session。因为这种业务最怕的就是发布时用户集体掉线而且多实例之间共享登录态是硬需求。用户量再大一点、微服务拆分再细一点我会考虑把 session 从业务应用里彻底剥离统一放到一个独立的会话服务中由网关统一校验业务服务不再直接依赖HttpSession。如果你已经全面用 JWT 或 OAuth2 Token那 session 的使用场景会被压缩不少。但注意Token 方案只能证明“用户身份有效”不等于可以替代所有会话数据。临时购物车、分布式事务里的中间状态、多步表单这些还是需要一个像 Redis 一样的临时存储。这也是为什么即使很多新项目已经“无状态化”Redis 作为会话存储依然有很强存在感的原因。7.2 从内存 session 迁到 Redis 时最容易被低估的成本很多人以为加个依赖、改几行配置就完了。实际上最大的成本在数据兼容和数据迁移。老系统里 session 可能存了大量复杂对象你的自定义类型里可能藏着不支持 JSON 序列化的ThreadLocal、类加载器等脏东西迁移过去必然报错。所以迁移前要做一次 session 内容的盘点只保留必须跨请求传递的字段把重量级对象放数据库或缓存里的单独 keysession 里只留轻量引用。另一个成本是监控。内存 session 时代你基本不用看 session 相关指标上了 Redis session 之后Redis 的内存、命中率、连接数、慢查询都变成了你要养的指标。别等到线上出问题才临时去配监控上线之前就要把 Redis 运维面板和大盘拉起来。7.3 最后分享一个我自己的“土办法”如果你刚接手一个老项目不确定它当前用的到底是内存 session 还是 Redis session先别急着看代码直接登录一次然后去 Redis 里执行keys *session*。有结果就是 Redis 方案没有结果基本是内存方案。这比翻配置文件快得多。然后再看代码里HttpSession被 setAttribute 过哪些 key决定后续优化方案。这个土办法我用过很多次每次都能快速定位问题。如果实在不想维护 Redis又没有多实例需求其实也可以换一种思路用本地内存缓存 分布式失效广播或者直接让业务层做成无状态 Token。但这些方案都有自己的复杂度不会比一个健壮的 Redis session 方案简单太多。选型这件事最重要的不是选最火的而是选你团队能长期养得起的。
返回列表