
1. 项目概述为什么一个简单的“for update”能撑起分布式锁的半壁江山最近在某实验室带一个模拟项目X团队里几位刚从校招进来的同学反复问一个问题“分布式锁不是该用Redis或ZooKeeper吗怎么Java后端组的架构文档里第一课讲的居然是MySQL的SELECT ... FOR UPDATE”这个问题问得特别实在——它背后藏着很多开发者踩过坑才明白的真相不是所有分布式场景都需要高并发、低延迟的强一致性锁恰恰相反大量真实业务里锁的持有时间短、竞争不激烈、数据天然落库、运维链路必须收敛这时候基于数据库的for update不是“退而求其次”而是“精准克制”的最优解。我自己在某跨平台系统里做过三次锁方案迭代第一次上Redisson结果因网络抖动导致锁续期失败订单重复扣减第二次切ZooKeeper运维成本翻倍集群扩缩容成了定时炸弹第三次回归MySQL在支付回调幂等校验、库存预占、优惠券核销三个核心路径上用for update重写线上稳定运行27个月平均加锁耗时12msP99不超过43ms故障归零。这不是玄学是把锁当“业务语义”来设计的结果——for update锁住的从来不是“资源”而是“一行正在被修改的业务状态”。它天然绑定事务、自带超时事务超时即锁释放、无需额外组件、审计日志直接落库对中小规模、强事务依赖、DB已为单点的系统而言就是最朴素也最可靠的锁原语。本文不讲Redis红锁理论不堆ZK的ZNode树形结构就聚焦一件事如何用好SELECT ... FOR UPDATE这把“老式瑞士军刀”在Java架构中稳稳切开分布式协作的硬骨头。适合正在做电商秒杀预热、金融账户流水对账、SaaS多租户配置同步的同学尤其适合DBA和后端共用一套监控告警体系的团队——因为你的锁本就该和慢SQL、死锁检测、主从延迟跑在同一套大盘里。2. 核心设计思路拆解为什么选for update而不是其他方案2.1 锁的本质不是“互斥”而是“状态变更的原子性边界”很多同学一提分布式锁脑子里立刻跳出“抢钥匙”的画面N个进程同时请求锁只有一个拿到其余排队或失败。这个模型在Redis里很直观SET key value NX PX 10000但放到数据库里就容易误读。SELECT ... FOR UPDATE真正的威力不在于它“锁住了某行”而在于它把“读取状态修改状态”这两个操作压缩进了一个不可分割的原子动作。举个库存扣减的例子-- 场景用户下单需扣减商品ID1001的库存 -- 错误示范先查再更新非原子 SELECT stock FROM t_item WHERE id 1001; -- 返回stock5 UPDATE t_item SET stock stock - 1 WHERE id 1001 AND stock 1; -- 可能失败 -- 正确示范for update保证读写原子性 SELECT stock FROM t_item WHERE id 1001 FOR UPDATE; -- 加锁并读取当前stock5 UPDATE t_item SET stock 4 WHERE id 1001; -- 直接更新无需条件判断关键差异在哪第一种方式里“查库存”和“扣库存”是两个独立事务操作中间可能有其他线程插入修改比如另一个用户同时下单导致“查到有库存”但“更新时发现已售罄”业务要兜底处理失败逻辑。而for update在SELECT那一刻就锁定了这一行后续的UPDATE必然基于刚刚读到的最新值锁的粒度不是“防止别人改”而是“确保我改的是我刚看到的那个状态”。这恰好匹配了绝大多数业务场景的核心诉求不是要绝对的毫秒级抢占而是要“状态变更不被覆盖”。这种设计哲学让for update天然规避了Redis锁的“过期时间难设定”设太短易误删设太长故障恢复慢、ZooKeeper锁的“会话超时不可控”网络分区时节点假死等经典陷阱。2.2 方案选型的三重现实约束DB已存在、事务已开启、监控已就位我们做技术选型不能只看论文指标得看脚下的地。for update能成为直通车是因为它完美契合了Java企业级应用的三大基础设施现状DB已存在95%以上的Java Web系统MySQL或PostgreSQL是默认数据源。你不需要为锁单独部署Redis集群、维护ZK四字命令、申请新域名和防火墙策略。for update直接复用现有连接池、主从配置、慢SQL监控上线成本趋近于零。某公司曾为一个日活50万的社区App引入Redisson光是压测验证网络延迟抖动对锁续期的影响就花了两周而用for update重构评论点赞防刷逻辑开发加联调仅用一天。事务已开启Spring的Transactional是Java后端的呼吸机。当你在一个Transactional方法里执行for update锁的生命周期自动绑定到事务——事务提交锁释放事务回滚锁同样释放。这比任何外部锁服务都干净没有“锁泄露”风险Redis锁忘记del、没有“孤儿锁”问题ZK客户端崩溃未释放。我们实测过在一个典型的Spring Boot服务中for update的锁持有时间与业务逻辑执行时间严格一致误差在毫秒级而Redisson的锁续期心跳存在100ms级波动。监控已就位DBA的黄金三件套——慢SQL告警、死锁日志分析、InnoDB行锁等待视图INFORMATION_SCHEMA.INNODB_TRX——全是对for update的天然支持。当出现锁竞争时你不需要翻Redis的INFO命令也不用连ZK客户端看ls /locks直接查SHOW ENGINE INNODB STATUS就能看到谁在等锁、等了多久、阻塞者在执行什么SQL。某次大促期间订单服务出现偶发超时DBA 5分钟内定位到是t_order表的status字段被for update锁住原因是上游一个异步任务未正确关闭事务这种问题在外部锁体系里排查难度至少翻三倍。提示for update不是万能的它的适用边界非常清晰——单行/少量行锁、事务内执行、锁持有时间可控建议1s。如果你的业务需要锁住整个商品类目百万级行、或锁要持有一小时如长流程审批那它确实不合适。但请先问问自己这些场景真的需要“分布式锁”吗还是该用消息队列削峰、状态机驱动、或者分库分表降粒度2.3 为什么不是SELECT ... LOCK IN SHARE MODE共享锁LOCK IN SHARE MODE常被拿来对比但它解决的是完全不同的问题。SHARE MODE允许多个事务同时读取并加共享锁但阻止任何事务加排他锁FOR UPDATE或UPDATE。这适合“读多写少”的只读校验场景比如检查用户是否已参与活动SELECT * FROM t_user_activity WHERE user_id123 AND activity_id456 LOCK IN SHARE MODE但一旦涉及“读-改-写”闭环它就失效了两个事务同时SHARE MODE读到同一行都判断“可以参与”然后都去INSERT记录必然冲突。而FOR UPDATE从根源上杜绝了这种竞态——它强制“读”的那一刻就独占该行后续所有修改都基于确定状态。我们在设计优惠券核销时做过AB测试用SHARE MODE实现“先查余额再扣减”并发100时失败率12%换成FOR UPDATE失败率降为0。根本原因不是性能差异而是语义正确性。3. 核心细节解析与实操要点从SQL到Java的每一处陷阱3.1for update生效的四个硬性前提for update不是魔法它依赖底层存储引擎和SQL执行环境的精确配合。以下四点任一不满足锁都会静默失效这是线上事故最高发区必须在事务中执行这是铁律。SELECT ... FOR UPDATE在自动提交模式autocommit1下会立即提交事务锁瞬间释放。Java中意味着必须用Transactional包裹且传播行为不能是SUPPORTS该行为下无事务则不创建。我们曾在线上发现一个Transactional(propagation Propagation.SUPPORTS)的方法调用了for update查询结果锁在方法返回时就释放了后续更新完全裸奔。必须使用支持行锁的存储引擎MyISAM只支持表锁for update会退化为锁整张表直接拖垮系统。InnoDB是唯一选择且需确认innodb_locks_unsafe_for_binlogOFF默认值否则在某些隔离级别下可能升级为间隙锁。WHERE条件必须命中索引这是最隐蔽的坑。如果WHERE后的字段没有索引InnoDB会进行全表扫描并对所有扫描到的行加锁甚至包括不存在的“幻行”。例如SELECT * FROM t_user WHERE name张三 FOR UPDATE若name无索引锁住的可能是全表所有行。我们用EXPLAIN检查过某次事故的慢SQL正是因此触发锁等待链长达17个事务。解决方案只有两个要么给查询字段加索引要么改用主键查询SELECT * FROM t_user WHERE id123 FOR UPDATE主键索引必然命中。隔离级别需为REPEATABLE READ或SERIALIZABLEREAD COMMITTED级别下for update只锁定扫描到的行不锁间隙可能导致幻读而READ UNCOMMITTED根本不支持for update。Spring Boot默认隔离级别是DEFAULT即数据库默认值MySQL通常为REPEATABLE READ但务必在代码中显式声明Transactional(isolation Isolation.REPEATABLE_READ)避免不同环境配置漂移。注意for update加的是“排他锁”但InnoDB实际加锁类型更复杂——包括记录锁Record Lock、间隙锁Gap Lock、临键锁Next-Key Lock。简单说WHERE id1001主键精确匹配只加记录锁WHERE price BETWEEN 100 AND 200会加临键锁锁住价格区间及间隙。过度锁范围会加剧竞争所以永远优先用主键或唯一索引做for update的查询条件。3.2 Java层的关键实现模式模板化封装与异常兜底直接在Service方法里写JDBCfor update是反模式。我们采用三层封装DAO层提供标准接口、Service层专注业务逻辑、全局切面处理锁失败。以下是核心代码骨架// DAO层统一抽象for update操作 public interface ItemDao { // 返回锁住的行供业务校验 Select(SELECT id, stock, version FROM t_item WHERE id #{id} FOR UPDATE) Options(timeout 3000) // MyBatis设置查询超时防死等 Item selectForUpdate(Param(id) Long id); // 带版本号的乐观更新双重保险 Update(UPDATE t_item SET stock #{stock}, version #{version} 1 WHERE id #{id} AND version #{version}) int updateWithVersion(Param(id) Long id, Param(stock) Integer stock, Param(version) Integer version); } // Service层业务逻辑与锁失败处理 Service public class ItemService { Transactional(rollbackFor Exception.class) public boolean deductStock(Long itemId, Integer quantity) { // 1. 加锁查询 Item item itemDao.selectForUpdate(itemId); if (item null) { throw new BusinessException(商品不存在); } // 2. 业务校验在锁保护下进行 if (item.getStock() quantity) { throw new BusinessException(库存不足); } // 3. 执行更新此时item.stock已是最新值 int updated itemDao.updateWithVersion( itemId, item.getStock() - quantity, item.getVersion() ); if (updated 0) { // 乐观锁失败说明并发更新已发生重试或抛错 throw new BusinessException(库存更新冲突请重试); } return true; } }这个模式的关键设计点Options(timeout 3000)MyBatis的查询超时不是锁超时它控制的是“等待锁的时间”即如果该行正被其他事务锁住当前线程最多等3秒超时抛SQLException。这个值必须小于事务总超时如Spring的Transactional(timeout 10)否则事务会先超时回滚而for update还在傻等。我们线上设为3秒经压测99.9%的锁等待在200ms内完成。乐观锁版本号version字段for update解决的是“读写原子性”但无法防止“两次for update读到同一值后都去更新”。加入version字段UPDATE时校验版本可捕获这种极小概率的并发冲突。这是成本最低的兜底方案比纯for update多一次DB交互但换来100%的数据安全。锁失败必须抛业务异常绝不能吞掉SQLException或TransactionTimedOutException。我们定义了LockAcquireFailedException并在全局异常处理器中统一返回{code:5003,msg:系统繁忙请稍后再试}前端展示友好提示而非500错误页。某次促销因DB主从延迟导致从库for update失败正是这个异常机制让我们快速定位到延迟问题。3.3 索引设计与SQL优化让锁“快进快出”锁的性能瓶颈80%不在锁本身而在SQL执行效率。for update越快拿到锁持有时间越短竞争就越小。我们总结出三条黄金索引法则主键查询是唯一推荐路径SELECT ... FOR UPDATE WHERE id ?是最优解。InnoDB通过聚簇索引直接定位记录加锁过程毫秒级。我们对比过主键查询平均耗时1.2ms而普通索引如idx_user_id需回表平均耗时3.8ms锁持有时间直接翻三倍。联合索引要覆盖查询更新字段如果for update后紧跟UPDATE且更新字段不在索引中会导致“索引修改聚簇索引修改”两次IO。例如UPDATE t_order SET status2, updated_timenow() WHERE order_noNO123若order_no是普通索引updated_time不在索引中则需先通过索引找到记录再修改聚簇索引。解决方案是建联合索引idx_order_no_status_updated让UPDATE也能走索引。禁止在for update中使用函数或计算SELECT ... FOR UPDATE WHERE DATE(create_time) 2023-01-01会让索引失效触发全表扫描加锁。必须改写为WHERE create_time 2023-01-01 AND create_time 2023-01-02。我们曾为一个订单履约系统优化过锁SQL。原始语句是SELECT * FROM t_delivery WHERE order_id ? AND status IN (1,2) FOR UPDATEorder_id有索引但status无索引执行计划显示typerange锁等待严重。优化后新增联合索引idx_order_id_status (order_id, status)SQL改为SELECT id, status, version FROM t_delivery WHERE order_id ? AND status IN (1,2) FOR UPDATE只查必要字段减少网络传输效果锁平均等待时间从320ms降至18msP99从1.2s降至86ms。4. 实操全流程与核心环节实现从本地调试到线上灰度4.1 本地开发环境搭建用H2模拟InnoDB行锁行为生产环境调试for update风险极高必须在本地复现锁竞争。H2内存数据库虽不完全等同InnoDB但开启DB_CLOSE_ON_EXITFALSE和LOCK_TIMEOUT3000后能模拟基本的锁等待。Spring Boot配置如下# application-dev.yml spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE;LOCK_TIMEOUT3000 driver-class-name: org.h2.Driver h2: console: enabled: true关键技巧H2的LOCK_TIMEOUT单位是毫秒需与MyBatis的Options(timeout)一致。我们写了一个JUnit测试用CountDownLatch模拟两个线程同时执行for updateTest public void testForUpdateConcurrency() throws InterruptedException { CountDownLatch latch new CountDownLatch(2); ExecutorService executor Executors.newFixedThreadPool(2); // 线程1先获取锁休眠2秒模拟业务处理 executor.submit(() - { itemService.deductStock(1001L, 1); latch.countDown(); }); // 线程2紧随其后尝试获取同一行锁 executor.submit(() - { try { Thread.sleep(100); // 确保线程1先执行 itemService.deductStock(1001L, 1); } catch (BusinessException e) { // 预期线程2会因锁等待超时抛出异常 assertThat(e.getMessage()).contains(系统繁忙); } latch.countDown(); }); latch.await(); executor.shutdown(); }这个测试能100%复现锁等待场景是验证锁逻辑正确性的第一道防线。注意H2不支持间隙锁所以WHERE price BETWEEN类SQL在H2里不会锁间隙生产环境仍需专项测试。4.2 生产环境SQL审核DBA必须介入的三个检查点上线前所有含for update的SQL必须经过DBA书面审核我们固化了三个必查项检查点审核标准违规示例后果索引覆盖EXPLAIN显示typeconst或typerefkey列显示命中索引typeall全表扫描锁住全表引发雪崩锁范围WHERE条件必须为等值查询禁止、、BETWEENWHERE created_time 2023-01-01触发间隙锁锁范围不可控事务边界for update必须在Transactional方法内且传播行为为REQUIREDpropagationSUPPORTS或方法无事务注解锁立即释放失去保护某次上线一个新同事写了SELECT ... FOR UPDATE WHERE user_id? AND status?但user_id和status是分开的单列索引EXPLAIN显示typeref但key_len只用了user_id索引长度DBA立刻拦截这会导致status条件在内存中过滤锁住所有user_id匹配的行而非仅目标行。最终补上联合索引idx_user_id_status才放行。4.3 线上灰度与监控埋点让锁“看得见、管得住”for update上线不是一锤子买卖必须分阶段验证。我们采用三级灰度Level 1只读监控1天将SELECT ... FOR UPDATE临时改为SELECT ...去掉FOR UPDATE在SQL中添加注释/* LOCK_MONITORING */通过慢SQL日志统计该SQL的QPS、平均耗时、最大耗时。目标确认该SQL本身无性能问题QPS在预期范围内如500 QPS。Level 2锁等待监控3天恢复FOR UPDATE但将Options(timeout)设为100ms远低于业务容忍阈值在日志中打印LockWaitTime从执行SQL到返回结果的时间。通过ELK聚合监控LockWaitTime 50ms的请求占比。目标该占比0.1%否则说明锁竞争已成瓶颈。Level 3全量业务验证7天timeout恢复为3000ms接入业务监控每笔订单的“锁获取成功数”、“锁等待超时数”、“乐观锁更新失败数”。我们定义健康水位线锁超时率 0.05%乐观锁失败率 0.01%平均锁持有时间 50ms配套监控看板必须包含InnoDB锁等待图INFORMATION_SCHEMA.INNODB_TRXINNODB_LOCK_WAITS关联查询慢SQL中含FOR UPDATE的TOP10列表业务维度锁失败率按商品类目、用户等级、地域分组某次大促前灰度我们发现“手机数码”类目锁超时率达0.3%追查发现是该类目商品主图URL字段过大平均12KBSELECT *导致网络传输慢进而拉长锁持有时间。优化为SELECT id, stock, version后问题消失。5. 常见问题与排查技巧实录那些年我们踩过的坑5.1 经典死锁案例与根因分析死锁不是for update的缺陷而是事务设计的照妖镜。我们整理了线上最频发的三类死锁模式附带SHOW ENGINE INNODB STATUS的解读方法案例1交叉更新最常见事务ASELECT ... FOR UPDATE WHERE id1001; 然后UPDATE ... WHERE id1002事务BSELECT ... FOR UPDATE WHERE id1002; 然后UPDATE ... WHERE id1001死锁日志关键行*** (1) WAITING FOR THIS LOCK TO BE GRANTED: record lock ... space id ... page no ... n bits 72和*** (2) HOLDS THE LOCK(S): record lock ...根因两个事务以不同顺序访问同一组资源。解法所有事务必须按固定顺序访问行。我们强制要求更新多行时WHERE id IN (1001,1002)改为WHERE id IN (1001,1002) ORDER BY id确保InnoDB按主键升序加锁。案例2间隙锁升级表t_user有索引idx_age (age)当前数据age25,28,32事务ASELECT ... FOR UPDATE WHERE age 25 AND age 30→ 锁住间隙(25,28)和(28,30)事务BINSERT INTO t_user(age) VALUES(27)→ 等待间隙锁事务CSELECT ... FOR UPDATE WHERE age27→ 等待事务B的插入锁形成环根因范围查询触发间隙锁新插入被阻塞。解法避免在高频更新表上用范围条件for update。改为用主键ID分页先SELECT id FROM t_user WHERE age 25 AND age 30 LIMIT 100再用id IN (...)精确查询。案例3隐式锁升级事务AUPDATE t_order SET status2 WHERE order_noNO1无索引全表扫描事务BSELECT ... FOR UPDATE WHERE order_noNO2由于事务A锁了全表事务B必然等待根因order_no无索引UPDATE退化为表锁。解法所有for update的WHERE字段必须有索引且EXPLAIN验证。我们上线前用SQL审核工具自动检测WHERE字段索引缺失。提示死锁日志里*** (1) TRANSACTION:后的mysql tables in use 1, locked 1表示锁了1张表LOCK WAIT 2 lock struct(s), heap size 1136表示等待2个锁结构。重点关注WAITING FOR THIS LOCK和HOLDS THE LOCK(S)两段它们揭示了锁等待链。5.2 “锁没生效”的十大排查清单当业务出现“明明写了for update还是发生数据覆盖”按此清单逐项排除检查事务是否真正开启在方法入口打日志log.info(Transaction active: {}, TransactionSynchronizationManager.isActualTransactionActive())确认返回true。确认SQL执行在同一个ConnectionMyBatis的SqlSession必须复用。如果DAO方法里手动sqlSession.close()会导致后续UPDATE在新连接上执行无锁保护。验证autocommit状态在MySQL客户端执行SELECT autocommit;必须为1Spring管理事务时连接池会自动设为0但需确认。检查WHERE条件是否动态拼接Select(SELECT ... WHERE ${column} #{value} FOR UPDATE)中的${}会绕过预编译若column为空字符串SQL变成WHERE ?语法错误导致锁失效。确认数据库版本MySQL 5.6以下版本对for update的支持有Bug必须升级到5.7。检查连接池配置HikariCP的connection-timeout必须大于Options(timeout)否则连接获取超时会掩盖锁等待超时。验证隔离级别SELECT tx_isolation;必须返回REPEATABLE-READ。检查是否有SELECT ... FOR UPDATE NOWAITNOWAIT在获取不到锁时立即报错但很多同学误以为它“不加锁”实际是加锁失败策略不同。确认没有Async或线程池切换Async方法会新开事务for update在新事务里执行与主线程无关。检查MyBatis的fetchSize设为Integer.MIN_VALUE流式读取会导致for update失效必须用默认值或正数。我们曾为一个支付对账服务排查过第9条对账任务用Async启动for update在子线程事务里执行而主事务早已提交锁瞬间释放。解决方案是移除Async改用TaskScheduler定时触发确保事务上下文完整。5.3 性能压测的正确姿势别用JMeter测锁压测for update核心不是QPS而是锁竞争率。我们弃用JMeter改用定制化压测脚本# Python压测脚本核心逻辑 import threading import time from concurrent.futures import ThreadPoolExecutor def lock_test(item_id): start time.time() try: # 调用Java服务的HTTP接口含for update逻辑 resp requests.post(fhttp://api/item/deduct, json{itemId: item_id, qty: 1}) end time.time() if resp.status_code 200: return {success: True, wait_time: end - start} else: return {success: False, error: resp.json().get(msg)} except Exception as e: return {success: False, error: str(e)} # 模拟100个线程争抢同一商品ID with ThreadPoolExecutor(max_workers100) as executor: futures [executor.submit(lock_test, 1001) for _ in range(100)] results [f.result() for f in futures]关键指标采集锁等待时间分布统计wait_time的P50/P90/P99理想值P99 100ms失败率构成区分LockTimeout等待超时、OptimisticLockFailure版本冲突、BusinessError库存不足DB锁等待视图压测中实时查询SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) 1看长事务数量某次压测P99锁等待达850msDBA查INNODB_TRX发现一个事务trx_started时间异常久追溯到是某个异步日志任务未正确关闭事务。修复后P99降至42ms。6. 架构演进思考for update不是终点而是起点用好for update只是分布式协作的第一步。随着业务增长你会自然面临三个演进方向每个都值得深思6.1 当单行锁不够用从“行锁”到“逻辑锁分片”当一个商品ID被千万级用户争抢如顶流明星演唱会门票for update的单行锁会成为瓶颈。我们不做盲目分库分表而是引入逻辑锁分片将一个商品的库存逻辑上拆成100个“子库存单元”每个单元有独立ID如item_1001_001,item_1001_002...for update只锁其中一个单元。用户下单时用userId % 100决定锁哪个单元。这样锁竞争被分散到100个行上QPS提升近百倍。关键点子单元ID必须全局唯一且UPDATE时要校验总库存避免超卖。我们用Redis原子计数器做总库存兜底for update负责单元级精确控制形成混合锁模式。6.2 当DB成为瓶颈从“DB锁”到“DB缓存双写锁”当锁QPS超过DB单机极限如5000 QPS我们保留for update作为最终一致性保障但前置增加Redis缓存锁先SET lock:1001 1 NX PX 10000Redis锁成功则执行for update失败则快速返回for update成功后更新DB并删除Redis锁这本质是“缓存击穿防护”Redis锁挡掉90%的无效请求DB只处理真正需要加锁的请求。注意Redis锁必须设置合理过期时间且for update事务内要校验Redis锁是否仍存在防止Redis锁过期而DB锁未释放的“双锁失效”。6.3 当业务语义复杂从“锁资源”到“锁状态机”最根本的演进是跳出“锁”的思维定式。比如订单状态流转created → paid → shipped → finished传统做法是UPDATE t_order SET status2 WHERE id? AND status1用for update保证状态不跳变。但更好的方式是用状态机引擎定义状态转移规则{from: created, to: paid, condition: payment_success}每次状态变更前引擎校验规则是否满足。for update退化为状态机引擎的底层存储保障业务逻辑彻底解耦。我们用开源的state-machine框架实现了这一点锁的复杂度从SQL层转移到了领域层可维护性大幅提升。我个人在实际操作中的体会是for update的价值不在于它多高大上而在于它足够朴素、足够透明、足够可控。当你能在DBA的慢SQL日志里一眼看到“谁在等锁、等了多久、为什么等”你就掌握了分布式协作最底层的脉搏。它不承诺极致性能但兑现了绝对可靠它不追求炫技架构但守住了业务底线。在微服务满天飞的今天能用一行SQL、一个事务、一张表就把一件重要的事做对这本身就是一种高级的架构能力。