网站首页 > 技术文章 正文
你好,我是对线面试官。
搞技术的,谁没在 Code Review 里揪出过几个问题?
但前几天,我们团队碰到一段代码,讨论结果有点反直觉:代码有问题,但一致同意,不改了。
是不是听着像和稀泥?别急,这背后其实是每个工程师都可能遇到的现实权衡。
先看“案发现场”
简化一下,问题代码大概长这样,场景是更新用户资料:
// 根据用户ID查出完整的用户资料对象
UserProfile profile = userProfileMapper.selectById(userId);
// 用户想修改昵称和邮箱
profile.setNickname(newNickname);
profile.setEmail(newEmail);
// 用包含了所有字段的 profile 对象更新数据库记录
userProfileMapper.updateById(profile);
老兵一眼就能看出问题:并发!
想象一下:用户张三读取了他的资料 (nickname=老王, email=old@example.com, points=100)。他正准备把昵称改成“潇洒哥”,邮箱改成“new@example.com”。
就在他提交前的一瞬间,另一个系统操作(比如登录奖励)把他的 points 更新为了 150。
然后张三提交了。上面那个 updateById(profile) 执行时,他手里 profile 对象的 points 字段还是旧的 100。数据库执行 UPDATE ... SET nickname='潇洒哥', email='new@example.com', points=100 WHERE userId=...,得,刚加上的 50 积分,瞬间就被抹掉了!
典型的“丢失更新” (Lost Update)。
教科书式的“正确”做法
稍微有点追求的同学会说,这还不简单?
方案一:按需更新(或者叫部分更新)。
// 创建一个只包含需要更新字段的对象或 Map
UserProfile profileToUpdate = new UserProfile();
profileToUpdate.setUserId(userId);
profileToUpdate.setNickname(newNickname);
profileToUpdate.setEmail(newEmail);
// 调用一个能智能处理部分更新的方法 (MyBatisPlus 的 updateById 默认可能就是这样,但要明确)
// 或者更推荐使用 updateSelective 或自定义只更新特定字段的 SQL
userProfileMapper.updateById(profileToUpdate); // 确保这个方法只更新非 null 字段,或使用 updateSelective
方案二:上锁!
可以用乐观锁(加个 version 字段或用 update_time 判断),更新时 WHERE userId = ? AND version = ?,更新失败就提示用户:“资料已被修改,请刷新重试”。
或者更直接点,上数据库悲观锁 (SELECT ... FOR UPDATE),操作前先锁住这行记录,一个处理完另一个才能动。
你看,解决方案明明白白。那我们为啥“头铁”不改呢?
不改的底气:情境 > 纯粹
那位写代码的哥们解释:这段逻辑用在一个内部管理后台,修改的是某种非关键的附属信息。
这几个字是关键。分析一下这个场景的特点:
- 操作者稀少且固定:通常就是几个内部运营人员。
- 操作频率极低:可能一天都未必有一次操作。
- 并发冲突概率极小:两个人同时改 同一个用户 的 不同附属信息 的可能性,小到可以忽略不计。
开发同学是知道这个并发风险的,但他判断,在这个特定场景下,风险无限趋近于零。
代码能工作,改动(即使是优化成按需更新)需要时间测试,而带来的实际收益(规避几乎不可能发生的风险)微乎其微。
所以,我们团队迅速达成共识:理论上有风险,实践中极难触发,开发知情风险,那就过。
再想想:这真的万无一失吗?
有人可能会抬杠:就算用了按需更新,也还有问题啊!
比如:运营A打开用户张三的资料页面,看到昵称=老王, 邮箱=old@example.com。他想把邮箱改成 new@example.com,这时去接了个水。在他接水期间,运营B也打开了张三的页面,迅速把昵称改成了“潇洒哥”,提交了。现在数据库里是 nickname=潇洒哥, email=old@example.com。
运营A接水回来,在他那个“旧”页面上点了提交(页面上的昵称还是“老王”)。他的请求执行了按需更新,只更新邮箱字段,数据库最终变成 nickname=潇洒哥, email=new@example.com。看起来没问题?
但如果运营A的操作是全量提交表单(前端把所有字段值都传回来),而后端用了类似 updateById(receivedProfile) 的逻辑,那运营B的昵称修改还是会被覆盖!这种情况怎么办?还是看场景。
- 有些场景,无所谓。 “谁最后提交听谁的”也能接受。
- 有些场景,绝对不行。 那就必须上锁(乐观锁提醒用户刷新,或悲观锁直接阻止)。
在我们那个内部后台、非关键信息的场景下,连这种“接水”式冲突的概率和影响都可以接受。
划重点:何时必须较真?
这种“差不多得了”的心态,绝不能滥用。
当我们 Review 的代码涉及核心业务流程,特别是钱、订单、账户余额、库存数量这类东西时,态度必须 180 度大转弯:有问题,必须改,零容忍!
对于这类高度敏感的核心逻辑,我会切换到“防御模式”来审视代码。我会关注几个关键点:
- 操作是否被有效隔离? 比如,有没有使用恰当的锁机制(数据库锁、分布式锁等)来确保同一时间只有一个请求在处理关键资源,防止并发冲突。
- 执行前是否有充分校验? 在真正修改数据前,有没有再次检查当前状态是否仍然符合操作的前提条件?有没有做好幂等性防护,防止同一操作被意外重复执行?
- 核心更新是否安全执行? 确保数据的修改是在前面这些保护措施都到位的情况下进行的。
- 资源是否总能正确释放? 无论操作成功还是失败(比如发生异常),之前获取的锁或其他独占资源,有没有确保一定会被释放掉?
缺少这些严密的“防御工事”,就等于在线上核心地带埋下了随时可能引爆的地雷。这种场景下,牺牲点性能或者用户体验,换取数据的绝对准确和一致性,是完全必要的。
工程师的成熟:与“不完美”和解
写代码久了,你会发现,“完美”是个理想化的目标,有时甚至不可及。修复一个低概率、低影响的 Bug,其成本(开发、测试、回归验证、上线风险)可能远超它潜在的破坏力。
Code Review 不仅仅是找语法错误或逻辑漏洞,更是基于风险评估、成本效益分析、业务场景理解的综合决策过程。知道何时可以接受“够用就好”,何时必须追求“万无一失”,是一种在实践中磨练出的务实智慧。
下次你在 CR 时发现一段“有味道”的代码,不妨先深入思考:
- 它运行在哪个具体场景?
- 出问题的概率有多大?一旦出问题,后果严重吗?
- 修复它需要多少投入?这点投入是否值得?
也许,你也会在某个时刻,面对一段有瑕疵的代码,做出那个决定:它确实有问题,但是……这次可以不用改。
你遇到过类似的情况吗?在评论区分享你的故事和看法吧。
**#编程 #CodeReview #软件开发 #并发控制 #技术决策 #工程师 #投入产出比 #务实 #用户资料更新
猜你喜欢
- 2025-06-13 Mybatis一级缓存和二级缓存原理区别(图文详解)
- 2025-06-13 MyBatis的10种用法(mybatis语句)
- 2025-06-13 MyBatis 插件原理与实战(mybatis插件执行顺序)
- 2025-06-13 MyBatis-Flex一个优雅的 MyBatis增强框架
- 2025-06-13 MyBatis动态SQL的5种高级玩法,90%的人只用过3种
- 2025-06-13 MyBatis-Plus码之重器 lambda 表达式使用指南,开发效率瞬间提升80%
- 2025-06-13 如何一眼定位SQL的代码来源:一款SQL染色标记的简易MyBatis插件
- 2025-06-13 MyBatisPlus又在搞事了!一个依赖轻松搞定权限问题!堪称神器
- 2025-06-13 Java精进-20分钟学会mybatis使用(mybatis怎么用)
- 2025-06-13 MyBatis(mybatis和MySQL的区别)
- 最近发表
- 标签列表
-
- cmd/c (64)
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- sqlset (64)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)