ARTICLE DETAIL

资讯详情

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

后端搜索与回收站实战:从索引设计到软删除清理

后端搜索与回收站实战:从索引设计到软删除清理 做后端开发的人基本都会在某个阶段接到一个听起来“不复杂”的任务把搜索做一下再把删除加个回收站。真上手之后才发现这两个模块一个管“用户怎么找到数据”一个管“用户删了怎么后悔”看似独立的两个功能做深了全是细节。这篇就围绕后端第七章里这两个模块把我实际落地时的设计思路、代码结构和踩过的坑一次性说清楚。先对焦一下场景。我们当时是在一个前后端分离的中型项目里后端用的Spring Boot 3数据库MySQL文件存储走的本地磁盘加NFS挂载。搜索模块要覆盖菜单、商品、订单等多类业务数据回收站模块则要覆盖逻辑删除、文件回收、定时清理和空间统计。这类需求在很多后台管理系统里是标配但做得糙和做得稳之间差距全在底层设计里。1. 搜索模块从“搜得到”到“搜得准”1.1 先想清楚你的系统到底需要哪种“搜索”很多新人一上来就聊Elasticsearch我不反对但得先说清楚一个事搜索需求和搜索需求不一样。我见过一个只有几千条菜单数据的管理后台非得上ES结果部署复杂度翻了十倍搜索延迟反而没比MySQL LIKE快多少。我自己的判断方法很简单分三档第一档是数据量小、查询条件固定的场景比如菜单搜索、用户列表筛选这种情况用数据库索引加模糊查询就够没必要引入额外组件。第二档是数据量中等、搜索字段多、还有排序和分词需求的比如商品库、文档库建议直接上全文检索引擎Elasticsearch或OpenSearch省得自己在MySQL里拼LIKE拼到怀疑人生。第三档是数据量极大、并发极高、需要毫秒级响应的比如网盘资源搜索、全网商品检索这类那ES还不够前面得加一层缓存或CDN甚至要上分片路由。判断依据就是三个词数据量、更新频率、部署成本。我见过一个很典型的项目数据量不到十万条却引入了三台ES节点最后运维天天喊内存不够。其实这类数据用MySQL自带的全文索引或者简单的倒排索引表就能解决。再说一个容易被忽视的问题搜索别只盯着“搜内容”还要考虑“搜什么类型的内容”。同样是关键词“abc”用户可能想搜商品名、想搜订单号、想搜文档标题甚至想搜菜单里的某个按钮名称。所以搜索模块的第一件事是明确搜索范围不同范围走不同的索引和查询策略而不是一把梭把所有字段拼在一起全表扫描。实操心得我建议先画一张搜索需求表把业务方说的每一句话拆成“搜索入口、目标数据、关键字段、预期结果排序”四列这一步能帮后端省掉后面80%的返工。1.2 搜索算法的两个底层思想组织与遍历有人看到“搜索二叉树”“深度优先搜索DFS”“宽度优先搜索BFS”这些词就觉得是面试八股跟实际项目没关系其实不对。后端里的搜索功能底层就两件事一是把数据组织成便于查找的结构二是用合适的策略去遍历这个结构。举几个例子就通了。搜索二叉树解决的问题是“如何让查找次数尽量少”对应到后端就是索引。MySQL的B树、Redis的跳表、ES的倒排索引本质都是“为了搜索更快而设计的数据组织方式”。所以面试里问搜索二叉树考的不是你会不会写那几行代码而是你懂不懂“索引为什么能加速查询”。DFS和BFS就更常用了。菜单模块的搜索、权限树的搜索、文件夹的递归扫描全是树的遍历问题。比如搜索菜单模块时用户输入“订单”你得遍历整棵菜单树找出所有名称里含“订单”的节点而且得考虑父子层级关系命中父菜单时子菜单要不要一起返回。这就是DFS的典型应用。至于宽度优先搜索典型场景是“搜索联想”和“好友推荐”一层一层往外扩。你要实现一个“相关搜索词推荐”用BFS的思路从当前关键词出发找到同一分类下的其他热词比直接查数据库效率高得多。所以我一直有个观点算法题不是没用是你还没遇到能把它用上的场景。搜索模块恰好是把这些基础算法变成业务价值的交汇点。另一个容易忽略的点倒排索引。如果你不想引入ES但又希望搜索效果比LIKE好一点可以在MySQL里自己维护一张分词-文档映射表。原理很简单把每一条业务数据拆成关键词存一张“词-数据ID”的关联表搜索时先查词再查数据这就是倒排索引的简化版。代价是写入时要多维护一张表但换来的是查询速度的质变。1.3 技术选型MySQL、Elasticsearch 还是 Redis先上结论90%的后台管理系统MySQL加合理索引够用需要全文检索的业务ES是标配搜索量不大但想快Redis做缓存或二级索引很香。我用一张表说清楚这三者的区别维度MySQL LIKE/全文索引ElasticsearchRedis数据量适用百万级以内亿级千万级以内分词能力弱中文需配合插件强内置中文分词无需自行处理排序能力简单字段排序相关度、多字段排序依赖数据结构部署成本低高内存大户低实时性实时近实时有刷新延迟实时典型场景后台管理、菜单搜索商品、文档、日志联想词、热词排行榜我自己的选型思路是先用MySQL扛住第一版数据量和搜索体验变成瓶颈了再上ES。因为搜索模块最怕的不是慢而是你在一开始就选了一个远超需求的架构把自己拖进运维泥潭。但有个例外如果你的业务从第一天起就知道数据量会暴涨搜索体验是核心竞争力那就别犹豫直接上ES。像网盘搜索这种场景核心卖点就是“搜得到、搜得快”用MySQL硬扛后面一定重写。补充一点前后端分离项目里搜索接口往往是一个独立的聚合层前端调一个/search接口后端再分发给不同的搜索引擎和数据源。这个聚合层用Java Spring Boot或Python FastAPI都可以核心是保持接口稳定不要让调用方感知底层用的是MySQL还是ES。2. 搜索接口的工程实现与细节打磨2.1 关键词处理分词、大小写、拼音兼容搜索接口第一件要处理的是关键词本身。用户输入的关键词五花八门有带空格的、有带错别字的、有中英文混打的、有只输入拼音首字母的。如果后端不做处理直接拼进SQL那搜索体验就是灾难级的。先说分词。中文场景下把“搜索精选”拆成“搜索”“精选”两个词和直接整句匹配效果差非常多。MySQL自带的全文索引对中文支持一般实际项目里要么用ES配合IK分词器要么在业务层自己实现一个简单的正向最大匹配分词器。再说大小写。英文、数字场景下比如订单号“ABC123”用户在搜索框输入“abc123”如果数据库字段是utf8_bin排序规则就搜不到。解决方案有两个一是建表时字段排序规则用utf8_general_ci二是查询时用LOWER函数统一转小写。注意在字段上做函数操作会导致索引失效所以更推荐建表时处理好。拼音兼容这块门槛比较高。如果业务有强需求比如要支持“ruoyi”搜出“若依”那需要额外维护一张拼音映射表在分词阶段做拼音转写和前缀匹配。这个功能很加分但工作量不小我建议第一版先不做把搜索命中率做扎实比功能炫酷更重要。还有一个小细节关键词的非法字符过滤。用户可能输入特殊符号、引号、百分号如果直接拼SQL轻则查询结果异常重则SQL注入。这块别偷懒用参数化查询同时把关键词长度限制一下比如最长50个字符超了直接截断或提示。常见失误有人喜欢在前端做关键词预处理后端不校验一旦有人绕过前端直接调接口脏数据就进来了。后端必须对搜索关键词做二次校验这个是底线。2.2 排序、分页与高亮决定用户体验的三个细节先说排序。搜索结果的排序直接影响用户的第一感受。最简单的做法是按相关度排但相关度这东西得先定义清楚。我的做法是给每个命中的记录打分规则如下标题完全匹配 10标题包含关键词 8描述/内容包含关键词 5分类/标签匹配 3命中位置靠前前缀匹配额外 2品牌/热度作为加权项排序字段在SQL里就是ORDER BY score DESC, update_time DESC。这里必然要算词频或用LOCATE函数LOCATE能判断关键词在字段中出现的位置结合LENGTH计算得出一个简单的相关度分数。这个方案不用引入ES就能做出不错的排序效果核心公式是相关性分数 命中次数 * 权重 位置权重代码上就是一个遍历关键词做打分累加的过程。如果数据量大这个计算应该放在查询阶段而不是先把全部数据捞到内存里再算。再说分页。分页是搜索接口的经典痛点。MySQL的LIMIT 10000, 20这种写法翻页越深越慢因为数据库要扫描并丢弃前10000行。我在项目里处理深分页有两种方案。第一种是游标分页用上次返回的最后一个ID作为下次查询的起点适用于实时性要求高、数据变动频繁的场景。第二种是限制最大页码比如超过100页就提示用户使用筛选条件缩小范围适用于大多数后台管理场景。骨架里的深分页问题很容易被忽略等到线上有人翻到第1000页就超时那时候再来改就要面对一堆兼容问题。最后说高亮。前后端分离项目里后端返回带高亮标记的文本前端负责渲染。ES的highlight功能可以直接返回高亮片段但MySQL方案就得自己处理。我的做法是在后端做字符串替换把命中的关键词用特定的标签包裹起来比如em订单/em前端拿到之后统一渲染。注意不要直接把HTML标签写死在数据库里而是返回纯文本加标记位。2.3 搜索历史与联想推荐把“搜索精选”做出来搜索模块做得好不好不只是“搜不搜得到”还包括“用户搜的时候爽不爽”。搜索历史和搜索联想就是两种很典型的体验增强。搜索历史我去过最省事的实现方式Redis里存一个List以用户ID作为key每次搜索时把关键词push进去保留最近20条。返回的时候倒序取出来就行。这里有几个细节一是去重同一个词多次搜索只保留最新一次二是过滤敏感词和空白词三是历史列表要分用户隔离别搞成全局共享。搜索联想就是输入过程中下拉提示的那个稍微复杂一点。最简单的方案是维护一张热搜词表用户每搜索一次就把关键词的词频加一联想时按词频倒序取前10条。这个方案实现成本低效果也够用。进阶版是前缀匹配加拼音匹配比如用户输入“dd”能联想到“滴滴后端面经”这种热度词。这个就得靠前面说的拼音映射表来实现。还有个容易被业务方点名的功能叫“搜索精选”其实就是运营把某些关键词的搜索结果固定下来比如用户搜“新人福利”返回运营配置的置顶商品。后端实现上就是在搜索接口里加一个“置顶命中的规则ID”的匹配逻辑命中了就插入到结果集最前面并标记‘运营推荐’。这个功能很讨喜但你得先规划好数据表否则后面运营提了一堆配置需求你会被改到崩溃。实操中的一点提醒搜索历史、联想词、搜索精选这三块业务上经常混为一谈但技术实现路径完全不同。跟产品经理确认需求时一定要让对方明确“输入框下拉出现的内容来自于哪里”否则后端做一堆前端完全不匹配就会出现两个模块都对不上号的尴尬。2.4 搜索语法高级筛选背后的解析逻辑有些系统对搜索要求比较高会提供“搜索语法”比如菜鸟教程里那种key:value的组合查询。用户输入status:已支付 amount:100-500就能精确筛选。实现思路不复杂后端先对关键词串做一次语法解析拆成多个条件块再拼接查询条件。这个解析过程用正则就能搞定但要注意边界情况值里含有空格、冒号、中划线的情况。我的做法是先用空格拆段再用冒号拆键值最后根据值的格式判断是精确匹配、范围匹配还是模糊匹配。这类功能在搜索引擎、网盘搜索工具里很常见比如“输入特定语法搜学号、搜文件类型”。后台管理系统里如果做了这个效率提升非常明显。但要注意普通用户不会用也记不住所以语法要足够简单同时在前端输入框旁边放一个语法提示说明。这一段做一个简单的实现描述// 关键词解析示例将 “status:paid name:订单” 解析为条件列表 ListSearchCondition conditions new ArrayList(); String[] parts keyword.trim().split(\\s); for (String part : parts) { if (part.contains(:)) { String[] kv part.split(:, 2); conditions.add(new SearchCondition(kv[0], kv[1])); } else { conditions.add(new SearchCondition(default, part)); } }解析完后遍历conditions拼接动态SQL注意所有值都用参数绑定防止注入。3. 回收站模块先设计好“软删除”这一层3.1 为什么不能直接DELETE业务的后悔药与数据安全回收站模块的核心说穿了就是一条原则业务删除不等于物理删除。用户点击“删除”按钮的时候心里想的其实是“这东西我不想要了但要后悔了还能拿回来”。如果你后端直接DELETE FROM那这行数据就彻底没了后面无论什么恢复、审计、追溯统统没戏。我见过很多项目第一版图省事删除就是DELETE等到线上运营误删了一批数据所有人抓瞎。后来不得不在代码里加各种日志表、操作记录表来弥补成本远高于一开始就设计好软删除。正确的做法是业务表增加一个deleted字段0表示正常1表示已删除。同时增加deleted_at删除时间和deleted_by删除人。查询列表时默认过滤掉deleted1的数据而删除操作只是UPDATE这个字段。但这只是最基础的软删除。真正做回收站还需要一张独立的回收站元数据表记录“哪个用户、在什么时间、把哪一条业务数据、从哪个模块删掉了”。不要试图在每张业务表里都塞一堆回收站字段而是用一张统一的回收站表把各个模块串起来。这里我要说一个关键区别软删除是数据层的逻辑回收站是业务层的呈现。你可以软删除而不用回收站比如只是下沉归档也可以做回收站而不只是软删除比如还要支持批量恢复、定时清理、空间统计。设计时先分清这两层代码才不会越写越乱。3.2 回收站表结构设计类型维度、删除时间、原路径直接给一个我实际用的表结构大家可以根据业务调整CREATE TABLE recycle_item ( id bigint NOT NULL AUTO_INCREMENT, biz_type varchar(50) NOT NULL COMMENT 业务类型file/document/menu/order等, biz_id varchar(64) NOT NULL COMMENT 业务数据ID, source_table varchar(100) NOT NULL COMMENT 来源表名, original_data json DEFAULT NULL COMMENT 删除前的原始数据快照, file_path varchar(500) DEFAULT NULL COMMENT 如果是文件记录原路径, file_size bigint DEFAULT 0 COMMENT 文件大小用于统计空间, deleted_by varchar(64) NOT NULL COMMENT 删除人, deleted_at datetime NOT NULL COMMENT 删除时间, expire_at datetime DEFAULT NULL COMMENT 过期时间超过后自动清理, status tinyint NOT NULL DEFAULT 0 COMMENT 0待清理 1已恢复 2已物理删除, PRIMARY KEY (id), KEY idx_biz_type_deleted_at (biz_type, deleted_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表解决了几个问题一是统一管理。不管你是删菜单、删文件、删订单都往这里插一条记录列表页查询、恢复、清理全部走同一套代码。二是数据快照。有些业务数据删除之后原表的结构可能变了但回收站里的快照还能还原当时的完整数据。三是空间统计。文件回收要记录file_size这样清理的时候才能计算释放空间对应到运营层面的“本次清理释放空间”报告。这里有个容易被忽略的细节original_data的快照内容。我建议删除的时候用JSON序列化把整行数据存下来而不是只存biz_id。因为恢复时如果原数据已经被修改或部分删除单靠ID不一定能还原。这个JSON字段看起来冗余关键时刻能救命。补充如果嫌JSON占用空间大也可以只存关键字段的变更历史但我的经验是“完整快照”收益远超成本尤其是在数据量不算大的管理系统中。存储便宜但数据不可恢复的代价太贵。3.3 存储占用与容量控制别让回收站变成“垃圾堆”回收站如果不做限制会变成一个隐形磁盘杀手。用户在系统里删了一个10G的压缩包这10G还占着磁盘但用户意识不到因为界面上看不到了。等你磁盘爆了再来排查往往已经晚了。我建议第一版就做两个限制保留期限策略默认保留30天过期自动物理删除。某些重要文件可以延长到90天但不管哪种一定要有expire_at字段并启动定时清理任务。容量上限策略类似Windows回收站设置最大占用空间。比如整个回收站占用超过磁盘的10%就不再接收新文件入回收站直接提示“回收站空间已满请先清理”或者触发压缩和淘汰策略。这两个策略听起来简单落地时经常出问题的点是不同业务类型往往需要不同的保留期限。我的做法是在biz_type上做配置比如文件模块保留30天、菜单模块保留7天、订单模块保留90天配置项放配置文件或数据库表里别写死在代码里。还有一个现实问题当回收站里数据量很大的时候列表页怎么查如果你不做限制用户看到的是一个几千条数据的回收站列表恢复和清理都会很卡。我建议列表页默认只展示最近30天的数据并且提供业务类型和删除人筛选。同时加一个“清空回收站”按钮一键物理删除所有待清理记录。这个按钮必须有二次确认弹窗且只能管理员操作操作前要记录日志。一个容易忽略的技术点MySQL里软删除字段要建索引。因为所有查询都会带WHERE deleted0这个字段如果没有索引会引起全表扫描。我记得之前项目里一张表忘记加deleted索引查询直接慢了十倍。deleted、deleted_at、deleted_by三个组合索引基本是标配。4. 清理任务与空间释放的全链路设计4.1 定时清理机制自动扫描、逐批删除、记录释放空间回收站里的数据不会自己消失得靠后端定时任务去清理。这块我踩过最大的坑是一次性把所有过期数据捞出来删结果数据量一大SQL超时数据库连接池被打满整个服务都跟着瘫了。正确的姿势是分批清理。核心思路是每次只取一批过期记录比如500条处理完再取下一批处理完毕退出。整个流程可以用Spring Boot自带的Scheduled注解实现也可以接分布式任务调度平台看团队的部署规模。清理任务的处理逻辑大致是1. 每小时执行一次 2. 查询 recycle_item 中 status0 且 expire_at now() 的 500 条记录 3. 遍历记录先物理删除原表中的数据如果原表还保留的话再删除文件系统的真实文件最后把 recycle_item 状态置为已清理 4. 累加本次清理释放的空间大小 5. 写入清理日志表供后续统计和展示这里要特别强调清理动作分两步先删原表数据再删真实文件。如果先删文件后删数据库中途失败了数据库里会残留大量无效记录反过来先删数据库再删文件中途失败会残留孤儿文件。我的建议是先删文件再删数据库记录因为孤儿文件比孤儿记录更难排查和回收。对比一下类似“安全清理电脑磁盘空间”的需求用户希望系统能自动扫描并删除临时文件、更新缓存、软件日志、无效缓存、回收站冗余文件但保留个人文档、照片、安装软件等重要数据。这个需求放到后端系统里就是我们的定时清理任务要能区分“可清理项”和“保留项”在配置文件里维护一个清理白名单/黑名单比如明确哪些目录可以删、哪些文件类型必须保留清理前先扫描计算可释放空间清理后报告实际释放大小。本质上就是把业务规则嵌入到清理任务里而不是无脑删除。函数化之后大概是Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void cleanExpiredItems() { ListRecycleItem batch recycleItemMapper.selectExpired(500); long freedSpace 0; while (!batch.isEmpty()) { for (RecycleItem item : batch) { // 删除真实文件并记录释放空间 boolean deleted fileService.deleteIfExists(item.getFilePath()); if (deleted) { freedSpace item.getFileSize(); } // 更新回收站记录状态 recycleItemMapper.markCleaned(item.getId()); } // 记录本次执行日志 cleanLogService.record(batch.size(), freedSpace); batch recycleItemMapper.selectExpired(500); } }这段逻辑虽然简单但有几个需要补强的地方任务要支持幂等同一批记录不能被两个调度实例重复处理。我在表上加了一个status字段来处理查的时候带FOR UPDATE SKIP LOCKEDMySQL 8.0支持或者先更新status为“处理中”再处理防止并发清理。4.2 文件回收与物理删除的解耦标记、异步、补偿回收站里如果有文件那“删除”动作实际上涉及两个存储系统数据库和文件系统。这两个系统的操作做不到原子性所以必须设计一个稳妥的流程来保证最终一致。我的方案是三步走标记回收调用删除接口时先把recycle_item记录插入或更新状态置为“待清理”同时把业务表里的deleted字段置为1。这个阶段不碰文件系统。异步物理删除定时任务扫描“待清理”记录把真实文件从磁盘删除并更新状态。这个过程是异步的不影响用户操作。兜底补偿不管哪个环节失败了最终由定时任务反复重试直到删除成功。重试次数超过阈值就把记录标记为“异常”方便人工介入。为什么要设计成异步而不是删除时直接物理删除因为用户点删除时系统只承诺“数据进了回收站”不代表磁盘立刻释放。异步清理把“用户可感知的操作耗时”和“后台资源回收的耗时”解耦前台的删除永远快后台的清理慢慢来。这个设计在文件数量多的场景下体验差距非常明显——我见过同步删除NFS网络盘上的文件一个1G的文件删除等了好几秒用户早就以为卡死了。这里可以通过一个状态机来管理0-待回收数据已软删除但原文件还在1-待清理回收期已过等待物理删除2-已清理文件已物理删除数据库记录已清理3-已恢复数据被恢复回收站记录关闭4-清理失败多次重试失败需要人工处理在代码里状态流转用枚举控制不直接写数字。避免后期维护的人看不懂。4.3 清理结果通知释放了多少空间如何计算与上报很多项目做回收站模块时忽略了一个很重要的体验点清理完之后告诉用户释放了多少空间。这个功能看似简单却是运营能直观感受到系统价值的窗口。实现思路清理任务执行时累加每条记录的file_size字段得到本次释放的空间大小。然后把释放结果写入一张清理日志表。这张表可以记录执行时间、清理条数、释放空间、耗时等信息。用户端或管理端可以展示“最近7天共清理XX条数据释放XX GB空间”之类的汇总。计算释放空间时要注意数据库记录里的file_size是逻辑大小不等于实际磁盘占用。如果文件系统块大小是4KB一个3KB的小文件实际占用4KB一个10KB的文件实际占用12KB。如果你需要精确统计就要在删除文件后用File.getTotalSpace()和getUsableSpace()这两个API直接读磁盘剩余空间的变化量更准确。还有一点清理报告要以“实际删除成功”的数据为准不能把“尝试删除但文件不存在”的记录算进去。文件本身可能已经被人手动删掉了这部分只能算“减少垃圾记录”不能算“释放空间”否则报告会虚胖。我对这个功能的评价是做一个简洁的清理报告前端一次展示比做十个花哨功能都更有说服力。用户看到“释放了3.2GB空间”这个数字才能体会到后台模块的价值。5. 文件回收站的经典问题与排查技巧实录5.1 “找不到回收站目录”的几种真相前几年我在一个信创项目里遇到一个特别诡异的问题操作系统是麒麟V10加装第二块SSD作为数据盘后用户删除文件时提示“无法为...找到或创建回收站目录”。这个问题在网上也有很多人遇到但排查思路往往不完整。我从后端视角复盘一下真相通常有这几种第一种是真的缺目录。用户主目录下没有.local/share/Trash目录或者数据盘挂载点没有.Trash-用户ID目录。图形化文件管理器删除文件时依赖~/.local/share/Trash/files和info目录缺了就报错。解决办法是手动创建目录结构并赋予正确权限。第二种是权限不足。回收站目录需要用户有读写权限如果SSD数据盘挂载时用了noexec、nosuid或者挂载点在root用户下普通用户没有创建目录的权限就会报找不到回收站目录。排查方法是用df -h和mount -l确认挂载选项。第三种是文件系统类型的问题。有些文件系统如FAT32不支持Trash语义或链接操作删除文件时也会有类似报错。换成ext4或xfs格式就能解决。那这个问题放到我们的后端回收站模块中对应的是文件存储目录要有独立分区且程序启动时要检查目录是否存在、是否可写。如果应用在启动时不检查这些正式环境上线后第一次删除就报错那可比开发环境晚发现得多。我强烈建议项目里做一个启动自检组件启动时检查文件存储目录、回收站目录、临时目录是否可读可写不能通过就fail-fast。这个组件十几行代码能省掉大量生产环境的前期踩坑。5.2 删除很慢、卡顿、前台超时的处理删除文件很慢通常发生在网络存储场景比如文件放在NFS上或者远端对象存储上。用户在前端点删除后端同步去删远端文件网络一抖动就超时。我的处理思路是前端删除一律走异步接口。前端调删除接口后端只负责把数据标记为“回收状态”返回“已移入回收站”。真实的文件删除放在后台清理任务里做。这样即使文件系统很慢也不影响前台的响应速度。如果某些场景必须同步删那就分成两步先在本地把文件移动到回收站目录同文件系统内mv操作很快再异步真正删除。这个思路与Linux桌面环境的Trash实现一致先RENAME到回收站目录再在后台清理。另外删除NFS文件慢还有一个常见原因NFS客户端没有开启actimeo参数导致每次文件操作都要重新向服务端确认属性。要提速就把挂载参数调一下设置合理的actimeo和noatime。做回收站模块时文件移动和复制也要注意跨文件系统的问题如果原文件在A盘回收站在B盘那移入回收站实际是复制加删除这个操作如果文件大会非常慢。因此回收站和数据文件尽量放在同一个文件系统内才能保证秒删。5.3 误删恢复与数据一致性回收站的“恢复”功能看起来就是把记录置为未删除但实际场景里坑不少。第一个坑是路径冲突。文件被删除后用户可能在原路径新建了一个同名文件恢复时就会冲突。我的方案是恢复时检测原路径是否存在存在就自动加后缀比如“订单(1).xlsx”并在响应里返回新的文件名让前端提示用户。第二个坑是权限变化。删除时用户有权限恢复时可能没权限了比如用户被调整了角色。恢复操作必须校验当前用户对目标数据的权限否则就会出现越权恢复的问题。第三个坑是跨模块恢复。回收站里存了菜单、文件、订单等多种类型恢复时不能一刀切。菜单的恢复要检查父菜单是否存在文件的恢复要检查存储目录是否还在订单的恢复要检查关联商品是否还在售。这些校验逻辑应该放在各个业务模块自己的恢复方法里而不是回收站模块的大而全逻辑里。我的建议恢复操作设计成模板方法模式回收站只负责找到对应业务模块的处理器具体的校验和恢复逻辑由各个模块自己实现。这样回收站模块不会越来越臃肿业务逻辑也能保持内聚。5.4 其他高频问题速查清理不动、空间没释放、状态不对我整理了一个回收站模块的常见问题速查表这些大部分都是在实际项目里遇到过的现象可能原因排查顺序清理任务没执行定时任务被调度中心摘除或数据库锁等待先看调度日志再看数据库锁表情况文件删除了但空间没释放文件被进程占用或删除的是小于块大小的稀疏文件用lsof查占用进程用du对比实际大小回收站记录状态一直“待清理”有事务没提交导致行锁或重试逻辑写错查事务和锁等待检查重试次数恢复后数据丢失original_data快照不完整检查删除时快照序列化是否截断回收站列表越来越慢deleted_at和biz_type没建索引看执行计划补联合索引磁盘爆满但回收站没多少数据可能有人绕过回收站直接删除或日志没用归档全盘扫描obj文件检查代码里是否有skip recycle的接口这张表的价值在于它把排查思路固化了。我见过很多同事遇到问题就重启服务、清缓存实际上仔细看日志和锁状态五分钟就能定位。6. 跟其他角色对齐需求时后端最容易失控的地方6.1 跟产品经理把“搜索和回收站”的范围聊透彻有个热搜词是“java后端怎样和产品经理确定”我太有感触了。搜索和回收站这两个模块产品经理往往一句话就完事但后端如果闷头就去开发后面一定被改到怀疑人生。我的做法是开需求会时直接问清楚八个问题搜索范围有哪些业务类型这些类型的字段结构差异大不大搜索是否需要支持分词、拼音、错别字纠正搜索结果的排序规则是什么回收站是全局统一入口还是每个模块单独的入口回收站的保留期限是多久回收站是否需要空间容量限制清空回收站需要哪些权限恢复时遇到冲突怎么处理这八个问题每个你都跟产品经理明确给出默认方案对方没异议就按默认做有异议当场确认。比如排序规则产品经理大概率只会说“相关的排前面”那你就解释一下“相关”在本系统里的计算逻辑是用标题权重还是热度权重给两个具体的例子让他选择。这样书面确认下来后面才不会返工。6.2 前后端联调中的几个摩擦点搜索和回收站模块在前端联调时有常见的几个摩擦点第一个是跨域。后端搜索接口通常在不同域名下前台调试时最容易遇到CORS问题。配置上要区分生产环境和开发环境生产环境用Nginx反向代理处理开发环境本地起一个代理服务或者配置CORS白名单。第二个是按钮重复提交。用户搜一次点一次或者连续点了多次删除按钮后端如果不去重结果会出现重复提交、重复删除。前后端都该做防抖但后端也要做好幂等。我的方案是在删除接口上做一个基于用户ID和业务ID的唯一键约束同一用户5秒内对同一数据重复提交直接返回成功。第三个是并发删除和恢复同时发生。用户一边删一个文件一边在另一个标签页恢复这个文件两边的请求打到后端处理顺序不同结果就不同。解决方式是在recycle_item表上做乐观锁版本号更新时带上版本号版本不一致就提示“数据已变化请刷新”。第四个是RECYCLE列表状态同步。前端显示回收站列表后如果清理任务刚好把某条记录清掉了前端再操作恢复就会404。后端要返回合理的错误码并引导刷新前端也要做“列表状态过期”的兜底。6.3 多个后端项目合并时模块边界怎么划另一个热搜词是“多个java后端项目合并要点有哪些”。搜索和回收站模块在项目合并时边界尤其容易出问题。因为这两个模块渗透性很强——搜索会关联所有业务表回收站也会被各个业务调用。我的边界划分原则是两条搜索模块作为基础设施不直接依赖具体业务表。搜索模块定义统一的Searchable接口各个业务模块负责把需要被搜索的数据“注册”进来。这样合并时A项目的搜索模块不会被迫理解B项目的表结构。回收站模块只负责元数据和流程不直接操作业务表。回收站定义统一的Recyclable接口业务模块实现恢复和真实删除的逻辑。回收站本身不关心订单长什么样、文件存在哪只负责调度。这样设计的好处是多个项目合并时搜索和回收站都可以作为一个独立的中间件级别模块抽离出来用配置方式接入不同业务方。代码用参数化导入去适配而不是各自复制一份实现然后改得面目全非。7. 最后分享几个我反复用到的经验这章写到这里核心内容基本都覆盖了。最后再分享几个我个人实际操盘项目时反复验证过的经验。搜索引擎约慢越要先看索引。我排查过很多搜索慢的接口大部分不是数据库慢而是查询没有走索引。EXPLAIN SELECT这个命令是最重要的调试工具没有之一。看到typeALL就说明全表扫描了这时候先优化索引比谈什么ES、Redis都更实在。回收站的清理任务要写在业务低峰期。文件删除会大量消耗IOPS如果在业务高峰期跑清理任务磁盘IO被抢占正常接口的延迟会肉眼可见地变高。我们的实践是凌晨执行和备份任务错峰效果不错。测试用例里一定要覆盖“恢复冲突”和“清理失败重试”。这两个场景在开发阶段很容易忽略但线上出问题基本都是这俩。让测试同学重点造数据尤其是同名文件恢复、文件被外部删除后清理失败这些都能提前暴露代码里的边界漏洞。数据快照是回收站最容易被低估的字段。别嫌那一个JSON字段浪费空间当你在线上看到一条三年前的已删除订单业务方要求恢复而这单关联的商品、价格、优惠券配置全变了如果没有快照你只能手工拼数据。有了快照一条INSERT就把能恢复的全恢复了。我自己在这两个模块上确实交了不少学费。第一次做搜索上来就选型ES结果运维成本高得离谱第一次做回收站删除是同步物理删线上一个大文件删除直接卡死应用。后来才慢慢明白搜索和回收站这两个模块拼的不是技术多炫而是设计上对业务的理解够不够深。把数据组织好、把状态设计好、把边界划清楚比堆一堆中间件更靠谱。如果你正准备开发这两个模块我的建议是别急着写代码先按上面的思路把表结构和状态机设计好再动手。磨刀不误砍柴工这两个模块一次性做对的收益比后面反复重构要高得多。
返回列表