前几天和几个做微服务架构的朋友讨论方案,有人突然问了一句:“项目里已经引入了 Seata,为什么还要用 Redisson 的分布式锁?这俩功能不是重复吗?二选一是不是就够了?”这个问题当时让讨论停滞了几秒,因为它确实不是个冷门疑问。我在不少团队的架构评审会上也见过类似的场景:画架构图时,Redisson 出现在分布式协调的格子里,Seata 出现在分布式事务的格子里,然后自然而然就有人冒出一句“看着像重复了”。
这个问题如果只看组件名和网上的碎片文章,确实容易被绕进去。但如果真在业务里跑过一段时间,就会知道这两者完全不是一回事。我先讲一段自己的经历:有一年我给一个促销系统做瘦身,为了减少中间件依赖,把 Redisson 的锁拿掉,想着“只要 Seata 能保证事务,数据就不会出问题”,结果压测阶段库存直接被扣成了负数。那次事故让我彻底想明白了一个结论——Redisson 和 Seata 不是替代关系,它们解决的是两个维度的问题。
今天这篇就把两者分别负责什么、为什么会被误读、真实场景里怎么搭配,以及我在选型和踩坑过程中积累的判断方式,一次性说清楚。
1. 误解从哪儿来:Redisson 和 Seata 被混为一谈
我第一次见到有人把这两个组件放在一起对比时,第一反应是有点懵。因为在我平时的设计习惯里,Redisson 是放在“并发控制与协调”这个分类下的,而 Seata 是放在“数据一致性保障”这个分类下的,两者平时根本不会放在同一个技术选项里去评比。但既然这个问题反复出现,就说明它的产生有必然性,不是个别现象。
1.1 面试里那个高频问题:“有了 Seata 还需要分布式锁吗”
很多把两者搞混的同学,最先接触它们是在面试题库里。搜索“Redisson 和 Seata”相关的热搜词,后面往往跟着“分布式锁”“分布式事务”“微服务架构”“Seata 面试题”这些标签。在面试题和底层原理文章里,分布式锁和分布式事务经常被放在同一个大的“分布式问题”分类下面。一个人如果先看了一遍 Redis 实现分布式锁的原理,又看了一遍 Seata AT 模式的原理,很容易在心里形成一个模糊的印象:都是解决分布式引入的问题,都是保证一致性,也许本质上是同一件事的不同叫法?
但实际上,面试题把它们放在一起,只是因为它们都属于“分布式架构中常见的高频问题”,不代表它们是同一种解决方案。类比一下:你在驾校学了倒车入库,也学了侧方停车,它们都是停车技术,可你不可能用倒车入库去处理侧方停车位,反之亦然。锁和事务在分布式系统里面对的“车位”根本不一样。
1.2 “锁”和“事务”在语义上被归到了同一个技术问题
锁解决的是一个很朴素的冲突问题:多个进程要修改同一份数据时,得保证同一时刻只有一个进程在改。你可以把它想象成公共厕所门口的红绿灯,红灯亮的时候只有一个人能进去,其他人在外面排队。这是并发互斥的问题。
事务解决的是另一件事:一系列操作要么全部成功,要么全部失败。想象你在整理一条走廊,要把五扇门都关上并且锁好,如果关到第三扇门时发现锁坏了,前面两扇已经关上的门也要重新打开,不能让走廊处于“一半门关了、一半门没关”的状态。这是原子性和一致性的问题。
红绿灯管的是谁能进门,走廊管理管的是五扇门是不是都能保持一致状态。这两个问题本身不冲突,也不重叠。但在分布式架构的语境下,大家开口闭口都是“分布式一致性”,就容易把“数据并发互斥”和“事务一致性”这两件事混在一个大帽子里,以为既然帽子是同一个,那下面的工具自然也重复了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redisson 的实际定位:解决进程间的资源竞争
Redsisson 本质上是一个基于 Redis 实现的 Java 客户端库,但它不是普通的那种 get/set 的缓存客户端。它提供了大量分布式组件,其中最出名的就是分布式锁。我用它主要解决的是进程之间“抢资源”的问题。
2.1 Redisson 实际提供的能力:从 RLock 到限流器
先梳理一下 Redisson 在真实项目里被用到的几个高频组件,这样能清晰看到它的职责边界:
- RLock:可重入分布式锁,也是用得最多的能力。支持自动续期(watch dog),默认锁超时时间是 30 秒,业务没执行完会自动延长锁的持有时间。
- RReadWriteLock:读写锁,适合读多写少场景,读锁之间互不阻塞,写锁独占。
- RSemaphore:分布式信号量,可以控制同时访问某个资源的线程数,比如限制同一时刻只能有 N 个请求去调用外部接口。
- RRateLimiter:分布式限流器,用于在集群环境下做统一的流量控制。
- RBucket、RMap、RAtomicLong:这些是分布式对象和原子计数器,比如用 RAtomicLong 做全局自增 ID。
注意看,这些能力全部集中在“并发控制、互斥、限流、计数”这个范畴里,没有一个是在管跨服务事务的。Redisson 再怎么封装,它背后的支柱还是 Redis 的 SETNX、EXPIRE 这些命令,核心解决的就是“大家别同时动同一块数据”。
2.2 一个具体例子:高并发扣减库存时它做了什么
假设有一个秒杀系统的扣减库存操作。在分布式环境下,请求会分散到多个应用实例上,每个实例都不能信任对方“不会同时来扣”,所以要用分布式锁把同一个 SKU 的扣减操作串行化。代码大概是这个意思:
java复制RLock lock = redissonClient.getLock("stock:sku:" + skuId);
boolean locked = lock.tryLock(0, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BizException("系统繁忙,请稍后重试");
}
try {
// 真正执行库存扣减逻辑
stockService.deduct(skuId, 1);
} finally {
lock.unlock();
}
这段代码的作用是:同一时刻,一个 SKU 的库存扣减动作只允许一个线程去做。其他线程拿到“系统繁忙”的提示,直接返回。这样就不会出现两个请求都读到库存剩余 1、然后都扣减成功、最后库存变成 -1 的情况。
2.3 Redisson 的能力边界
但 Redisson 不管业务的后续环节。它保证扣减库存的那一刻没有并发冲突,但它不保证“扣减库存”和“写订单”这两个动作是不是都能落地。如果在扣完库存之后,写订单的服务宕机了,库存已经扣掉,订单却没了,这就是 Redisson 管不到的事情。它也不是为了管这个而存在的,你不可能靠一个红绿灯去保证走廊里的五扇门状态一致。
3. Seata 的实际定位:解决跨服务的数据一致性
Seata 和 Redisson 走的是完全不同的技术路线。Seata 是阿里开源的一套分布式事务中间件,解决的是多个服务之间数据操作的原子性问题。它把一次分布式调用链里涉及的所有数据库操作,汇聚到一个全局事务里面来协调。
3.1 TC/TM/RM 三个角色在请求链路里如何协作
Seata 的核心模型是三个角色:
- TC(Transaction Coordinator):事务协调者,独立部署的服务端,负责统筹全局事务的提交或回滚。
- TM(Transaction Manager):事务管理器,嵌入在发起全局事务的业务应用里,负责告诉 TC “我要开一个全局事务”“这个事务要提交了”。
- RM(Resource Manager):资源管理器,嵌入在参与事务的每个服务里,负责管理各自数据库连接对应的分支事务,并向 TC 注册分支事务。
一次完整的调用流程大致是这样:TM 向 TC 申请开启全局事务,拿到一个全局事务 ID;然后调用链里每个服务执行本地数据库操作时,RM 都把这个操作记录成一个分支事务,注册到 TC 下面;当最外层的业务方法执行完毕后,TM 通知 TC 发起全局提交或回滚。如果中间任何一个环节失败,TC 就会通知所有 RM 回滚各自的分支事务。
代码层面的使用方式很简洁:
java复制@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO order) {
productClient.deductStock(order.getProductId(), order.getCount());
orderMapper.insert(order);
}
3.2 AT/TCC/SAGA/XA 四种模式该怎么选
Seata 支持四种模式,这个问题也是很多人在选型时纠结很久的地方。它们的区别可以这样理解:
| 模式 | 核心机制 | 适用场景 | 性能特征 |
|---|---|---|---|
| AT | 一阶段执行 SQL 并生成 undo log,二阶段根据 undo log 自动回滚 | 基于关系型数据库的常规 CRUD | 侵入性小,性能一般 |
| TCC | 手写 Try、Confirm、Cancel 三个接口 | 不依赖数据库、需要精细控制资源的情况 | 性能较好,开发量大 |
| SAGA | 通过状态机编排一系列本地事务,出错后执行反向补偿 | 长事务、异步流程 | 高可用,适合复杂业务链路 |
| XA | 依赖数据库原生分布式事务协议 | 对一致性要求极高、数据库支持 XA | 性能较慢 |
我自己的经验是:如果业务都是基于 MySQL 的普通 CRUD,AT 模式最省事;但如果涉及非数据库资源(比如调用第三方 API、操作文件),AT 模式就管不住了,这时候得考虑 TCC 或 SAGA。这四种模式不管选哪种,Seata 要解决的问题始终是“让跨服务、跨库的数据操作在整体上保持一致”。
3.3 Seata 为什么不能替代分布式锁
这里有一个很关键的技术细节:Seata 的 AT 模式在并发控制上提供的保护是有限的。AT 模式的默认隔离级别是“读未提交”,它通过全局锁来避免多个全局事务同时修改同一条数据,但对于一个业务系统来说,“避免同时修改”和“防止超卖”是两个不同强度的问题。全局锁保证的是事务之间的数据冲突能被识别并触发回滚,可是如果两个事务在极短的时间里都各自完成了本地提交,其中一个回滚会直接导致数据库层面的数据出现异常,而 Seata 的机制并不能在所有场景下自动感知这种“业务层面的逻辑并发错误”。
用一个现实中的例子来说明:一辆公交车核载 30 人,两个售票员同时卖票。分布式锁是在售票窗口前放一道闸机,一次只放一个人进来买票,这样票一定不会超卖。Seata 的事务管理是记录每笔交易,如果后来发现有人买重了还能想办法退款,但它管不了“两个售票员同时把同一张票卖给了两个人”这个瞬间发生的错误。要避免同秒下两单都成功,必须在入口处就做好互斥,这个入口互斥就是分布式锁的职责。
4. 为什么“二选一就够了”会让人信以为真
这个误区能在很多团队里流传,不是偶然的。我总结了三个层面的原因,每一个都能在现实讨论中找到对应。
4.1 组件命名的误导与概念混淆
Redisson 的名字里有“Red”,看到“分布式锁”的字眼,再看 Seata 的名字,听到“分布式事务”,大脑很容易把“分布式”这个定语当成它们之间的共同点。“既然都是处理分布式问题的,那应该可以二选一”——这是最直觉的反应,但也是最容易被直觉带偏的地方。
在技术讨论中还有一个常见误区:把“数据一致性问题”当成一个统一的命题。实际上,复杂系统里有弱一致性、最终一致性、强一致性、因果一致性等各种维度,而分布式锁和分布式事务处在完全不同的维度上。前者关注的是并发时序,后者关注的是故障恢复与原子性。用“一致性”这个词把两个维度捆在一起,自然会得出功能重复的错误结论。
4.2 前端文章和面试题讨论的论述盲区
网上的技术文章经常把“Redis 实现分布式锁”和“Seata 分布式事务”并列在同一个课程目录里,讲完锁的面试题就讲 Seata 的面试题。这种结构会让读者产生一种错觉,以为两者是“同一个问题的两种解法”。但实际上它们只是“分布式系统学习路线上相邻的两个知识点”,相邻不代表同义。
很多面试题描述也不是很严谨。比如“你们系统是怎么解决超卖问题的?”有人回答“用了 Redis 分布式锁”,有人回答“用了 Seata 分布式事务”。这两种回答在面试官那里得到的评价完全不同,但很多候选人不理解为什么不同——同样是解决超卖,为什么答案不一样?因为在超卖场景下,分布式锁是在入口处避免并发问题,而 Seata 是在出口处保证数据回滚不动乱,两者解决的问题阶段不同,使用上也不是替代关系。
4.3 简化架构的冲动与反向极端
我还遇到过一类团队,想精简中间件数量,觉得“能少一个组件就少一个组件”。这种心态本身没错,但要注意别走极端。架构设计里有一个经典误区:把“简化”理解成“少用组件”,而不是“让每个组件都职责清晰”。Redisson 和 Seata 如果确实在各自的职责范围内被需要,砍掉任何一个都会把新的复杂度引入到业务代码里。
有个更隐蔽的误判是:认为“有 Seata 兜底回滚,就不怕并发错误”。我踩过的那个库存负数坑,正是这种心态的代价。后面第 7 节我会详细讲这个事故的排查过程。
5. 真实架构里两者是怎么搭配的:三个典型场景
理论讲再多,不如看几个能落地的场景。下面这三种架构是我实际见过或亲历过的,也是 Redisson 和 Seata 并存最典型的组合方式。
5.1 场景一:秒杀/活动库存系统
秒杀系统是整个微服务集群里并发压力最大的场景之一。一次秒杀请求链路会同时涉及库存服务、订单服务、活动积分服务等多个模块。我习惯的设计是:
- 用户请求先经过网关限流,这里可以用 Redisson 的 RRateLimiter 做集群级限流。
- 进入秒杀接口后,先用 Redisson 的分布式锁锁住“商品 SKU”维度,防止并发扣减库存。
- 拿到锁之后,执行库存扣减和订单创建的远程调用。
- 整条调用链用 Seata 的 @GlobalTransactional 包裹,保证库存扣减和订单创建要么同时成功,要么同时回滚。
在这个设计里,Redisson 管住“不能有多个请求同时操作同一个 SKU”,Seata 管住“库存扣了但订单没写成功”这类跨服务故障。两条链路互相补充,任何一条缺失,线上都会出问题。
5.2 场景二:分布式定时任务调度
很多团队会用分布式任务调度平台,比如 xxl-job、ElasticJob。这些平台本身一般只负责把任务分发到某个节点,不负责任务执行过程中的并发控制。如果多个服务实例同时部署了一个定时任务,并且没有做互斥,就会发生同一批数据被处理两遍的问题。
我常用的方案是:在任务执行方法的第一行,用 Redisson 的 tryLock 抢一把任务锁,只有抢到锁的实例才执行任务逻辑。任务内部如果有跨服务的数据写入(比如把报表数据同步到另一个服务),再用 Seata 保证这些跨服务操作的一致性。这里锁的维度是“任务”,事务的维度是“任务内部的数据变更”,相互独立、互不干扰。
5.3 场景三:跨服务转账/订单流程
转账这类业务天然要求强一致。A 账户扣钱、B 账户加钱,如果分别在两个服务里,就必须用 Seata 做全局事务,否则扣钱成功、加钱失败,资金就平白无故消失了。但转账还有一个隐藏的并发风险:同一个账户同时收到两个转账请求,余额为 100,第一个请求转 80,第二个请求转 60,如果不加锁,两次操作都判断余额充足,实际却会透支。
在单库时代,这个问题可以用数据库的行锁解决;在微服务时代,如果账户数据在独立的账户服务里,跨服务行锁不生效,就需要用 Redisson 对账户维度加分布式锁。所以一个健壮的转账链路里,Redisson 负责“防止同一账户并发操作”,Seata 负责“保证两个账户的增减要么都成功要么都失败”。两者适用不同的故障模型,谈不上重复。
6. 选型决策框架:什么情况下确实只需要其中一个
当然,不是说任何项目都必须同时上 Redisson 和 Seata。如果一个系统部署很简单、业务也不复杂,确实不需要引入任何一个。关键是有一套清晰的判断依据,而不是凭感觉或随大流。
6.1 判断并发冲突程度
先问自己一个问题:我的系统里,是否存在多个应用实例会同时修改同一份数据的场景?比如:
- 同一个商品库存,会被多个服务实例并发修改。
- 同一个任务,会被多个调度节点同时执行。
- 同一个账户余额,会被多个请求同时变更。
如果答案是“存在”,并且允许并发修改会导致严重后果(超卖、重复执行、透支),那么就需要 Redisson 这类分布式锁,或者在数据层用数据库行锁、版本号等方案替代。如果所有实例都不存在抢同一份资源的情况,或者并发量低到可以接受重试,那就不需要为分布式锁专门引入组件。
6.2 判断数据一致性要求
第二个问题:一个业务请求,是否会跨多个服务或跨多个数据库写入数据,并且要求“要么都成功、要么都失败”?
- 下单链路要写订单库、扣库存库、加积分库。
- 转账链路要写 A 账户库、B 账户库。
- 对账系统要把核算结果同步写进多个报表库。
如果存在这种场景,就需要 Seata 这类分布式事务框架。如果所有写操作都集中在同一个数据库里,用数据库本地事务就能解决,那么 Seata 就是一个额外的重量级依赖,引入它反而增加了复杂度和运维成本。
6.3 一张决策清单解决架构评审争论
我经常在方案评审会上用下面这张表来收敛结论,很多争论到最后都会变成“你属于哪种情况”的问题:
| 系统状态 | 需要 Redisson | 需要 Seata | 说明 |
|---|---|---|---|
| 单实例部署,单库 | 否 | 否 | 本地锁 + 本地事务即可 |
| 多实例部署,但写操作仍集中在单库 | 可能 | 否 | 并发抢数据时用 Redisson,事务交给本地事务 |
| 多实例部署,跨服务写操作 | 根据并发冲突情况决定 | 是 | Seata 管一致性,锁按需引入 |
| 多实例部署,高并发抢资源 + 跨服务写 | 是 | 是 | 两者搭配,各管一段 |
这张表的关键在于:只有当“高并发抢资源”和“跨服务写操作”同时出现时,才需要 Redisson 和 Seata 同时出现。当只有其中一个条件满足时,对应的组件也要相应减少。
7. 我踩过的坑:锁与事务混为一谈的代价
7.1 去掉 Redisson 只留 Seata 的库存负数事故
那次事故发生在一次大促活动的压测阶段。当时项目里有一个商品详情页的秒杀功能,最初用的是 Redisson 分布式锁控制库存扣减,后来架构评审会上有人提出“系统里已经上了 Seata,是不是可以用 Seata 的事务来保证一致性,把 Redisson 去掉?”我当时也认为 Seata 的全局锁可以兜底,就在压测前把 Redisson 的锁代码删了。
结果压测刚开始,后台监控就报警:库存出现了 -2 的情况。我顺着调用链排查,发现两个请求同时打到了库存服务,两者都从数据库里读到了剩余库存为 1(Redis 里并没有锁拦截),然后在各自的本地事务里执行了 update stock set count = count - 1 的语句。因为 Seata 的 AT 模式通过 undo log 来记录分支事务,它保证的是“如果全局事务失败,所有分支都能回滚”,但它并不能阻止两个本地事务都成功提交后库存变负数——这种业务层面的并发错误,必须在逻辑入口加锁才能挡住。
那次事故让我彻底理解了 Seata 和 Redisson 的边界:Seata 是万不得已的保底方案,Redisson 是主动避免并发问题的手段。保底方案不能替代主动性防御,主动性防御也替代不了保底方案。
7.2 Redisson 锁时间设置不当带来的重复执行
还有一个教训来自 Redisson 本身的参数细节。有段时间我在代码里显式设置了分布式锁的 leaseTime(锁的自动释放时间),设成了 10 秒。结果有个复杂的业务方法执行了 15 秒,锁在第 10 秒时就自动被 Redis 释放了,另一个请求趁虚而入,把同一笔业务重复执行了一遍。
后来查文档才知道:Redisson 的分布式锁如果不显式设置 leaseTime,默认会启用 watch dog 自动续期机制,每 10 秒检测一次,如果业务线程还活着,就自动把锁的过期时间延长到 30 秒。但如果显式设置了 leaseTime,watch dog 就不会生效,锁会在固定时间后强制释放。对于执行时间不确定的业务逻辑,不要轻易设置固定的 leaseTime,除非你能准确预估业务执行时长。
这个坑虽然不是 Redisson 和 Seata 之间的功能混淆,但它提醒了我一个更本质的问题:分布式锁并不是拿来就能用的基础设施,锁的粒度、超时时间、续期逻辑都要结合具体业务去设计。
7.3 架构文档中如何拆解“并发控制”和“事务一致性”
经历了前面几次踩坑之后,我调整了团队架构文档的编写方式。以前我们习惯把“分布式锁”和“分布式事务”放在同一个“分布式协调方案”章节里,这样写容易让新人误以为它们是同一类东西。现在我会把这两个关注点拆成两个独立章节:
- “并发与资源竞争控制”章节,专门描述系统里哪些资源可能被并发修改、用什么锁或幂等方案来保护。
- “跨服务数据一致性”章节,专门描述哪些业务链路涉及跨服务写操作、用什么事务方案来兜底。
这样做的好处是,架构评审时每个人都能一眼看清某个组件的职责边界,不会再把“有 Seata 就不需要锁”这种错误结论带到代码里去。每次设计新的业务模块时,我也会先按照 6.3 的决策清单走一遍,确认这个模块到底落在四个象限的哪个位置,而不是靠“感觉要加个锁”或者“感觉事务能兜住”来拍脑袋。
我个人最大的体会是:架构设计里真正重要的,从来不是某个组件本身有多强,而是你能不能把问题维度拆清楚。Redisson 和 Seata 不存在能不能二选一的问题,它们各自解决的是一个独立维度的问题,强行合并只会把风险从组件层转移到业务层。如果你在架构评审时也遇到过类似的争论,建议把“并发控制”和“事务一致性”这两件事分开写进设计文档里,先用两到三个真实业务场景验证一下,再决定组件要不要留、怎么留。纸上谈兵永远不如线上事故来得直观。
