news 2026/11/1 19:16:19

苍穹外卖项目技术架构与面试高频问题解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
苍穹外卖项目技术架构与面试高频问题解析

1. 项目架构全景:从单体到微服务的演进之路

很多同学在面试中被问到“苍穹外卖”这类项目时,第一反应就是背八股文,说用了什么技术栈。但面试官真正想听的,是你对这个项目架构演进的思考。我当年做这个项目时,一开始也不是上来就搞一堆复杂中间件,而是从最朴素的单体架构开始的。

最开始,项目就是个简单的Spring Boot应用,所有模块——用户、菜品、订单、员工管理——都打包在一个JAR包里,扔到内嵌的Tomcat里就跑起来了。前端呢,就是一些静态页面,直接放在Nginx里。这个阶段的核心矛盾是“快速上线验证业务”,所以技术选型怎么简单怎么来。但很快,用户量上来,第一个瓶颈就出现了:所有请求都打到同一个Tomcat上,高峰期直接卡死。

这时候,架构的第一次演进就发生了:前后端分离与反向代理。我们把前端静态资源(HTML、CSS、JS)彻底从后端工程里剥离出来,独立部署在Nginx服务器上。后端API则单独部署。你可能会问,用户访问前端页面,页面里的JS再去请求后端API,这不就跨域了吗?没错,所以Nginx在这里扮演了关键角色——反向代理。我们在Nginx里配置一条规则,把所有以/api/开头的请求,转发到后端的Tomcat服务器。对浏览器来说,它始终只和Nginx对话,根本不知道后端服务器的存在。这带来的好处太大了:前端可以独立开发部署,后端API可以灵活扩容,而且Nginx本身性能极高,能扛住大量静态资源请求。

随着业务量进一步增长,单台后端服务器又成了瓶颈。这时就引入了负载均衡。我们买了两台配置一样的服务器,都部署上后端JAR包。然后在Nginx配置里,用upstream指令定义了一个服务器组,把这两台机器的地址都列进去。Nginx默认使用轮询策略,把请求均匀分给这两台机器。这就好比一个餐厅,原来只有一个厨师,现在有了两个,接待能力直接翻倍。这里有个实战细节:为了确保会话(Session)不丢失,我们采用了Redis来集中存储Session,这样无论用户请求被分配到哪台后端服务器,都能拿到登录状态。

数据库层面,单点故障和读写压力是下一个要解决的问题。我们采用了MySQL主从复制。主库(Master)负责处理所有的写操作(增、删、改),比如用户下单、管理员修改菜品信息。从库(Slave)通过MySQL的binlog日志实时同步主库的数据,专门负责处理读操作(查),比如用户浏览菜品、查看订单历史。这样读写分离,主库的压力就小了很多。在代码里,我们用了Sharding-JDBC这个轻量级框架来透明地实现读写分离。配置好主、从数据源,写操作会自动路由到主库,读操作会自动路由到从库,对业务代码几乎是零侵入。这个方案上线后,在促销期间数据库的CPU负载下降了近40%。

最后,为了应对高并发查询,我们引入了Redis缓存集群。像菜品分类、店铺营业状态这种变化不频繁但访问量巨大的数据,非常适合放进缓存。我们采用了一主两从三哨兵的模式。主节点负责写,从节点负责读,哨兵节点负责监控主节点健康状态,一旦主节点宕机,能自动选举新的主节点,实现高可用。我印象很深的是,第一次把菜品列表接口加上缓存后,平均响应时间从200毫秒直接降到了20毫秒以内,效果立竿见影。

所以你看,一个项目的架构不是一蹴而就的,而是随着业务发展,一步步“生长”出来的。从单体到分离,从单机到集群,每一步都是为了解决当前阶段最突出的矛盾。在面试中,你能把这个演进过程和自己做的技术决策讲清楚,远比罗列一堆技术名词要加分得多。

2. 核心模块深度拆解与高频面试题

2.1 Nginx:不只是反向代理,更是性能守卫

在苍穹外卖里,Nginx是我们的第一道大门。很多同学只知道配个proxy_pass就完事了,但里面的门道可不少。首先,反向代理的配置,我们通常不会直接写IP,而是用upstream定义后端服务器组,这样管理和扩容都方便。

upstream backend_servers { server 192.168.1.101:8080 weight=2; # weight表示权重,这台机器处理2份请求 server 192.168.1.102:8080 weight=1; # 这台处理1份,性能好的机器可以多分担 keepalive 32; # 保持长连接数量,减少TCP握手开销 } server { listen 80; server_name sky-takeout.com; location /api/ { proxy_pass http://backend_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 把用户真实IP传给后端,很重要! proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 3s; # 连接后端超时时间 proxy_read_timeout 10s; # 读取后端响应超时时间 } # 静态资源服务,开启gzip压缩和缓存 location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { root /usr/share/nginx/html; expires 7d; # 客户端缓存7天 gzip on; gzip_types text/css application/javascript; } }

面试高频问题1:Nginx负载均衡策略有哪些?你们用的哪种?为什么?

除了默认的轮询(round-robin),常用的还有:

  • weight(权重):给性能好的服务器更高权重,让它处理更多请求。我们给新采购的高配服务器权重设为3,老服务器设为1。
  • ip_hash:根据客户端IP计算哈希,固定分配到某台后端。这能解决Session问题,但我们用了Redis存Session,所以没选这个。
  • least_conn:把新请求发给当前连接数最少的服务器。这在后端服务器处理能力不均时比较有用。
  • fair(第三方):按后端服务器的响应时间分配,响应快的多接活。

我们主要用加权轮询。原因很简单:我们的后端服务器配置不完全一样,新机器性能强,理应多承担流量。ip_hash虽然能保持会话,但可能导致负载不均,而且如果某台服务器宕机,上面绑定会话的用户就会受影响。least_conn和fair更智能,但需要额外的模块支持,我们当时追求的是稳定和简单可控。

面试高频问题2:正向代理和反向代理的区别?

这个问题几乎必问。你可以这样通俗地解释:

  • 正向代理:替客户端办事。比如你在公司,网络不能直接访问某网站,你通过公司的一台代理服务器去访问。对于目标网站来说,请求来自代理服务器,它不知道你的存在。代理对象是客户端。
  • 反向代理:替服务器办事。比如用户访问sky-takeout.com,实际上请求被Nginx接收,Nginx再悄悄转发给后端的Tomcat服务器。对于用户来说,他以为Nginx就是最终服务器。代理对象是服务端。

我们项目中用的Nginx就是典型的反向代理,它隐藏了后端服务器的真实IP,提供了负载均衡和安全屏障。

2.2 Redis:缓存设计与穿透、雪崩、击穿应对实战

用Redis做缓存,绝不是set、get那么简单。在苍穹外卖里,我们主要缓存了三类数据:

  1. 全局性低频变更数据:菜品分类、店铺营业状态。这类数据一旦放入,除非管理员后台修改,否则不会变,缓存时间可以设得很长(比如12小时)。
  2. 热点数据:今日推荐菜品、销量排行榜。这类数据访问频率极高,我们设置一个较短的过期时间(如5分钟),并配合定时任务定期更新。
  3. 会话数据(Session):用户登录状态。我们采用Spring Session集成Redis,将Tomcat的HttpSession外置,实现了分布式集群下的会话共享。

缓存策略我们采用“旁路缓存”模式:先查缓存,命中则返回;未命中则查数据库,写入缓存后再返回。这是最常用的模式。

面试高频问题3:如何解决缓存穿透、雪崩、击穿?

这是Redis三座大山,必须讲清楚。

  • 缓存穿透:请求一个数据库中根本不存在的数据(比如id=-1的商品),缓存没有,每次都会打到DB,可能压垮数据库。

    • 我们的解决方案:布隆过滤器(Bloom Filter)。在查询缓存前,先用布隆过滤器判断这个key是否存在。布隆过滤器说“不存在”,那一定不存在,直接返回空。布隆过滤器说“存在”,则可能存在(有极小的误判率),再去查缓存/DB。我们用了Guava库提供的布隆过滤器,在系统启动时预热所有有效的菜品ID、用户ID等。
    • 补充方案:对于查询结果为空的key,也缓存一个空值(如set key-null ""),并设置一个很短的过期时间(30秒),下次同样的请求直接返回空,避免穿透。
  • 缓存雪崩:同一时间大量缓存同时过期,所有请求瞬间涌向数据库。

    • 我们的解决方案:给缓存过期时间加上随机值。比如原本缓存1小时,现在改成1小时 + 随机(0-300秒)。这样就能避免大量key在同一时刻失效。
    • 补充方案:热点数据永不过期,通过后台定时任务异步更新。或者采用Redis集群,将数据分散到不同节点,降低单个节点宕机的影响。
  • 缓存击穿:某个热点key过期瞬间,大量并发请求同时来查这个key,全部打到DB。

    • 我们的解决方案:互斥锁(Mutex Lock)。当第一个发现缓存过期的请求到达时,它去获取一个分布式锁(我们用Redis的SETNX命令实现),然后去数据库加载数据并回设缓存。其他并发请求在此期间要么等待锁释放,要么直接返回旧的缓存数据(如果业务允许)。等第一个请求完成后,释放锁,后续请求就能从缓存中获取到新数据了。
    // 伪代码示例:使用Redis分布式锁解决缓存击穿 public Product getProductById(Long id) { String cacheKey = "product:" + id; String productJson = redisTemplate.opsForValue().get(cacheKey); if (productJson != null) { return parseProduct(productJson); } // 缓存未命中,尝试获取锁 String lockKey = "lock:product:" + id; boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (locked) { try { // 双重检查,防止其他线程已经更新了缓存 productJson = redisTemplate.opsForValue().get(cacheKey); if (productJson != null) { return parseProduct(productJson); } // 查数据库 Product product = productMapper.selectById(id); if (product != null) { redisTemplate.opsForValue().set(cacheKey, toJson(product), 30, TimeUnit.MINUTES); } else { // 防止穿透,缓存空值 redisTemplate.opsForValue().set(cacheKey, "", 1, TimeUnit.MINUTES); } return product; } finally { // 释放锁 redisTemplate.delete(lockKey); } } else { // 未获取到锁,等待片刻后重试或返回降级数据 Thread.sleep(50); return getProductById(id); // 简单重试,生产环境需控制次数 } }

面试高频问题4:Redis为什么快?

  1. 纯内存操作:数据都在内存里,读写速度是纳秒级,磁盘是毫秒级,差了好几个数量级。
  2. 单线程模型(核心网络请求处理):避免了多线程上下文切换和竞争的开销。虽然6.0之后引入了多线程处理网络I/O,但核心的键值对读写命令处理仍是单线程,保证了原子性。
  3. 高效的数据结构:Redis自己实现了一套精炼的数据结构,比如跳跃表实现有序集合,压缩列表实现小数据量的哈希和列表,非常节省内存且高效。
  4. I/O多路复用:使用epoll这样的机制,一个线程可以管理成千上万个连接,避免了为每个连接创建线程的开销。

2.3 MySQL与Sharding-JDBC:读写分离下的数据一致性

我们项目里,订单表增长非常快,单表很快就要破千万。单纯的主从读写分离不够,我们还考虑了分库分表,但鉴于项目规模和团队经验,初期只实现了读写分离。这里重点讲一下Sharding-JDBC的配置和坑。

首先,在application.yml中配置主从数据源:

spring: shardingsphere: datasource: names: master,slave0,slave1 # 定义数据源名称 master: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://master-host:3306/sky_takeout?useUnicode=true username: root password: master-password slave0: ... # 配置从库1 slave1: ... # 配置从库2 masterslave: load-balance-algorithm-type: round_robin # 从库负载均衡策略:轮询 name: ms # 主从逻辑数据源名称 master-data-source-name: master slave-data-source-names: slave0,slave1 props: sql: show: true # 显示SQL日志,调试用

面试高频问题5:主从同步有延迟怎么办?写后立刻读,可能读不到最新数据。

这是我们遇到的一个典型问题。用户下单成功后,立刻跳转到订单详情页,如果这个查询请求被路由到了从库,而主从同步有几百毫秒的延迟,用户就会看到“订单不存在”,体验极差。

我们的解决方案:

  1. 强制走主库:对于这类“写后立即读”的关键业务,我们使用Sharding-JDBC的HintManager强制将查询路由到主库。
    @GetMapping("/order/{orderId}") public OrderDetail getOrderDetail(@PathVariable String orderId) { // 尝试从缓存读 OrderDetail detail = redisTemplate.opsForValue().get("order:" + orderId); if (detail != null) { return detail; } // 缓存没有,且是刚创建的订单,强制走主库查询 try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); // 设置强制路由到主库 detail = orderMapper.selectById(orderId); if (detail != null) { redisTemplate.opsForValue().set("order:" + orderId, detail, 5, TimeUnit.MINUTES); } return detail; } }
  2. 关键数据缓存:订单创建成功后,除了落库,也立刻写入Redis缓存一份,设置一个较短的过期时间(比如5分钟)。后续的读请求优先走缓存,避免直接查从库。缓存和数据库的最终一致性,通过监听数据库binlog的 Canal 或者 Debezium 来保证,但这个方案我们当时没上,因为复杂度较高。

面试高频问题6:如何保证缓存与数据库的双写一致性?这是一个经典难题。我们根据数据一致性要求的高低,采用了不同策略:

  • 最终一致性(容忍短暂不一致):对于菜品分类、用户头像这类信息,采用“先更新数据库,再删除缓存”的策略。为什么是删缓存而不是更新缓存?因为更新缓存可能涉及复杂计算,且并发写缓存可能引发数据错乱。删除缓存后,下次查询自然会从数据库加载最新值到缓存。虽然存在一个“删除缓存后、下次查询前”的短暂不一致窗口,但业务可以接受。
  • 强一致性(要求极高):对于库存、优惠券数量这类资金敏感数据,我们采用了“先更新数据库,再更新缓存”,并且用分布式锁保证这两个操作的原子性。确保在更新过程中,不会有其他请求读到脏数据。当然,这会牺牲一些性能。

2.4 布隆过滤器:自己实现 vs. 第三方库

在解决缓存穿透时,我们提到了布隆过滤器。面试官很喜欢追问细节。

面试高频问题7:布隆过滤器的原理是什么?误判率怎么控制?

布隆过滤器本质上是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数)。它的工作流程是:

  1. 添加元素:将元素通过K个哈希函数计算得到K个哈希值,将位数组中对应的K个位置设为1。
  2. 检查元素:同样用这K个哈希函数计算元素的K个哈希值,检查位数组中对应的K个位置是否都为1。如果全是1,则可能存在(有误判);如果有一个为0,则一定不存在。

误判率取决于位数组大小m、哈希函数个数k和已插入元素数量n。公式很复杂,但结论很简单:位数组越大,误判率越低;哈希函数个数需要取一个最优值。我们使用Guava库时,可以直接指定预期的元素数量n和可接受的误判率fpp(false positive probability),库会自动计算所需的m和k。

// 使用Guava布隆过滤器 BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.defaultCharset()), 1000000, // 预期插入100万个元素 0.01 // 可接受的误判率:1% ); // 预热数据 for (String validId : allValidIds) { bloomFilter.put(validId); } // 查询前判断 if (!bloomFilter.mightContain(queryId)) { return null; // 一定不存在,直接返回 } // 可能存在,继续后续查询

面试高频问题8:为什么不用Redis自带的BitMap自己实现?我们评估过。自己实现需要:

  1. 确定m和k。
  2. 实现多个无碰撞的哈希函数。
  3. 处理布隆过滤器的扩容问题(布隆过滤器一旦创建,扩容很麻烦)。 使用Guava库,几行代码就搞定了,而且经过充分测试,性能稳定。在分布式环境下,如果需要共享布隆过滤器,我们会把Guava构建好的位数组序列化后存入Redis BitMap,兼顾了开发效率和分布式需求。自己造轮子的性价比不高,除非有极其特殊的定制需求。

3. 业务场景下的技术实战与设计模式

3.1 订单超卖与分布式锁

外卖业务最核心、最怕出问题的就是订单和库存。超卖问题,我估计每个电商项目都会遇到。我们的菜品库存是存在数据库里的,在“秒杀”或“爆款促销”时,并发扣减库存就是个挑战。

最初 naive 的方案:

UPDATE dish SET stock = stock - 1 WHERE id = #{dishId} AND stock > 0;

这条SQL在低并发下没问题,但它不是原子的“查询+更新”吗?在高并发下,多个线程可能同时读到stock=1,然后都执行了stock-1,结果库存变成了 -1,超卖了。

解决方案一:数据库悲观锁

BEGIN; SELECT * FROM dish WHERE id = #{dishId} FOR UPDATE; -- 加行锁 -- 在代码中判断 stock > 0 UPDATE dish SET stock = stock - 1 WHERE id = #{dishId}; COMMIT;

用SELECT ... FOR UPDATE锁住这行数据,别的线程必须等待。这确实能解决超卖,但性能太差,FOR UPDATE会让并发请求串行化,不适合高并发场景。

我们最终采用的方案:Redis分布式锁 + Lua脚本为什么是Redis?因为它的单线程模型和原子操作特性,非常适合做分布式锁。

  1. 抢锁:用SET lock_key unique_value NX PX 30000命令。NX表示只有key不存在时才设置,PX设置过期时间(防止死锁)。unique_value一般用UUID,确保是当前线程的锁。
  2. 扣减库存:获取锁后,我们在Redis中预扣库存。我们把库存也缓存一份在Redis里(比如dish_stock:1001),用DECR命令原子扣减。如果扣减后库存>=0,说明成功,然后异步去更新数据库库存。如果库存不足,直接返回失败。
  3. 关键点:整个“判断库存-扣减”的操作,必须用Lua脚本保证原子性,避免多个命令执行间隙被其他请求打断。
    -- 扣减库存Lua脚本 local key = KEYS[1] -- 库存key,如 'dish_stock:1001' local change = tonumber(ARGV[1]) -- 扣减数量,如 -1 local current = redis.call('GET', key) if (not current) then return -1 -- 缓存中无库存记录,需回源查数据库 end if (tonumber(current) + change >= 0) then redis.call('INCRBY', key, change) return tonumber(current) + change -- 返回扣减后的库存 else return -2 -- 库存不足 end
  4. 释放锁:用Lua脚本判断unique_value是否还是自己的,是则删除。防止误删其他线程的锁。
    -- 释放锁Lua脚本 if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

这个方案将库存扣减的压力从数据库转移到了Redis,利用Redis的高并发能力扛住了流量峰值。数据库库存只作为最终持久化的兜底,通过定时任务或监听binlog来同步Redis的扣减结果,实现最终一致。

3.2 ThreadLocal:在权限与事务上下文中的妙用

ThreadLocal是Java中一个看似简单但极易用错的工具。在苍穹外卖里,我们主要在两个场景用它:

场景一:用户权限上下文传递用户登录后,我们会把他的ID、角色等信息生成一个JWT Token返回给前端。后续请求中,前端在Header里带上Token。在拦截器里,我们解析Token,将用户信息(如userId)存入ThreadLocal。

public class UserContext { private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>(); public static void setUserId(Long userId) { USER_ID_HOLDER.set(userId); } public static Long getUserId() { return USER_ID_HOLDER.get(); } public static void clear() { USER_ID_HOLDER.remove(); // 关键!必须清理 } } // 在拦截器或过滤器中 @Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); Claims claims = JwtUtils.parseToken(token); // 解析JWT UserContext.setUserId(claims.get("userId", Long.class)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后,务必清除ThreadLocal,防止内存泄漏! UserContext.clear(); } }

这样,在Service层、Mapper层的任何地方,都可以通过UserContext.getUserId()无侵入地获取当前用户ID,用于自动填充create_user,update_user等字段,或者做权限校验。

场景二:管理数据库事务上下文在一些复杂业务中,我们可能需要手动控制事务边界。虽然Spring的@Transactional注解很好用,但在某些需要根据业务逻辑动态开启/提交/回滚事务的场景,我们会用TransactionTemplate,并结合ThreadLocal来标记一个“事务上下文”,确保在同一个线程内,多个数据库操作共享同一个连接和事务。

面试高频问题9:ThreadLocal为什么会内存泄漏?如何避免?这是ThreadLocal的经典坑。每个Thread内部都有一个ThreadLocalMap,它的key是弱引用的ThreadLocal实例,value是强引用的值。当ThreadLocal实例被回收(比如置为null)后,由于key是弱引用,会被GC回收,但value的强引用还在,如果线程一直不结束(比如线程池中的线程),这个value就永远无法被回收,造成内存泄漏。避免方法:

  1. 用完一定要remove()!就像我们上面在afterCompletion里做的那样。
  2. 尽量将ThreadLocal变量声明为private static,这样它的生命周期和ClassLoader绑定,通常比线程更长,避免了因ThreadLocal实例被回收而key为null的情况。
  3. 如果使用线程池,线程是会复用的,所以每次任务执行完毕,都必须清理ThreadLocal。

3.3 浮点数精度与金额计算

金融相关的计算,比如订单金额、优惠券抵扣,最忌讳用float或double。我踩过一个坑:早期用double存储订单总价,计算时出现了59.90000000000001这样的结果,虽然差得不多,但在对账时就是灾难。

解决方案:使用BigDecimal,并且注意两点:

  1. 构造时一定要用字符串,不要用double直接构造。
    // 错误!精度已经丢失 BigDecimal a = new BigDecimal(0.1); // 正确 BigDecimal b = new BigDecimal("0.1");
  2. 进行除法运算时,必须指定精度和舍入模式,否则遇到除不尽的情况会抛ArithmeticException。
    // 计算平均价格 BigDecimal total = new BigDecimal("100"); BigDecimal count = new BigDecimal("3"); // 指定保留2位小数,采用四舍五入(ROUND_HALF_UP) BigDecimal avg = total.divide(count, 2, RoundingMode.HALF_UP);

我们在项目中,所有涉及金额的字段,在数据库中用DECIMAL(10,2)类型(10位精度,2位小数),在Java实体类中用BigDecimal类型,在传输时(如JSON)也序列化为字符串,彻底杜绝精度问题。

4. 面试复盘:如何清晰表达你的项目经验

最后,结合我自己的面试和被面试经验,聊聊怎么在面试中聊好“苍穹外卖”这类项目。面试官不想听流水账,他想听的是你遇到什么问题,你是怎么思考的,你对比了哪些方案,为什么选这个,结果怎么样。

STAR法则永远是好用的框架:

  • S(情境):我们项目初期用户量暴增,高峰期订单接口响应慢,数据库CPU飙升到90%。
  • T(任务):我的任务是优化订单查询性能,保证在秒级高峰下系统稳定。
  • A(行动):我首先用Arthas做了线上诊断,发现慢SQL集中在订单历史查询。然后我做了三件事:第一,针对用户订单列表,引入了Redis缓存,用用户ID+分页参数作为key,缓存5分钟;第二,对订单表创建了合理的复合索引(user_id, create_time);第三,和DBA一起,将订单表的历史数据归档到历史库,减少主表数据量。
  • R(结果):优化后,订单查询接口的平均响应时间从800ms降到了120ms,数据库CPU峰值下降了60%,平稳度过了后续的促销活动。

对于技术细节,要能深挖: 当你说“用了Redis缓存”,面试官可能会问:

  • 缓存键怎么设计的?(业务前缀:唯一标识,如order:1001)
  • 过期时间设多少?为什么?(根据业务变更频率,分类数据12小时,热点数据5分钟)
  • 缓存和数据库一致性怎么保证?(采用先更新数据库,再删除缓存的最终一致策略,对库存等强一致数据用分布式锁)
  • 缓存穿透怎么办?(布隆过滤器+空值缓存)
  • Redis集群方案?(一主两从三哨兵,讲了哨兵选举过程)

展现你的技术视野和成长: 不要只说你做了什么,还要说你学到了什么,如果重来你会怎么做。比如:“当时为了快速上线,缓存策略写得比较粗糙,导致一些缓存键设计不合理,后来引发了内存问题。如果重来,我会在项目初期就引入Redis Key的前缀规范和内存监控告警。另外,我现在觉得,对于订单这种强状态流转的业务,用CQRS(命令查询职责分离)架构可能更合适,把读模型和写模型彻底分开,读模型可以用Elasticsearch来承载复杂的查询,这样架构会更清晰。”

记住,面试是双向的交流。你是在通过一个项目,向面试官展示你的技术能力、解决问题的思路和潜力。把项目吃透,把背后的原理想明白,你就能自信地应对任何追问。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/1 19:15:35

360加固APP逆向破解避坑指南:从脱壳到重新签名的完整解决方案

360加固应用逆向实战&#xff1a;从脱壳到重签的深度排雷手册 如果你在逆向分析的道路上摸索过一阵子&#xff0c;大概率已经和各类应用加固方案打过照面。其中&#xff0c;360加固以其相对复杂的保护机制&#xff0c;常常成为逆向爱好者进阶路上的一道坎。网上能找到的教程不少…

作者头像 李华
网站建设 2026/11/1 19:12:50

如何用开源工具打造专业声音作品?OpenUtau全流程指南

如何用开源工具打造专业声音作品&#xff1f;OpenUtau全流程指南 【免费下载链接】OpenUtau Open singing synthesis platform / Open source UTAU successor 项目地址: https://gitcode.com/gh_mirrors/op/OpenUtau 在数字音乐创作领域&#xff0c;专业声音合成工具往往…

作者头像 李华
网站建设 2026/10/22 4:48:16

HTML到DOCX的无缝转换:开源工具html-to-docx全攻略

HTML到DOCX的无缝转换&#xff1a;开源工具html-to-docx全攻略 【免费下载链接】html-to-docx HTML to DOCX converter 项目地址: https://gitcode.com/gh_mirrors/ht/html-to-docx 1. 文档转换的困境与破局&#xff1a;为什么选择html-to-docx&#xff1f; 在数字化办…

作者头像 李华
网站建设 2026/10/22 4:56:29

3步解锁PDF表格数据:给数据工作者的Python PDF数据处理效率工具

3步解锁PDF表格数据&#xff1a;给数据工作者的Python PDF数据处理效率工具 【免费下载链接】tabula-py Simple wrapper of tabula-java: extract table from PDF into pandas DataFrame 项目地址: https://gitcode.com/gh_mirrors/ta/tabula-py 在数据驱动决策的时代&a…

作者头像 李华
网站建设 2026/10/22 4:37:40

HTML转Word全攻略:从业务痛点到零代码解决方案

HTML转Word全攻略&#xff1a;从业务痛点到零代码解决方案 【免费下载链接】html-to-docx HTML to DOCX converter 项目地址: https://gitcode.com/gh_mirrors/ht/html-to-docx 一、核心价值&#xff1a;为什么HTML转Word如此重要&#xff1f; &#x1f50d; 真实业务场…

作者头像 李华
网站建设 2026/10/22 4:47:32

企业级springbo在线考试系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

摘要 随着信息技术的快速发展&#xff0c;在线考试系统逐渐成为教育领域的重要组成部分。传统的考试方式存在效率低、管理成本高、安全性不足等问题&#xff0c;亟需一种高效、便捷且安全的解决方案。企业级在线考试系统通过数字化手段优化考试流程&#xff0c;提升管理效率&am…

作者头像 李华