ARTICLE DETAIL

资讯详情

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

ShenBi简历实习部分亮点解析

ShenBi简历实习部分亮点解析 实习情况首先定位要清楚就是我们是一个初创公司然后规模比较小研发部就四五个人。然后全部都是实习生公司主要是为了减少成本以及快速试错然后招到我们几个人来快速实现原型验证市场因此从0到1实现了这个图片检索系统。业务是什么这里以公司的第一家客户为例该客户公司是做家居行业的内部有成千上万的家居产品每个产品的原型图以及各种概念图很多该公司的新销售一般很难快速掌握全面的产品类型这就导致该公司的顾客来询问是否有某些类似产品时新销售总是要花费大量时间去搜索和寻找。本系统就是为了解决这个问题而开发的我们借助 AI 的图片检索能力来完成了这么一个事情帮助销售在海量图片当中自动匹配搜索最最相似的图片返回这样销售就能快速知道是否本公司具有该顾客想要的产品。类似于某宝上的图片搜索功能是比较类似的。然后因为都是实习生然后基本上每次要开发什么功能就大家随意认领这样子会开个简单的需求分析会然后大家认领就完事儿了。然后下面提供了一下我原来版本的写法如果你的简历比较空可以试试下面的写法供理解和参考1、负责智能图像检索平台的 AI模型研发与系统架构设计构建基于 Spring Boot MinIO Python 模型 Milvus MySQL 的一体化图像搜索系统实现 用户图像上传与自动分割、特征向量提取、相似度检索与多维度过滤支撑高精度、智能化的图像内容搜索与管理2、基于 Detic CLIP 构建零样本家具检测与裁剪能力将大规模预训练模型迁移至家具场景同时开发用户反馈驱动的图像检索训练 Pipeline基于自定义 Triplet Loss 微调 ResNet101 预训练模型并结合 Milvus 向量数据库 实现高效相似度检索3、设计并实现基于 Redis 并发锁机制 的用户注册安全体系集成 图形验证码与短信验证码双重校验并通过IP、手机号实现多级限流、验证码一次性消费与并发锁控制保障系统安全性与数据一致性4、重构图片数据删除流程为 基于 RabbitMQ 的异步事件驱动架构解耦数据库、对象存储与向量检索服务结合 日志监控、Spring Retry 重试机制与邮件兜底通知提升任务容错与运维可控性同时 显著缩短接口响应时间 提升用户体验5、设计 Spring Scheduled 定时任务 数据库索引 方案每日批量检查即将到期账户通过短信提醒用户提升续费率与用户体验6、基于 AI 编程工具Cursor辅助前端功能开发及各类 CRUD 业务实现提高编码效率与代码质量同时编写产品功能说明文档搜索业务这里介绍一下主要的业务是什么样的。用户是先上传查询图片然后查询图片会被自动分割的嘛这样用户就可以不用自己手动裁剪得到自己想查询的主体部分了然后分割完之后点击自己最想要的那部分的图块然后就可以进行搜索了因此主要有两个部分分割和搜索部分。Milvus 中图片向量的 ID 和 MySQL 中图片的 ID 二者是一一对应的因此可以直接匹配对应搜索。而 Minio 上图片的存储路径是存放在 MySQL 中图片记录字段里的。关于上述两点可以在上传部分的代码中看到保存 MySQL 中保存 Milvus 中分割逻辑:检索逻辑:模型优化相关具体看这篇文章模型优化的具体内容以及代码短信注册业务更加具体的内容可以看这里短信注册业务口述如下当用户访问公司官网如果对我们的产品感兴趣想试用产品时可以来到注册页面进行注册。在注册页面只需要输入用户名以及所属企业然后输入一个手机号和填写手机号验证码的内容。这里直接讲主要流程了用户需要先点击获取短信验证码这个按钮然后会弹出一个图形验证码窗口这里的图形验证码就是四个歪歪扭扭的数字或字母再添加了一点杂乱的横线曲线的背景这个验证码是通过 HuTool 工具包来完成的并且转成 Base64 编码的格式发送给前端。当点击获取短信验证码按钮时用户的手机号就会被作为获取图形验证码接口的请求参数为了防止被爆破首先会对这次请求的 IP 地址进行访问次数限制也就是获取客户端的IP做限制, 1分钟内最多10次也就是说一分钟内来自同一客户端的最多可以访问这个图形验证码接口最多十次一直刷新或者说一直换图形验证的图一分钟最多换十次。然后判断手机号格式是否正确使用的正则表达式判断是不是符合中国大陆的手机号格式。然后再判断这个手机号是不是已经发送过对应的短信验证码了, 如果在 Redis 中存在的话那压根就不用再返回图形验证码的请求了因为这个肯定是之前就已经发送过短信验证码了此时只要再返回该手机号对应的剩余发送时间即可因为一个手机号一分钟只能发送一次短信只要发送之后就会在 Redis 中设置一个对应的计时器key为手机号然后value为1分钟倒计时秒数。为什么需要这个主要是因为正常情况下在用户填对图形验证码的结果之后点击验证就会发送短信验证码了此时前端的“获取短信验证码”按钮就会开始倒计时一分钟此时用户是没办法再次点击发送的但是如果用户刷新了页面前端的“获取短信验证码”按钮就会被显示出来哪怕该手机号还在一分钟的冷却期内因此这里需要校验一下这个手机号是不是在一分钟内发送过短信了如果在一分钟内已经发送过那么就不再返回图形验证码也是为了进行流量的限制避免返回不必要的IO资源。上面都判断通过说明是一个合理的请求那么此时就调用 HuTool 工具包生成一个图形验证码再生成一个该图形验证码所对应的唯一标识 UUID将该 UUID 作为 key 然后对应的图形验证码的答案作为 Value 存入 Redis 中然后设置两分钟有效期正常人在两分钟内是肯定可以填完这个验证的。最后将这个图形验证码转为 Base64 图片编码然后和对应的唯一标识一并返回给前端即可。此时前端就会拿到一个图形验证的图形以及这个图形验证对应的唯一标识。当用户输入图形验证码之后就会点击验证也就是访问真正的发送短信的接口了。当用户点击验证按钮时,就会将用户填写的手机号,刚刚返回的uuid也就是图形验证码的唯一标识以及用户输入的图形验证码答案一起发送到这个发送短信的接口。首先对来源 IP 请求发送短信的频率限制检查, 5分钟内最多发送3次也就是说五分钟内最多发送三次短信这样又可以降低接口被爆破成功的概率。然后校验图形验证码的答案也就是拿着图形验证码的这个唯一 UUID 标识作为 Key 去 Redis 中查询对应图形验证的答案和用户输入的图形验证结果进行比对如果发现 Redis 中不存在该 Key 对应的 value那么就说明验证码已经过期了或者说这个验证码压根儿就是恶意用户随便输入的直接返回报错就可以如果发现和正确的图形验证结果不一致那么就返回输入的图形验证码错误重新请求即可。如果相同也就是说图形验证码验证通过的话那么就立马删除掉 Redis 中的这个图形验证的 Key防止后续可能被重复使用。然后校验手机号格式依然是用正则表达式。然后再次判断这个手机号是不是已经发送过对应的短信验证码了也就是是不是还处在一分钟的冷却期内如果一分钟内已经发送过了那么这里就还是直接返回剩余多少秒即可。然后生成随机六位数调用 Ucloud 的 API 发送短信给用户手机。发送成功之后在 Redis 中保存验证码和限频标记保存验证码的意思就是使用电话作为 Key然后生成的对应的短信验证码为 Value在 Redis 中保存五分钟时间也就是说五分钟之内这个验证码都是有效的。而限频标记的意思就是以该手机号为 Key一分钟倒计时这表示一分钟之内只能发送一次短信。最后返回通知前端验证码已经发送。当用户收到验证码填完表单信息之后就可以点击注册访问真正的注册接口了。在这个接口当中由于存在先判断手机号注册过没注册过然后再进行手机号的注册这个操作因此这里需要进行加锁为什么假设有两个线程 A 和 B 都使用同一个手机号来注册A 查询的时候没注册过B 查询的时候也发现没注册过于是 A 和 B 可能有一个先注册了那么另一个再注册此时就可能有问题了比如数据库中可能就出现了两个相同的手机号使用数据库唯一性约束倒是也可以但是很有可能在保存到数据库之前的业务非常耗时比如说几分钟的时间结果到保存到数据库的时候发现已经存在了存不进去这效率就非常的低下因此加个锁还是可以的虽然这种情况不太常见但是毕竟这个是公司的业务嘛还是小心一点比较好。为什么会想到这个?其实是因为当时是在别人的代码框架上改的嘛用的开源的 ERP 系统, 直接改数据库就是对原有系统有侵入了,就自己想了这么个法子,也顺便可以实践一下并发操作,虽然确实是高射炮打蚊子.怎么加锁呢一开始其实想的是直接加一个 synchronized 锁就可以了因为对于注册功能来说其实并发量是不可能很高的甚至可以说非常低一个 synchronized 锁足够了直接用syn锁住整个注册接口方法这样所有用户的注册就都得串行执行了但是当时问了 GPT 他认为这还是有点太粗暴了万一真的有很多人用的话这个效率就比较低了于是最后优化成了 synchronized ConcurrentHashMap 的组合形式。也就是一种细粒度的 synchronized 锁形式我们用 ConcurrentHashMap 保存手机号和其对应的一把 Object 对象锁也就是 key 是 手机号然后value是对应的 Object 类型的一把对象锁。然后在接收到用户请求时使用 ConcurrentHashMap 的 computeIfAbsent 原子方法当 map 中不存在对应手机号的时候才往里存该手机号并 new Object() 对象作为该手机号的 Value也就是为每个手机号设置一把专用的对象锁如果 map 中已经存在该手机号的话那么就返回之前已经 new 好的 Object() 对象锁也就是相同的手机号来进行注册的话那么锁对象就是相同的也就会串行执行了。此时再使用这个对应手机号的 Object 对象锁来进行 synchronized 同步控制由于是对同一个手机号的多线程注册进行了并发控制因此比原来那个不管谁来都是直接锁住整个方法的效果都要好起到了优化的效果。为了保证锁资源的释放使用了 try- catch- finally 的机制在 finally 中执行了 map 中对应 key-value 的删除操作。讲完了锁机制的优化那么接下来说一下这个业务流程对于注册接口来说不需要 IP 访问的频率控制,因为如果没有前面两个接口的信息的话,这个接口它也请求不了而且就算请求了实际上也就是多请求几下并没有什么资源的损失不像前面的图形资源还有短信资源不限制的话损失有点大。然后还是要先校验一下手机号是不是对的符不符合大陆手机号规范然后校验一下提交上来的手机验证码是不是对的根据用户上传上来的手机号去 Redis 中查找之前保存好的对应的短信验证码然后和用户上传上来的验证码进行一个比对如果错误直接返回如果正确的话那么就检查一下手机号是不是已经注册过了也就是查询一下数据库中是否存在不存在那么就保存到数据库中即可。验证码验证成功且用户信息已经持久化之后立即删除key防止重复使用也就是删除那个保存了五分钟的验证码。整个业务流程就结束了。注意这个业务中大量的使用到了 Redis 包括计时啊以及次数记录啊数据结构采用的都是 String。MQ 解耦优化公司的项目一开始要尽快的跑起来因此当时有一个删除图片数据的操作我们是直接在一个接口里面同步删除MySQL数据库、milvus向量数据库、minio对象数据库中的图片数据这很明显是非常糟糕的主要有三点原因1、由于是同步删除删除效率上非常慢假如一张图片十几兆或者说直接删整个逻辑数据库的话也就是很多张图片这样要一个删除完了才能进行删除下一个对于前端用户来说非常慢效率低下。2、由于是同步删除假如当前这次请求我在MySQL当中删除成功了但是执行到删除 milvus 还有 minio 的时候出现了异常导致没删除那么此时就会造成数据不一致前端用户的体验非常不好同时后台还有脏数据。3、三个数据库耦合性太高拓展性太差因为是同步操作那假如后面我可能这个图片数据还要放在某某数据库或者说删除完了之后要做别的服务通知我岂不是还要来改动这个接口往这个接口里面加代码这显然会继续增加耦合度而且拓展性非常差。这不是一个合理的做法:基于上面的原因我选择了引入mq 消息队列。因为消息队列比较核心使用比较频繁的使用场景就有三个解耦、异步和削峰。不过这里我们主要是用到了解耦和异步这两个比较重要的机制。异步本来要 5 5 5 15 ms 的执行时间引入MQ之后只需要 6 ms 了而且异步还有个好处假如我们的 milvus 数据库宕机了或者哪个数据库一时间不可用那么用户的操作也是不会失败的只要删除的消息发送到了消息队列那么就算删除成功不影响业务而且也提高了用户的响应速度。之后等数据库恢复访问之后那么 mq 是可以继续传递消息的还可以重试直到删除成功还保证了一致性。解耦解耦就更简单了我们后续新增数据库服务或者是别的什么服务就不需要去改动原来接口中的代码只需要让新添加的服务去订阅对应的主题就可以了。这里再补充一个削峰的概念项目中程序是怎么写的1:交换机与队列配置 DeleteImageMQConfig.java2: 生产者发送消息,也就是在服务层中进行发送MQ的消息发送3: 消费者之一,MinIO删除处理器MinioDeleteConsumer.java:4: 消费者之一,Milvus删除处理器MilvusDeleteConsumer:那如果是一个java程序要调用python程序是通过http进行调用的,Java程序中的代码需要等到python拿到调用结果才能继续往下执行,这种情况是不是不适合使用mq?总结一下过程实际上就是先定义了一个 fanout 交换机从代码中可以看出然后又定义了两个队列分别对应 minio 的删除队列和 milvus 的删除队列将这两个队列绑定到 fanout 交换机上。然后在删除业务中数据库直接调用 sql 删除对于 minio 和 milvus 上的数据则是通过 rabbitTemplate 发送广播消息出去我们同时定义了两个数据库的删除Consumer在这两个消费者类上用了RabbitListener(queues DeleteImageMQConfig.MILVUS_QUEUE)这个注解表示监听对应队列上的消息然后用RabbitHandler这个注解来标识对应消费方法的处理逻辑。同时还使用了 Spring Retryable 重试机制保证了在业务处理发生异常时自动重试再次执行确保最终一定 能删除掉数据Retryable(value{Exception.class},// 遇到哪些异常需要重试maxAttempts3,// 最大重试次数包含第一次backoffBackoff(delay2000,multiplier2)// 延时指数退避)如果超过最大重试次数那么说明有问题了在超过最大重试次数之后会调用 Recover 标识的兜底通知方法在该方法内部会使用邮件通知有关人员去检查排查错误// 当重试多次都失败时会调用这个方法Recoverpublicvoidrecover(Exceptione,ProductImageDeleteDtodeletDto){log.error(All retries failed for deleting file: {},deletDto.getFilePath(),e);// 发送邮件通知人工处理emailService.sendAlert(文件删除失败请人工介入: deletDto.getFilePath());}这个 Recover 和 Retryable 都是 Spring Retry 框架提供的因为用起来比较方便嘛就使用了这个。之所以不用别的消息重试机制是因为太麻烦了 RabbitMQ 的异常重试处理机制一般都是使用 死信队列 或者手动 ACK / NACK 重试不管哪种方式都过于复杂在这里完全没必要用。延时通知业务回答面试官就是一个 定时任务用于对用户账户临近到期的情况进行提醒。任务每天中午 12 点执行一次查询未来 1 到 5 天即将到期的账户通过当前时间与数据库中的 endDate 字段进行比较并通过短信通知用户账户剩余天数。对于每个查询到的用户会调用短信工具发送通知同时记录发送成功或失败的日志以保证用户及时收到账户到期提醒。定时任务要先在 SpringBootApplication 主程序类上加个注解EnableScheduling。然后在 定时任务方法上加个 Scheduled(cron “0 0 12 * * ?”) 定时启动。这个定时任务类要用 Component 注解标注嗷标识其为一个 Spring 容器否则 Spring 容器不会管这个类。下面是理解用的。业务场景这个业务场景是当用户账号的使用期限还剩最后比如三天的时候,给用户发送短信通知,但是用户账户的使用期限一般也很长,比如一年的样子,那有没有什么效率高性能好的方式来实现呢?现在是用户还有五天时间到期的话,就每天都发送一条信息提醒用户还有多少天过期,比如还有五天的时候通知还有五天时间就过期,还有四天的时候就提示还有四天就过期.又申请了一个短信通知的模板:解决方案这是一个典型的延迟通知业务场景。对于长周期如一年的到期提醒确实需要高效的解决方案。让我为你分析几种方案定时任务数据库索引推荐Redis延迟队列方案消息队列方案高并发场景对于这个场景推荐定时任务方案.业务实现packageorg.example.imageappserver.ScheduledTask;importlombok.extern.slf4j.Slf4j;importorg.example.imageappserver.model.Users;importorg.example.imageappserver.service.userService.UsersService;importorg.example.imageappserver.utils.UcloudMsgUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.scheduling.annotation.Scheduled;importorg.springframework.stereotype.Component;importjava.time.LocalDate;importjava.time.ZoneId;importjava.util.Date;importjava.util.List;/** * 用户账户临近到期使用短信通知用户 * 倒计时5天每天发一次 */ComponentSlf4jpublicclassAccountExpiryNotificationTask{AutowiredprivateUsersServiceusersService;// 每天中午12点执行一次Scheduled(cron0 0 12 * * ?)publicvoidcheckExpiringAccounts(){log.info(开始检查即将到期的账户);// 获取今天的日期LocalDatetodayLocalDate.now();// 分别查询1-5天后到期的账户for(intdays1;days5;days){LocalDatetargetDatetoday.plusDays(days);DateexpiryDateDate.from(targetDate.atStartOfDay(ZoneId.systemDefault()).toInstant());// 查询指定日期到期的账户ListUsersexpiringUsersusersService.findByExpiryDate(expiryDate);log.info(发现 {} 个账户将在{}天后{}到期,expiringUsers.size(),days,targetDate);// 批量发送通知for(Usersuser:expiringUsers){try{sendExpiryNotification(user,days);}catch(Exceptione){log.error(发送到期通知失败用户ID{}剩余天数{},user.getId(),days,e);}}}log.info(账户到期检查完成);}publicvoidsendExpiryNotification(Usersuser,intremainingDays){// 计算剩余时间单位为天StringdaysString.valueOf(remainingDays);booleansuccessUcloudMsgUtils.sendPhoneMsg(user.getUsername(),days);if(success){log.info(到期通知发送成功用户{}剩余天数{},user.getUsername(),remainingDays);}else{log.warn(到期通知发送失败用户{}剩余天数{},user.getUsername(),remainingDays);}}}上述userMapper的业务层方法如下:对应的SQL如下:注意要使用定时任务可别忘了在启动类上加一个启动定时任务的注解:业务逻辑梳理执行逻辑是:
返回列表