1. 为什么我要折腾 Seata 1.3 的 Redis 模式
分布式事务这个话题,做过微服务的人应该都不陌生。订单扣库存、账户扣余额、积分加记录,这几个操作分散在不同服务里,任何一个环节挂了,数据就可能对不上。Seata 作为一款开源的分布式事务框架,在业内用得相当广泛,它提供了 AT、TCC、SAGA、XA 四种事务模式,其中 AT 模式因为对业务代码侵入小,成了大多数团队的首选。
但 AT 模式有一个绕不开的组件——TC(Transaction Coordinator,事务协调器)。所有分支事务的注册、状态上报、全局锁的申请与释放,都要经过 TC。而 TC 的存储模式,直接决定了整个分布式事务链路的性能上限。Seata 1.3 之前,TC 的存储主要支持 file、db、redis 三种模式。file 模式只适合单机演示,db 模式依赖关系型数据库,在高并发下写入压力很大,redis 模式理论上性能最好,但在 1.3 版本之前,redis 模式的实现存在不少问题,比如序列化方式笨重、key 设计不合理、批量操作缺失等。
我所在的团队当时正好在做一次服务拆分,核心交易链路拆成了六个微服务,分布式事务的 TPS 峰值预估在 3000 左右。用 db 模式压测下来,TC 的数据库连接池经常被打满,全局事务的平均耗时从 80ms 飙升到 400ms 以上。于是我把目光投向了 redis 模式,但发现社区里关于 Seata 1.3 redis 模式重构的资料非常零散,官方文档只讲了怎么配置,没讲为什么这么改、改完性能到底提升多少。这篇文章就是把我自己从源码到压测的完整过程整理出来,给同样在选型路上纠结的朋友一个参考。
不管你是刚接触 Seata 的新手,还是正在为 TC 性能发愁的老手,只要你对分布式事务的底层存储机制感兴趣,这篇内容应该都能帮你省下不少查资料和踩坑的时间。我会从重构的背景讲起,拆解核心改动点,给出完整的压测方案和对比数据,最后分享几个我在生产环境里验证过的调优技巧。
2. Seata 1.3 Redis 模式重构的背景与核心思路
2.1 旧版 Redis 存储模式到底卡在哪里
要理解这次重构的价值,得先搞清楚旧版 redis 模式的问题出在哪儿。Seata 1.3 之前的 redis 存储实现,核心逻辑集中在RedisTransactionStoreManager这个类里。我翻完源码后,总结出三个最致命的瓶颈。
第一个问题是序列化方式。旧版默认用的是 JDK 原生序列化,把整个GlobalSession对象直接序列化成字节数组存进 redis。JDK 序列化有两个毛病:一是体积大,一个普通的全局事务对象序列化后动辄 2KB 以上;二是速度慢,序列化和反序列化的开销在高频读写场景下非常可观。你可以想象一下,每次分支事务注册都要把整个全局会话对象读出来、反序列化、修改、再序列化写回去,这个过程的 CPU 消耗是线性增长的。
第二个问题是 key 的设计过于分散。旧版把全局事务、分支事务、全局锁分别存在不同的 key 里,而且没有做合理的命名空间隔离。一次全局事务的提交,需要读取和写入十几个 key,每次都是独立的 redis 命令。在网络往返延迟(RTT)面前,这种碎片化的操作模式让 redis 的高性能优势荡然无存。我实测过,单次全局事务提交在旧版 redis 模式下会产生 20 次以上的 redis 命令调用。
第三个问题是缺乏批量操作和管道机制。旧版代码里几乎看不到 pipeline 或者 Lua 脚本的使用,所有操作都是同步单条执行。redis 本身支持 pipeline 批量发送命令,能把多次 RTT 压缩成一次,但旧版实现完全没有利用这个特性。这就好比你去超市买东西,明明可以一次性把购物车推过去结账,却非要一件一件拿到收银台,效率差距可想而知。
注意:如果你现在还在用 Seata 1.3 之前的版本,并且 TC 配置的是 redis 模式,建议先别急着上生产。旧版 redis 模式在并发超过 500 TPS 后,全局事务的成功率会明显下降,这个是我在压测环境里反复验证过的。
2.2 重构的核心思路:从对象存储到结构化管理
Seata 1.3 对 redis 模式的重构,本质上是一次存储模型的重新设计。核心思路可以概括为三句话:用轻量序列化替代 JDK 序列化、用结构化 key 替代扁平 key、用批量操作替代单条操作。
先说服序列化。重构后的代码把默认序列化方式改成了 Jackson JSON,同时保留了自定义序列化器的扩展点。JSON 的好处是体积小、可读性强、跨语言兼容。一个全局事务对象用 JSON 序列化后,体积通常能压缩到 500 字节以内,比 JDK 序列化小了四倍多。而且 Jackson 的序列化速度在大多数场景下比 JDK 原生序列化快 2 到 3 倍。这个改动直接降低了 redis 的网络带宽占用和 CPU 开销。
再说 key 的设计。重构后引入了更清晰的命名空间前缀,比如SEATA_GLOBAL_、SEATA_BRANCH_、SEATA_LOCK_,并且把关联数据尽量聚合到同一个 key 下。举个例子,旧版把全局事务的状态和它的分支事务列表分开存,新版则把分支事务 ID 列表作为全局事务对象的一个字段,一起序列化存储。这样读取全局事务时,一次就能拿到所有关联信息,减少了 key 的查找次数。
最后说批量操作。重构后的RedisTransactionStoreManager在多个关键路径上引入了 pipeline。比如全局事务提交时,需要同时更新全局事务状态、删除分支事务记录、释放全局锁,这三个操作被打包成一个 pipeline 批量发送。虽然 redis 的 pipeline 不是原子操作,但在 TC 的单线程模型下,这种批量发送已经足够安全,而且能把三次 RTT 压缩成一次。
2.3 重构前后的架构对比
为了让你更直观地理解这次重构的力度,我把新旧两版的核心差异整理成了下面这张表。这张表里的数据是我通过阅读 Seata 1.2 和 1.3 的源码,结合压测结果总结出来的,不是官方文档的照搬。
| 对比维度 | Seata 1.2 及之前 | Seata 1.3 重构后 |
|---|---|---|
| 默认序列化方式 | JDK 原生序列化 | Jackson JSON |
| 单对象平均体积 | 约 2KB | 约 500B |
| 全局事务提交命令数 | 20+ 次 | 6-8 次 |
| 是否使用 pipeline | 否 | 是,关键路径批量发送 |
| key 命名空间 | 扁平,无统一前缀 | 结构化前缀,分类清晰 |
| 全局锁存储 | 独立 key,逐个操作 | 聚合存储,批量读写 |
| 扩展性 | 序列化器扩展困难 | 支持自定义序列化器 |
从这张表能看出来,重构不是小修小补,而是从存储模型到操作模式的全面升级。序列化方式的改变降低了单次操作的成本,key 结构的优化减少了操作次数,pipeline 的引入压缩了网络往返。这三个改动叠加在一起,才带来了后面压测中看到的性能跃升。
3. 核心细节解析与实操要点
3.1 序列化器的选择与配置
Seata 1.3 的 redis 模式默认使用 Jackson 序列化,但如果你有特殊需求,比如想用 Protostuff 或者 Kryo,也可以通过实现Serializer接口来扩展。我个人的建议是,除非你的团队已经有成熟的序列化方案,否则直接用默认的 Jackson 就行,没必要为了那一点点性能提升去引入额外的依赖和复杂度。
配置序列化器的方式很简单,在 TC 的配置文件registry.conf或者file.conf里指定即可。不过要注意,Seata 1.3 的配置项命名和之前版本有些差异,旧版的store.redis.serializer在新版里变成了store.redis.serializerType。这个细节官方文档没写清楚,我当时升级的时候被坑了半天,日志里只报序列化失败,没提示配置项名字错了。
store { mode = "redis" redis { host = "127.0.0.1" port = 6379 password = "your_password" database = 0 minConn = 1 maxConn = 10 queryLimit = 100 maxTotal = 100 serializerType = "jackson" } }上面这段配置里,serializerType就是指定序列化器的地方。可选值有jackson、kryo、protostuff,默认是jackson。queryLimit这个参数值得单独说一下,它控制的是单次查询返回的最大记录数,默认 100。如果你的全局事务并发很高,建议适当调大这个值,但也不要太大,否则单次响应体积过大会拖慢网络传输。
提示:切换序列化器后,redis 里已有的旧数据会无法反序列化。如果你是从旧版升级上来,务必先清空 redis 中 Seata 相关的 key,或者等所有存量事务处理完毕后再切换。
3.2 Key 结构设计与命名空间隔离
Seata 1.3 的 redis key 设计比旧版清晰很多,核心前缀有这么几个:SEATA_GLOBAL_用于全局事务,SEATA_BRANCH_用于分支事务,SEATA_LOCK_用于全局锁。每个前缀后面跟的是事务 ID 或者资源 ID,这样在 redis 里查看 key 的时候,一眼就能分辨出数据类型。
全局事务的 key 格式是SEATA_GLOBAL_{xid},value 是序列化后的GlobalSession对象。分支事务的 key 格式是SEATA_BRANCH_{branchId},value 是BranchSession对象。全局锁的 key 格式是SEATA_LOCK_{resourceId}_{tableName}_{pk},value 是锁的持有者信息。
这种结构化设计带来的好处是,你可以很方便地用SCAN命令按前缀统计各类数据的数量。比如想知道当前有多少个活跃的全局事务,执行SCAN 0 MATCH SEATA_GLOBAL_* COUNT 1000就能大致估算出来。旧版的 key 是扁平的 UUID,根本没法做这种分类统计。
不过这里有个坑要注意:SEATA_LOCK_前缀的 key 在事务提交或回滚后会被删除,但如果 TC 异常退出,这些锁可能残留。Seata 1.3 没有内置的锁清理机制,需要你自己写个定时任务去扫描超时的锁并清理。我的做法是,在 TC 启动时先扫描一遍SEATA_LOCK_*,把创建时间超过 10 分钟的锁全部删掉,避免脏锁阻塞后续事务。
3.3 Pipeline 批量操作的实现细节
Pipeline 是这次重构里对性能影响最大的改动之一。我拿全局事务提交这个场景举例,说说 pipeline 具体是怎么用的。
在旧版实现里,全局事务提交需要依次执行这些 redis 操作:读取全局事务、更新全局事务状态为 Committed、读取分支事务列表、逐个删除分支事务、逐个释放全局锁。这一套下来,至少 10 次以上的独立命令调用。
新版把这些操作重新组织了。首先,全局事务对象里直接包含了分支事务 ID 列表,不需要单独读取。其次,删除分支事务和释放全局锁的操作被合并到一个 pipeline 里批量发送。最后,更新全局事务状态和删除全局事务记录也放在同一个 pipeline 里。这样一次全局事务提交,redis 命令调用次数从 20 多次降到了 6 到 8 次。
// 伪代码示意,展示 pipeline 的使用方式 try (Jedis jedis = jedisPool.getResource()) { Pipeline pipeline = jedis.pipelined(); // 更新全局事务状态 pipeline.set(globalKey, serialize(globalSession)); // 批量删除分支事务 for (String branchKey : branchKeys) { pipeline.del(branchKey); } // 批量释放全局锁 for (String lockKey : lockKeys) { pipeline.del(lockKey); } pipeline.sync(); }上面这段伪代码展示了 pipeline 的核心用法。关键点在于pipeline.sync()之前的所有操作都只是缓存在客户端,直到sync()调用时才一次性发送给 redis。这样就只需要一次网络往返,而不是每个命令一次。
但 pipeline 不是银弹,有两个限制要注意。第一,pipeline 不保证原子性,如果中间某个命令失败,前面的命令已经执行了,不会自动回滚。在 TC 的单线程模型下这个问题不突出,但如果你自己扩展了多线程处理,就要小心。第二,pipeline 里的命令不宜过多,否则会占用大量客户端内存,而且单个 pipeline 的响应体积过大会导致网络阻塞。我的经验是,单个 pipeline 里的命令数控制在 100 以内比较稳妥。
3.4 全局锁的存储优化
全局锁是 AT 模式的核心机制,它保证了不同事务不会同时修改同一行数据。旧版的全局锁存储有两个问题:一是锁的 key 设计不合理,二是锁的查询效率低。
旧版的锁 key 是lock_{resourceId}_{tableName}_{pk},看起来没问题,但它在查询某个事务持有的所有锁时,需要遍历所有锁的 key,效率极低。新版把锁的持有者信息也编码进了 key 里,格式变成了SEATA_LOCK_{resourceId}_{tableName}_{pk},同时在全局事务对象里维护了一个锁列表。这样在释放锁的时候,直接从全局事务对象里拿到锁列表,批量删除即可,不需要遍历。
另外,新版在锁的获取路径上引入了本地缓存。TC 在内存里维护了一个LockMap,记录当前所有被持有的锁。当有新的锁请求进来时,先查本地缓存,缓存未命中再查 redis。这个改动把锁查询的 redis 调用频率降低了一个数量级。不过本地缓存带来了一致性问题,如果多个 TC 实例同时运行,本地缓存可能不一致。Seata 1.3 的解决方案是,锁的最终裁决仍然以 redis 为准,本地缓存只作为快速失败的前置判断。
注意:如果你部署了多个 TC 实例,全局锁的本地缓存会导致短暂的锁状态不一致。在极端情况下,两个事务可能同时认为自己拿到了锁。Seata 1.3 通过 redis 的
SETNX命令来保证最终只有一个事务能成功,但另一个事务会收到锁冲突异常,需要业务方做好重试。这个问题在单 TC 实例下不存在。
4. 实操过程与性能对比测试
4.1 测试环境搭建与参数配置
为了得到可信的对比数据,我搭建了一套尽可能贴近生产的测试环境。硬件方面,三台虚拟机分别部署 TC、Redis 和压测客户端,配置都是 4 核 8G,千兆内网。Redis 用的是 5.0 版本,单机模式,没有开持久化,避免 RDB 和 AOF 对性能的干扰。TC 分别用 Seata 1.2 和 Seata 1.3 各部署一套,配置除了存储模式相关的参数外,其他保持一致。
压测客户端我选的是 JMeter,因为它对分布式事务的模拟比较方便。我写了一个简单的业务场景:一个全局事务包含两个分支事务,分别操作两张不同的表,每个分支事务执行一条 UPDATE 语句。这个场景虽然简单,但足以覆盖全局事务的注册、分支注册、全局锁申请、提交、回滚等核心路径。
TC 的关键配置参数如下,这些参数在两次测试中保持一致:
server { port = 8091 maxCommitRetryTimeout = -1 maxRollbackRetryTimeout = -1 sessionHoldTimeout = 300000 } store { mode = "redis" redis { host = "192.168.1.100" port = 6379 database = 0 minConn = 10 maxConn = 100 queryLimit = 100 maxTotal = 200 } }sessionHoldTimeout设为 300000 毫秒,也就是 5 分钟,这是全局事务在 TC 里的最大保留时间。maxConn和maxTotal设得比较大,是为了避免连接池成为瓶颈。压测前我先用 100 并发跑了 5 分钟做预热,让 JIT 编译器和连接池都进入稳定状态,然后再开始正式采集数据。
4.2 压测方案设计与执行
压测方案我设计了三个维度:并发数、事务复杂度、读写比例。并发数从 100 开始,逐步加到 500、1000、2000、3000,观察不同压力下的表现。事务复杂度分两档:简单事务(2 个分支)和复杂事务(5 个分支)。读写比例方面,我模拟了 80% 提交、20% 回滚的混合场景,因为回滚路径的 redis 操作和提交路径差异很大,必须单独覆盖。
每个压力档位跑 3 分钟,采集稳定期的数据。采集的指标包括:全局事务平均耗时(从 TC 收到全局事务开始到返回结果的耗时)、TPS、P99 耗时、redis 命令调用次数、TC 的 CPU 和内存占用。为了避免单次测试的偶然性,每个档位重复跑三次,取平均值。
这里有个细节要说明:JMeter 的压测结果受客户端本身性能影响很大。我在 3000 并发时发现 JMeter 所在机器的 CPU 已经跑满了,这时候测出来的 TPS 下降其实是客户端瓶颈,不是 TC 的问题。所以我在 3000 并发档位换用了分布式压测,用两台机器同时施压,确保客户端不是瓶颈。
4.3 性能对比数据与解读
下面这张表是简单事务(2 个分支)场景下的核心对比数据。数据是三次测试的平均值,单位分别是 TPS(每秒事务数)和毫秒(耗时)。
| 并发数 | 版本 | TPS | 平均耗时 | P99 耗时 | Redis 命令数/事务 |
|---|---|---|---|---|---|
| 100 | 1.2 | 820 | 118ms | 245ms | 22 |
| 100 | 1.3 | 1450 | 68ms | 132ms | 7 |
| 500 | 1.2 | 1650 | 298ms | 680ms | 22 |
| 500 | 1.3 | 3200 | 152ms | 310ms | 7 |
| 1000 | 1.2 | 1800 | 550ms | 1200ms | 22 |
| 1000 | 1.3 | 4100 | 240ms | 520ms | 7 |
| 2000 | 1.2 | 1750 | 1140ms | 2800ms | 22 |
| 2000 | 1.3 | 4300 | 460ms | 980ms | 7 |
| 3000 | 1.2 | 1600 | 1870ms | 4500ms | 22 |
| 3000 | 1.3 | 4200 | 710ms | 1500ms | 7 |
从这张表能看出几个关键结论。第一,Seata 1.3 的 redis 模式在 TPS 上比 1.2 提升了 75% 到 160%,并发越高提升越明显。第二,平均耗时和 P99 耗时都大幅下降,3000 并发下 P99 从 4500ms 降到了 1500ms,这个改善对用户体验的影响是决定性的。第三,redis 命令数从 22 次降到了 7 次,这直接验证了 pipeline 和 key 结构优化的效果。
再看复杂事务(5 个分支)场景的数据:
| 并发数 | 版本 | TPS | 平均耗时 | P99 耗时 |
|---|---|---|---|---|
| 500 | 1.2 | 980 | 510ms | 1100ms |
| 500 | 1.3 | 2100 | 238ms | 490ms |
| 1000 | 1.2 | 1050 | 950ms | 2100ms |
| 1000 | 1.3 | 2600 | 385ms | 820ms |
| 2000 | 1.2 | 1000 | 2000ms | 4800ms |
| 2000 | 1.3 | 2700 | 740ms | 1600ms |
复杂事务场景下,1.3 的优势更加明显。因为分支越多,旧版的碎片化操作问题越突出,而新版的批量操作和聚合存储正好对症下药。2000 并发下,1.2 的 TPS 已经跌到 1000 左右,基本不可用,而 1.3 还能维持在 2700,差距接近三倍。
4.4 资源占用对比
性能提升的同时,资源占用也值得关注。我记录了 TC 进程在 1000 并发下的 CPU 和内存占用情况。
| 指标 | Seata 1.2 | Seata 1.3 |
|---|---|---|
| CPU 使用率 | 78% | 45% |
| 堆内存占用 | 1.2GB | 680MB |
| Redis 内存占用 | 320MB | 95MB |
| 网络带宽 | 45Mbps | 18Mbps |
CPU 使用率下降是因为序列化和反序列化的开销降低了,同时 redis 命令数减少也降低了网络处理的 CPU 消耗。堆内存占用下降是因为 JSON 序列化后的对象体积更小,TC 内存里缓存的会话数据也更少。Redis 内存占用从 320MB 降到 95MB,这个改善非常显著,意味着你可以用更小的 redis 实例支撑同样的并发量,直接降低了基础设施成本。
网络带宽的下降也值得一提。旧版每次操作都要传输 2KB 左右的序列化对象,新版只有 500B 左右,再加上命令数从 22 降到 7,整体网络流量下降了 60%。在跨机房部署的场景下,这个改善对延迟的降低会更明显。
5. 常见问题与排查技巧实录
5.1 升级后事务无法提交的排查
从 Seata 1.2 升级到 1.3 后,我遇到的最常见问题是事务提交失败,日志里报serialize error或者deserialize error。这个问题的根源通常是 redis 里还有旧版序列化的数据,新版用 Jackson 去反序列化 JDK 序列化的字节数组,自然就失败了。
排查思路很简单:先用redis-cli连上 redis,执行SCAN 0 MATCH SEATA_* COUNT 100看看有没有残留的旧数据。如果有,直接DEL掉对应的 key 即可。但要注意,如果这些 key 对应的是正在进行的全局事务,删除会导致事务无法提交。所以稳妥的做法是,升级前先停止所有业务流量,等存量事务全部处理完毕,再清空 redis 中的 Seata 相关 key,然后升级 TC。
提示:升级前可以用
SCAN 0 MATCH SEATA_GLOBAL_* COUNT 1000统计一下当前活跃的全局事务数量,确认归零后再操作。
5.2 全局锁释放不及时的处理
全局锁释放不及时是另一个高频问题。表现是业务侧频繁收到GlobalLockConflictException,但查 redis 发现对应的锁 key 已经不存在了。这种情况通常是 TC 的本地缓存和 redis 不一致导致的。
Seata 1.3 的本地锁缓存有一个过期时间,默认是 10 秒。如果 redis 里的锁已经释放,但本地缓存还没过期,新的锁请求就会被本地缓存拦截,误报锁冲突。解决办法是调小本地缓存的过期时间,在file.conf里把lock.retryInterval和lock.retryTimes配合调整。我的经验是把本地缓存过期时间设为 3 秒,同时把业务侧的重试次数设为 3 次,这样即使误报一次,重试也能成功。
lock { retryInterval = 10 retryTimes = 30 retryPolicyBranchRollbackOnConflict = true }上面这段配置里,retryInterval是重试间隔,单位毫秒,retryTimes是重试次数。这两个参数要配合使用,间隔太短会导致无效重试,太长会影响业务响应时间。我实测下来,间隔 10ms、重试 30 次是一个比较平衡的配置。
5.3 Redis 连接池打满的应对
高并发下 redis 连接池被打满,表现为 TC 日志里大量Could not get a resource from the pool异常。这个问题的根源通常是maxTotal设得太小,或者连接泄漏。
Seata 1.3 的 redis 连接池参数在store.redis下面配置。minConn是最小连接数,maxConn是最大连接数,maxTotal是连接池的总容量。这三个参数的关系是minConn <= maxConn <= maxTotal。我的建议是,maxTotal设为 TC 线程数的 1.5 倍左右。比如 TC 的server.port对应的处理线程默认是 100 个,那maxTotal设为 150 比较合适。
如果调整参数后还是频繁打满,就要检查是否有连接泄漏。Seata 1.3 的 redis 操作都用了 try-with-resources 或者显式关闭,正常情况下不会泄漏。但如果你自己扩展了RedisTransactionStoreManager,忘记关闭连接就会导致泄漏。排查方法是,在 redis 服务端执行CLIENT LIST命令,看看连接数是否持续增长不下降。
5.4 常见问题速查表
为了方便你快速定位问题,我把上面提到的和另外几个常见问题整理成了速查表。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 事务提交报序列化错误 | redis 有旧版序列化数据 | SCAN 查看 SEATA_* key | 清空旧数据后重试 |
| 频繁锁冲突但锁已释放 | 本地缓存未过期 | 检查 lock 配置 | 调小本地缓存过期时间 |
| 连接池打满 | maxTotal 太小或连接泄漏 | CLIENT LIST 查看连接数 | 调大 maxTotal,排查泄漏 |
| TPS 上不去 | pipeline 未生效 | 抓包看 redis 命令数 | 确认使用 1.3 版本 |
| 事务超时回滚 | sessionHoldTimeout 太短 | 查看 TC 日志 | 调大 sessionHoldTimeout |
| redis 内存暴涨 | 脏锁未清理 | SCAN SEATA_LOCK_* | 定时清理超时锁 |
这张表里的每一条都是我实际踩过的坑,尤其是脏锁未清理导致 redis 内存暴涨这个问题,当时排查了很久才定位到。Seata 1.3 没有内置的锁清理机制,如果你的业务有异常退出的情况,一定要自己加个定时清理任务。
6. 生产环境调优经验与扩展建议
6.1 参数调优的实战心得
在生产环境跑了三个月后,我总结出几个关键参数的调优经验。首先是sessionHoldTimeout,这个参数控制全局事务在 TC 里的最大保留时间。默认是 60000 毫秒,也就是 1 分钟。如果你的业务事务执行时间较长,比如涉及外部接口调用,建议调到 300000 毫秒。但也不能太大,否则异常事务会占用大量内存。
其次是queryLimit,这个参数控制单次查询返回的最大记录数。默认 100,在全局事务并发超过 1000 的场景下,建议调到 500。但要注意,这个值太大会导致单次响应体积过大,反而拖慢网络传输。我的经验是,queryLimit设为并发数的十分之一左右比较合适。
最后是 redis 的maxTotal,前面提过设为 TC 线程数的 1.5 倍。但如果你用的是 redis 集群模式,每个节点都要单独配置连接池,总连接数会翻倍。这时候要相应调小每个节点的maxTotal,避免总连接数超过 redis 服务端的maxclients限制。
6.2 监控指标的搭建
光有调优还不够,生产环境必须要有监控。我搭建了一套基于 Prometheus 和 Grafana 的监控体系,重点采集这几个指标:全局事务的 TPS 和平均耗时、redis 的命令调用次数和延迟、TC 的 JVM 内存和 GC 情况、全局锁的持有数量和等待队列长度。
其中全局锁的等待队列长度是最关键的指标。如果这个值持续增长,说明锁竞争激烈,要么是业务热点数据太集中,要么是锁释放不及时。我的做法是,当等待队列长度超过 100 时触发告警,超过 500 时自动扩容 TC 实例。Seata 1.3 的 TC 是无状态设计,可以水平扩展,但要注意多实例下的本地缓存一致性问题。
6.3 后续可以扩展的方向
Seata 1.3 的 redis 模式重构已经解决了大部分性能问题,但还有几个方向可以继续优化。第一个是引入 redis 集群模式的支持,目前 Seata 1.3 对 redis 集群的支持还不够完善,key 的分布和 pipeline 在集群模式下会有兼容性问题。第二个是增加更细粒度的锁控制,比如支持行级锁和表级锁的混合模式,减少锁冲突。第三个是优化序列化器,虽然 Jackson 已经比 JDK 序列化快很多,但 Protostuff 和 Kryo 在特定场景下还能再快 20% 到 30%。
我在实际使用中还发现一个小技巧:如果你的业务允许最终一致性,可以把一些非核心的分支事务配置成异步提交,这样能进一步降低全局事务的耗时。Seata 1.3 支持在分支事务注册时指定asyncCommit标志,开启后分支事务的提交不会阻塞全局事务的完成。但这个特性要慎用,因为异步提交失败后需要业务方自己补偿。
最后再分享一个排查问题的思路:当你遇到性能问题时,先用redis-cli --latency看看 redis 本身的延迟是否正常,再用MONITOR命令抓一段时间的命令流,统计各类命令的调用频率。很多时候问题不在 Seata 本身,而是 redis 的配置或者网络环境有问题。我遇到过好几次压测 TPS 上不去,最后发现是 redis 的maxmemory-policy设成了allkeys-lru,导致频繁淘汰数据,改成noeviction后就正常了。