ARTICLE DETAIL

资讯详情

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

3个招行app开发高频面试题,教你从零搭建高并发项目

3个招行app开发高频面试题,教你从零搭建高并发项目 3个招行app开发高频面试题,教你从零搭建高并发项目 很多后端新人卡在“会写语法,不会搭项目”的深坑里。你背熟了Java集合类,也刷过LeetCode,但面试官一问到招行app这类金融级应用的并发处理、数据一致性,瞬间就卡壳。这不仅仅是语法问题,更是工程化思维的缺失。在掘金技术社区的众多实战分享中,我们发现,真正能落地的项目,往往不是代码量最大,而是对细节把控最严的。今天我们就拆解3个高频面试题,通过模拟一个简化的招行app核心交易模块,带你从零搭建一个具备生产级思维的项目。 项目目标与核心痛点拆解 我们要做的不是一个完整的银行App,而是一个能跑通“查询余额-发起转账-处理结果”的核心交易服务。这个场景覆盖了招行app最典型的业务流,也恰好是面试中关于“高并发”、“幂等性”、“分布式锁”的重灾区。 为什么选这个场景?因为它简单,但坑极多。并发竞争:同一账户,两个请求同时扣款,余额会不会变负? 网络抖动:请求发出去了,没收到响应,重试会不会导致重复扣款? 数据一致性:A转给B,A扣了,B没加,中间崩了怎么办?很多初级开发习惯用try-catch吞掉异常,或者直接用数据库默认隔离级别就上线。在金融场景,这等于自杀。我们的目标,是用最小的代码量,解决这三个核心问题,让你在面对高频面试题时,能拿出真实的代码逻辑去讲,而不是背八股文。 目录结构设计原则 工程化思维的第一步,是目录结构。不要把所有类扔在一个包里。我们采用经典的分层架构,但针对高并发场景做了微调。 com.bank.core ├── controller # 接口层,只做参数校验和响应封装 ├── service # 业务层,核心逻辑,事务边界在这里 │ ├── impl │ └── exception # 自定义业务异常 ├── dao # 数据访问层,MyBatis Mapper ├── model # 实体类、DTO、VO ├── util # 工具类,ID生成器、日志封装 └── config # 配置类,Redis配置、线程池配置关键点:service 包下的 exception 子包。很多新人把异常直接抛到最外层,导致Controller里到处是catch (Exception e)。在金融项目里,业务异常(如余额不足)和系统异常(如数据库连接超时)必须分开处理,这决定了你的补偿策略。 核心代码实现:从扣款到幂等 1. 幂等性设计:防止重复扣款 这是招行app最核心的安全机制。用户网络不好,点了一次转账,没反应,再点一次。如果服务端没做幂等,用户就少钱了。 @Service public class TransferServiceImpl implements TransferService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate RedisTemplateString, String redisTemplate;@Override@Transactional(rollbackFor = Exception.class)public Result transfer(String fromAccount, String toAccount, BigDecimal amount) {// 1. 生成全局唯一流水号,作为幂等KeyString uniqueId = UUID.randomUUID().toString().replace(-, );// 2. 设置Redis键值,TTL 10分钟,防止Key无限堆积String idempotentKey = bank:transfer:idempotent: + uniqueId;Boolean setFlag = redisTemplate.opsForValue().setIfAbsent(idempotentKey, 1, 10, TimeUnit.MINUTES);// 如果Key已存在,说明是重复请求,直接返回成功(或上次结果)if (Boolean.FALSE.equals(setFlag)) {return Result.success(Duplicate request, ignored);}try {// 3. 核心扣款逻辑doTransfer(fromAccount, toAccount, amount);return Result.success(Transfer success);} catch (Exception e) {// 4. 发生异常,删除幂等Key,允许用户重试redisTemplate.delete(idempotentKey);throw e;}} }逐行讲解:setIfAbsent:这是原子操作。如果Key不存在,设置并返回true;如果存在,返回false。这比“先查后写”安全得多,避免了并发下的竞态条件。 TTL设置:金融业务通常有对账周期,10分钟是一个经验值,既覆盖了用户重试窗口,又不会让Redis内存爆满。 异常删除Key:这点至关重要。如果扣款失败(如余额不足),必须释放幂等Key,否则用户即使修改金额也无法再次发起请求。2. 并发扣款:乐观锁实战 很多面试官喜欢问:“不用Redis分布式锁,怎么保证扣款安全?”答案是:乐观锁。 private void doTransfer(String fromAccount, String toAccount, BigDecimal amount) {// 1. 查询当前账户状态(包括版本号)Account fromAcc = accountMapper.selectByAccountId(fromAccount);Account toAcc = accountMapper.selectByAccountId(toAccount);// 2. 业务校验if (fromAcc.getBalance().compareTo(amount) 0) {throw new BusinessException(ErrorCode.BALANCE_NOT_ENOUGH, 余额不足);}// 3. 乐观锁更新:WHERE条件带版本号int rows = accountMapper.updateBalance(fromAccount, amount, fromAcc.getVersion(), System.currentTimeMillis());// 4. 如果影响行数为0,说明版本冲突,需要重试if (rows == 0) {throw new BusinessException(ErrorCode.CONCURRENT_CONFLICT, 系统繁忙,请重试);}// 5. 更新收款方(此处简化,实际应保证两个操作的原子性)accountMapper.addBalance(toAccount, amount, toAcc.getVersion()); }对应的MyBatis SQL: update id=updateBalanceUPDATE t_accountSET balance = balance - #{amount},version = version + 1,update_time = #{updateTime}WHERE account_id = #{accountId}AND version = #{version} /update避坑指南:不要信任客户端传过来的Balance:必须从数据库查出来再减。 Version字段:这是乐观锁的灵魂。每次更新,version+1。如果更新时发现DB里的version和你查出来的不一样,说明被别人抢先扣了,你这次失败,抛出异常让前端重试。 重试机制:在Controller层或拦截器中,捕获CONCURRENT_CONFLICT异常,自动重试1-2次。如果还是失败,才告诉用户“系统繁忙”。3. 分布式事务的简化实现 A扣款成功,B加款失败,怎么办?在招行app这种强一致场景,通常会用TCC或Seata。但对于中小型项目或面试回答,本地消息表是最稳妥的折中方案。 // 伪代码逻辑 1. 开启本地事务 2. 扣减A余额 (UPDATE t_account) 3. 插入转账记录 (INSERT t_transfer, status=PROCESSING) 4. 提交事务 5. 异步发送消息 (RabbitMQ/Kafka) 6. 消费端收到消息,执行B加款 7. 如果B加款失败,消费端重试,直到成功 8. 更新转账记录状态为 SUCCESS这种方案牺牲了极短时间的一致性(毫秒级),换来了极高的可用性和实现复杂度降低。在面试中,你要强调:“我们采用最终一致性,通过消息队列保证可靠性,并通过定时任务扫描PROCESSING状态的记录进行兜底补偿。” 运行与测试:像测试工程师一样思考 代码写完了,别急着mvn package。先想:怎么测?单元测试: 用Mockito Mock掉AccountMapper。测试正常转账。 测试余额不足(抛出业务异常)。 测试并发冲突(Mock updateBalance 返回0)。压力测试: 使用JMeter或Gatling,对同一个账户发起1000并发转账请求。观察指标:数据库CPU、Redis命中率、错误率。 预期结果:所有请求要么成功,要么失败并提示“余额不足”或“系统繁忙”。绝对不允许出现余额为负数的情况。混沌工程: 手动杀掉MySQL主库,观察应用是否能自动切换到从库(如果配置了)。手动断开Redis连接,观察幂等性是否失效(此时应降级为数据库唯一索引约束)。在掘金技术社区的一篇高赞文章中,作者提到:“90%的生产事故,不是代码逻辑错,而是没测过极端并发下的资源泄漏。” 你的测试用例,必须包含“网络超时”、“数据库死锁”、“Redis宕机”这三个场景。 优化扩展:从能用到高可用 当基础功能跑通后,如何体现你的架构能力?缓存预热: 热门账户(如大型商户)的余额,可以缓存在Redis中。注意:余额类数据严禁长缓存,只能做短TTL或主动失效。限流熔断: 使用Sentinel或Hystrix。限流:单个IP每秒最多发起10次转账请求,防止恶意刷单。 熔断:如果下游账户服务错误率超过50%,熔断10秒,直接返回“系统维护中”,防止雪崩。日志追踪: 接入SkyWalking或Zipkin。每个请求生成唯一的TraceId,贯穿HTTP请求、Service调用、DB操作、MQ消息。当用户投诉“钱扣了没到账”时,你能在5分钟内通过TraceId定位到是哪个环节卡住了。数据库分库分表: 当t_account表数据量超过2000万,单表查询会变慢。方案:按account_id哈希分片。 难点:跨分片的转账(A在分片1,B在分片2)。 解决:依然依赖消息队列的异步最终一致性,或者引入分布式ID生成器(Snowflake),确保全局唯一。小结与互动 搭建一个招行app的核心交易模块,本质上是在解决“并发”、“一致性”和“可靠性”这三个矛盾。你不需要一开始就搞出最复杂的架构,但要清楚每个设计决策背后的代价。幂等性是用Redis换来的,代价是Redis依赖。 乐观锁是用数据库版本字段换来的,代价是重试带来的额外QPS。 最终一致性是用消息队列换来的,代价是短暂的数据不一致窗口。面试时,不要只说“我用了Redis”,要说“我在招行app类似的场景下,权衡了实时性和可用性,选择了基于Redis的幂等控制,并通过本地消息表保证了转账的最终一致性”。 现在,回到开头的问题:面对高并发扣款,你更常用乐观锁还是分布式锁?在什么场景下你会放弃幂等性而采用其他方案?评论区交流你的实战踩坑经历。
返回列表