ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Redis事务深度拆解:从原子性真相到秒杀场景实战

Redis事务深度拆解:从原子性真相到秒杀场景实战 1. 我为什么专门写一篇Redis事务的文章先抛一个观点Redis事务可能是整个Redis生态里“被误解最深”的一个特性。很多人面试前背了几条命令知道MULTI、EXEC、DISCARD、WATCH这几个单词但真正被问到“Redis事务能保证原子性吗”的时候往往答不到点子上更别提实际项目里用Redis事务解决具体问题了。我写这篇东西的动机很简单最近在帮团队做缓存一致性治理顺手把几个订单场景的分布式事务方案重新捋了一遍发现Redis事务在一部分场景下其实是比分布式锁更轻量的选择。但前提是你得真正理解它的边界哪些它能做哪些它绝对做不了。这篇文章不打算给你画大饼也不打算把官方文档翻译一遍而是把我自己从踩坑、看源码、做压测里总结出来的东西讲明白。内容从基础命令开始逐步到本质分析、实际场景、常见面试追问最后是一份可以直接抄的实操清单。适合三类人看准备面试的Java/PHP/Go后端开发正在做缓存与数据库一致性设计的架构师以及那些被“Redis事务就是个鸡肋”这种论调误导过的朋友。看完你应该能回答清楚Redis事务是不是事务它和MySQL事务的差别到底在哪里哪些业务场景真的应该用它2. 从命令层面理解Redis事务的完整执行流程2.1 四条核心命令的职责划分Redis事务相关的命令一共就五条最常用的是前四条MULTI开启事务标记当前连接进入事务状态EXEC执行事务队列中的所有命令DISCARD取消事务清空命令队列WATCH乐观锁监视一个或多个key在EXEC之前key被修改则事务被中断有一个很容易被忽略但非常关键的命令UNWATCH用于取消所有WATCH监听。一个典型的事务执行流程是这样的 MULTI OK SET order:1001 status paid QUEUED INCR order_count QUEUED EXEC 1) OK 2) (integer) 101这里有个细节值得注意从MULTI开始所有的命令都不会立即执行而是进入一个队列每个命令返回QUEUED。直到EXEC被调用Redis才会按顺序、一次性执行队列里的所有命令。这个“排队”机制是理解Redis事务的起点也是和MySQL事务最大的分水岭MySQL事务里的每条SQL在执行时就会加锁、修改undo日志而Redis事务里命令真正执行的时刻被推迟到了EXEC。2.2 事务执行的三阶段拆解官方对Redis事务的定义是三阶段事务开始MULTI命令把连接上下文标记为事务状态命令入队后续命令全部进入队列此时服务端只做语法检查不执行业务逻辑执行事务EXEC被调用后服务端按先进先出的顺序逐个执行队列里的命令第一阶段和第三阶段很好理解重点说第二阶段。命令入队时Redis只做两类检查命令是否存在、参数个数是否正确。至于key存不存在、类型对不对、命令执行会不会报错全部留到EXEC阶段才会暴露出来。这个特性导致了Redis事务里一个著名的坑如果某个命令在入队时没有语法错误但在执行时发现操作了错误类型的数据结构它不会影响其他命令的正常执行。我可以给你演示这个场景 MULTI OK SET name zhangsan QUEUED LPUSH name list-data QUEUED INCR age QUEUED EXEC 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) (integer) 1看到没有LPUSH执行失败了但SET和INCR照样执行成功。这说明Redis事务不具备回滚机制更没有“要么全成功、要么全失败”的原子性保障。这一点必须刻在脑子里面试问“Redis事务满足原子性吗”正确答案是不满足传统意义上的原子性它只保证执行过程的隔离性以及命令序列的批量执行。3. 深度拆解Redis事务的本质它到底是什么不是什么3.1 原子性的真相没有回滚只有中断MySQL事务失败时可以ROLLBACK把数据恢复到事务开始前的状态。Redis事务呢它把所有命令执行完之后根本没有undo log没有MVCC没有回滚段。如果执行过程中某条命令报错Redis会继续执行后面的命令已经执行成功的命令不会被撤销。那事务里主动判断逻辑错误怎么办比如两个命令之间有依赖关系前面命令成功了才允许后面命令执行。Redis给出的是DISCARD命令——但这个DISCARD只能在你还没EXEC的时候手动调用用来放弃整个队列一旦进入EXEC你就不可能干预执行过程无法在中间某条命令出错时让后面的命令停止执行。用一句话总结Redis事务只有“执行前的中断”没有“执行后的回滚”。WATCH机制本质上也是在EXEC之前依靠key的版本变化来中断事务而不是在执行之后去恢复现场。理解了这一点你才算摸到了Redis事务的真正边界。3.2 隔离性真相单线程模型下的天然串行Redis是单线程模型所有命令都是串行执行的。这带来一个直接结论在EXEC执行期间不会有其他客户端的命令插入进来。所以Redis事务的隔离性其实是“单线程串行”带来的副产品不需要复杂的锁机制也不需要MVCC。但这个隔离性有个前提只针对Redis自身。事务执行期间如果有其他客户端向同一个key发起了写操作那这个写操作是等EXEC整体执行完才会被处理还是可能在事务执行过程中的某个间隙被插入答案是没有间隙。Redis服务端在处理EXEC时会一次性把队列里所有命令顺序执行完毕中间不会去处理网络上的新请求。这是Redis事务隔离性的核心保证也解释了为什么WATCH需要在事务之外单独工作——它是在EXEC执行前对key进行监视而不是在事务执行过程中加锁。3.3 与MySQL事务、分布式事务的边界对照为了把Redis事务的本质讲透我把它和MySQL事务、以及外部常见的分布式事务方案做了一张对照表维度MySQL事务Redis事务分布式事务如2PC/TCC原子性支持回滚保证全成或全败不保证某条命令失败不影响其他命令通过协调者与参与者协议保证隔离性支持四种隔离级别间隙锁、行锁单线程串行天然隔离需要分布式锁或额外隔离机制持久性依赖redo log可配置依赖AOF/RDB受持久化配置影响依赖各参与节点的持久化能力适用场景强一致、结构化数据、复杂关联操作轻量级、低延迟、操作简单的缓存或计数场景跨库、跨服务的强一致业务性能成本锁竞争、日志刷盘开销明显无锁开销批量执行极快网络交互多性能损耗大看完这张表你应该明白Redis事务不是一个“弱化版MySQL事务”而是一个设计目标完全不同的机制。它牺牲了原子性和持久性换来了极致的性能与简洁的执行模型。所以与其纠结“Redis事务能不能替代MySQL事务”不如问自己我的场景需要回滚吗需要跨多个key保证数据强一致吗如果答案是需要那Redis事务不是你的菜如果只是需要“一次性执行一串命令并且不想被其他客户端的操作插队”那它可能正好够用。3.4 Redis事务写操作的底层实现细节Redis事务之所以能“排队”靠的是客户端状态机。在Redis源码里每个redisClient结构体较新版本为client有一个flags字段其中包含了CLIENT_MULTI标志位。当客户端发送MULTI时服务端把这个标志位置为1之后的每条命令服务端会调用queueMultiCommand方法把命令追加到一个链表形式的c-mstate.commands数组里。有个很体现设计精妙的地方命令入队时Redis会提前解析命令参数并检查命令合法性这样EXEC执行的时候就不需要重复解析命令了。这个优化让事务的执行速度非常快也是它能在高性能场景下被广泛使用的基础。我当年在看源码时注意到一个有意思的细节MULTI之后如果执行DISCARD服务端会清空mstate里的命令队列并释放相关内存此时如果之前设置过WATCH事务中断后WATCH还在生效吗答案是WATCH会被保留除非你显式调用UNWATCH。这是很多人忽略的细节容易导致后续事务被意外中断。4. 实际应用场景Redis事务真正能解决的问题4.1 场景一秒杀和库存扣减事务比分布式锁更优雅很多人提到库存扣减第一时间想到分布式锁但分布式锁有锁的获取、释放、超时、重入等一堆问题而且在高并发下性能开销不小。如果只涉及单key的原子扣减根本不需要锁用INCR/DECR这种原子操作就够了但如果涉及多key的一致性比如“扣减库存生成订单号记录操作日志”这三个步骤需要一起完成且不允许被其他线程插队Redis事务就是非常合适的选手。举个例子秒杀场景里的典型操作WATCH stock:sku001 MULTI DECR stock:sku001 INCR order:total LPUSH order:list user:1001 EXEC在WATCH的帮助下如果事务执行前stock:sku001被其他请求修改了EXEC会返回nil而不是执行队列里的命令从而避免超卖。这套逻辑比分布式锁轻量不需要引入额外组件也不需要考虑锁超时续期问题在单Redis实例场景下是一个极其高效的方案。这里我必须强调一个前提上面这个方案要求所有操作都命中同一个Redis实例。如果你用的是Redis Cluster多个key不在同一个slot上事务就会报CROSSSLOT错误。解决办法是使用Hash Tag比如把key设计成{stock:sku001}:stock、{stock:sku001}:order让它们落在同一个slot中。4.2 场景二批量命令执行减少网络往返Redis事务的另一个天然优势是减少RTT往返时延。假设你要执行五条命令正常逐条执行需要5个网络往返而用事务封装后只需要2个往返MULTI一个EXEC一个。在局域网环境下可能差别不明显但在跨机房、跨云的场景下每条命令的RTT可能达到几十毫秒这时事务的性能优势就会被放大。我在实际项目里做过一次优化某个接口需要同时更新用户的积分、等级、最近活跃时间原来是用Pipeline后来因为需要保证这批操作不被其他命令插队改成了事务。最终效果是接口耗时降低了近四成而且因为事务是串行执行的省去了Pipeline模式下回包顺序的顾虑。4.3 场景三Redis做中间件时的命令编排比如延迟队列和限流现在很多团队把Redis当作轻量中间件使用比如用ZSet实现延迟队列、用Lua脚本实现令牌桶限流。在这些场景里Redis事务可以作为保证多命令一致性的基线方案。比如延迟队列的消费逻辑从ZSet取出到期的任务记录到执行日志中从ZSet删除该任务这三个动作如果分步执行在并发消费时可能出现同一个任务被多个消费者拿到用事务把ZRANGEBYSCORE、LPUSH、ZREM包在一起配合WATCH能够显著降低重复消费的概率。不过要提醒一句如果业务逻辑比较复杂或者条件判断比较多我通常建议优先考虑Lua脚本。因为Lua脚本在Redis里是原子执行的不仅支持条件逻辑还能在脚本内做控制流判断比事务更灵活。你可以理解为事务是“一串无脑执行的命令队列”Lua脚本是“带逻辑判断的原子执行块”。等会儿在面试题部分我会再展开这两者的对比。4.4 场景四非强一致场景下的订单与库存状态更新热搜词里有“订单与库存分布式事务”很多文章动辄就上Seata、RocketMQ事务消息但说实话对于规模不大、允许秒级最终一致的业务Redis事务完全可以作为轻量方案。举一个实际的电商例子用户下单后需要扣减库存、更新订单状态、写一条待支付消息到延迟队列。如果这些操作分散在MySQL和Redis中就会面临分布式事务难题。一种讨巧的设计是把订单状态和库存状态都维护在Redis中作为热点数据的缓存或预扣存储通过Redis事务保证这三个key的更新原子执行之后再由异步任务把Redis结果同步到MySQL。在这个设计中Redis事务承担了“短暂期间的强一致”职责避免了在支付前窗口出现超卖或状态不一致。当然如果MySQL已经是最终数据源且Redis只做缓存那需要考虑缓存与数据库的双写一致性这种情况Redis事务解决不了需要用其他策略比如延迟双删、Binlog订阅同步等。别把工具用错地方。5. 实操落地完整可复现的Redis事务代码示例5.1 环境准备本地快速搭建Redis无论你是Windows还是macOS先确保有一个可以连的Redis实例。Windows上最简单的方式是用memurai或者官方的redis-windows分支这些现在都支持Windows原生运行不一定要用WSLmacOS用户直接用Homebrewbrew install redis redis-server /usr/local/etc/redis.conf基础安装配置完成后我建议先用redis-cli把今天讲的事务命令各跑一遍养成肌肉记忆。比如redis-cli MULTI SET user:001:score 90 INCR user:001:score EXEC如果返回结果里第二项是(integer) 91说明你的环境没问题可以继续后面的代码。5.2 Python实操库存扣减的完整事务函数我用Pythonredis-py写一个库存扣减的示例这是生产环境里最典型的用法import redis client redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) STOCK_KEY stock:sku001 ORDER_KEY order:total LIST_KEY order:list def stock_deduct_with_transaction(user_id: str): while True: try: # 1. 开启WATCH监视库存key client.watch(STOCK_KEY) # 2. 读取当前库存 stock int(client.get(STOCK_KEY) or 0) if stock 0: client.unwatch() return False # 3. 开启事务执行扣减和记录 pipe client.pipeline(transactionTrue) pipe.decr(STOCK_KEY) pipe.incr(ORDER_KEY) pipe.lpush(LIST_KEY, f{user_id}:{stock}) # 4. 执行事务这里在redis-py里会调用EXEC pipe.execute() return True except redis.WatchError: # 如果WATCH的key在事务执行前被修改会抛出WatchError # 最简单的策略是重试或者记录冲突次数后重试 continue这段代码里有几个细节值得说明。第一watch()必须在pipeline(transactionTrue)之前调用否则监视不生效。第二get之后到execute之间如果另一个客户端修改了STOCK_KEY服务端会让本次EXEC返回空redis-py会抛出WatchError我们捕获后重试即可。第三返回的stock是事务开始前读到的值用在了日志记录里这保证了日志里的库存和扣减前的库存是一致的。实际压测过这个函数在普通笔记本上可以跑到每秒数万次远高于分布式锁方案的吞吐量。当然这是单实例、无持久化压力的前提生产环境还要看网络和AOF策略。5.3 Java实操使用Spring Data Redis操作事务服务端开发里Java占有率很高我再给一个Spring Data Redis的写法。注意Spring Data Redis操作事务需要把连接绑定到线程否则多个方法拿到的不是同一个连接。先写一个简单的Service方法Service public class OrderService { Autowired private StringRedisTemplate stringRedisTemplate; public boolean createOrderWithRedisTx(String userId, String skuId) { return stringRedisTemplate.execute(new SessionCallbackListObject() { Override public ListObject execute(Nonnull RedisOperations operations) throws DataAccessException { operations.watch(stock: skuId); Integer stock Integer.valueOf(operations.opsForValue().get(stock: skuId)); if (stock 0) { operations.unwatch(); return null; } operations.multi(); operations.opsForValue().decrement(stock: skuId); operations.opsForValue().increment(order:total); operations.opsForList().leftPush(order:list, userId : skuId); return operations.exec(); } }); } }用SessionCallback的好处是整个回调在同一个Redis连接里执行multi()和exec()天然配对。如果WATCH的key被修改exec()返回null你可以据此判断是否要重试。有一点需要特别注意StringRedisTemplate的序列化器默认是StringRedisSerializer如果你用RedisTemplate且设置了其他序列化器要保证key的序列化方式一致否则watch和后续get操作的key可能映射到不同的字节数组导致监视失效。这是我在项目里踩过的真实坑当时排查了半天最后发现是key的序列化前缀不一致。5.4 Go实操使用go-redis实现事务控制Go生态里go-redis对事务的支持也很完善核心是用TxPipeline。import ( context github.com/redis/go-redis/v9 ) func DeductStock(ctx context.Context, rdb *redis.Client, userID, skuID string) (bool, error) { stockKey : stock: skuID for { // 使用TxPipeline先WATCH然后排队执行 tx : rdb.TxPipeline() pipe : func(p redis.Pipeliner) error { // 在pipeline里没法直接watch需要先单独watch return nil } _ pipe // 更清晰的方式使用Watch方法 err : rdb.Watch(ctx, func(tx *redis.Tx) error { stock, err : tx.Get(ctx, stockKey).Int() if err ! nil { return err } if stock 0 { return redis.ErrOutOfStock // 自定义错误 } _, err tx.TxPipelined(ctx, func(p redis.Pipeliner) error { p.Decr(ctx, stockKey) p.Incr(ctx, order:total) p.LPush(ctx, order:list, userID:skuID) return nil }) return err }, stockKey) if err nil { return true, nil } if err redis.TxFailedErr { // 说明事务被其他客户端中断重试 continue } return false, err } }这段代码和上面的Python版思路一模一样。rdb.Watch的回调参数*redis.Tx就是一个已监视指定key的事务对象。回调内部用TxPipelined把多个命令打包然后由go-redis自动发送MULTI/EXEC。实现上TxPipelined内部会调用tx.Multi()之后执行Exec()。如果在Watch阶段发现key被改返回TxFailedErr这时候重试即可。我自己的体会是Go版写起来最顺手因为它把回调、错误处理和重试逻辑组织得很紧凑适合服务端高并发环境。6. 事务与Lua脚本、Pipeline的选型对比6.1 为什么说Lua脚本是事务的进阶替代品很多人在面试中被问到“Redis事务和Lua脚本有什么区别”时会卡住。我的理解可以浓缩为一句话事务是把一堆命令打包执行Lua脚本是把一堆逻辑打包原子执行。区别体现在两个方面。第一事务命令在执行时不能做条件判断每个命令都是“提前定好”的Lua脚本可以在内部读写Redis数据并且根据结果决定后续步骤灵活性高得多。第二事务执行过程中某条命令失败的后续行为是“继续执行其他命令”Lua脚本如果在执行过程中报错整个脚本会终止并回滚已执行的写操作其实是单线程运行导致后续命令未被执行看起来更符合“原子性”直觉。举一个具体例子我们想要“只有在库存大于0时才扣减”。用事务做你必须在外部先GET库存然后判断再MULTI、DECR用Lua做local stock tonumber(redis.call(GET, KEYS[1])) if stock 0 then return -1 end redis.call(DECR, KEYS[1]) redis.call(INCR, KEYS[2]) redis.call(LPUSH, KEYS[3], ARGV[1]) return 1这段脚本通过redis.call(GET, KEYS[1])读取库存在脚本内完成条件判断执行一次EVAL就能完成多步操作且整个过程因为Redis单线程执行Lua而不会被打断。这个“条件原子性”是事务做不到的。因此我的建议是如果事务队列里的命令彼此独立、没有依赖关系用事务就够了如果有依赖判断优先选Lua脚本。在实际的高性能生产系统里Lua脚本的使用频率远高于事务原因也在这里。6.2 Pipeline与事务的异同Pipeline和事务都能减少网络往返不同之处在于Pipeline只是把多条命令一次性发给服务端命令之间没有原子隔离也没有WATCH机制事务则要求MULTI/EXEC包裹命令在服务端排队并串行执行。有一个细节值得注意在Pipeline里加MULTI/EXEC就变成了“Pipeline 事务”。很多客户端库的pipeline(transactionTrue)就是这个原理。如果你只需要批量操作且允许中间被其他客户端插入用纯Pipeline性能可能略高少了事务状态机的开销如果你需要“不可插队”的语义那就用事务。7. 常见问题与排查技巧实录7.1 为什么我用了事务并发时数据还是错了这是我在技术群里被问得最多的问题。看完前面的分析你应该有感觉了八成是因为没加WATCH或者WATCH的key不对。事务只能保证队列内的命令不被其他请求插队但如果你在MULTI之前用GET读了值在MULTI之后根据这个值做DECR那么这个“读到的值”在MULTI到EXEC之间可能已经过期了。正确做法是先WATCH目标key再GET再MULTI、EXEC这样当key在期间被修改时EXEC会返回nil。简单来说事务给不了你“Read Your Write”的快照隔离WATCH才是乐观锁的核心。我见过不少初学者用事务做乐观锁但忘了WATCH最后高并发下出现超卖又反过来怪Redis事务设计不行。工具本身没有问题问题是使用姿势不对。7.2 事务里的命令为什么返回QUEUED然后什么都不执行这种情况通常是你在一个已经处于事务状态的连接里又发送了MULTI或者因为异常退出导致连接处于悬挂状态。解决方案很简单发送DISCARD重置连接状态或者直接重连。还有一次我在Java里遇到了诡异的“事务里只有一部分命令执行了”查了半天发现是连接池中的redisTemplate被其他线程复用事务命令被分散到了多个连接上。所以说用Spring Data Redis操作事务务必使用SessionCallback就是这个原因。另一个常见情况Redis Cluster下事务执行报CROSSSLOT错误。这是cluster模式对多key操作的天然限制。解决办法是把相关key规划到同一个hash slot中或者改用Lua脚本Lua脚本同样受slot限制但可以通过Hash Tag解决。7.3 Redis事务能不能解决缓存一致性我再强调一遍不能。Redis事务解决的是“多个Redis命令之间的原子执行与隔离”不是“Redis与MySQL之间的数据一致性”。如果你缓存里放了数据库的快照然后通过事务更新了多个缓存key这只能保证缓存内部一致无法保证缓存和数据库一致。热点搜索词里的“订单与库存分布式事务”如果要彻底解决跨Redis和MySQL的一致性问题要么用可靠消息要么用本地消息表要么用Seata等方案而不是指望用Redis事务做完一切。我自己的一个项目实施原则是“跨数据源的问题不要在单一数据源内部求解”否则只会制造出更复杂的脏读、延迟和补偿逻辑。7.4 事务执行期间AOF刷盘问题会影响一致性吗很多人问Redis事务执行完了但AOF没刷盘进程崩溃了事务结果会不会丢这个问题的本质是Redis持久化的配置策略和其他写操作一样。默认appendfsync everysec下AOF最多丢一秒数据。事务不会额外保证持久性也不提供比普通命令更高的持久化语义。如果你的业务对数据安全要求高需要结合Redis主从集群、AOF刷盘策略、哨兵/Cluster的故障切换机制来综合设计。7.5 为什么我的WATCH一执行EXEC就返回nil有几种可能WATCH的key确实在事务执行前被修改了最常见同一个连接多次使用WATCH且key被改动后没有UNWATCH使用了Clusterkey的监视在不同的节点上Cluster模式不支持跨slot的WATCH客户端库在multi()之前自动发送了watch()和你的显式watch产生冲突排查办法很简单在WATCH之后、MULTI之前用CLIENT LIST看连接状态或者在命令之间加GET确认key的版本。实在不行先UNWATCH再重新WATCH。8. 面试题深度回答模板你能拿分的三个层次关于Redis事务的面试题网上随便一翻就是一大把。我见过很多候选人背答案但真正能拿到高分的是能够区分层次回答的人。这里给出一套我自己的答题框架。第一层基础概念层。回答Redis事务是什么四个命令的作用执行流程三段论。这层最多拿及格分。第二层原理对比层。主动说出Redis事务与MySQL事务在原子性、隔离性上的本质差异举一个失效场景说明事务执行中命令失败不会回滚。这层能体现出你真的用过而不是只会背八股。第三层场景与取舍层。结合项目实际说明什么时候用Redis事务什么时候用Lua脚本什么时候该用分布式锁以及Redis Cluster对多key事务的限制。如果能顺带讲出你在Spring Data Redis里遇到的连接复用问题和解决过程面试官大概率会在心里给你加一分。下面我整理了一份面试官高频追问的参考回答追问推荐回答方向Redis事务保证原子性吗不保证传统原子性没有回滚机制命令错误不影响其他命令Redis事务如何解决并发竞争用WATCH做乐观锁检测key在事务执行前是否被修改WATCH和分布式锁哪个好场景不同乐观锁适合短操作、冲突不频繁分布式锁适合临界区需要长时间持有的场景MULTI执行失败还能重试吗可以但要重看业务逻辑如果部分命令已执行重试前可能需要补偿Redis事务和Lua脚本怎么选无依赖用事务有判断逻辑用LuaLua可脚本内条件与循环控制更精细Cluster下怎么用事务用Hash Tag把key约束到同一slot或者放弃事务改用Lua同样需同slot9. 我的几点实战心得与小技巧最后分享几个书本之外的体会。第一生产环境中我很少单独用Redis事务来做复杂的库存扣减至于为什么前面已经说过了缺少回滚和条件判断Lua脚本能做得更好。但这并不代表Redis事务没有价值它在批量操作、轻量级一致性场景下的简洁性和性能优势非常突出尤其是跨命令的“不可插队”能力在计数统计、日志聚合里很实用。第二几乎所有主流Redis客户端库对事务的支持都已经很成熟但引入事务时要特别注意连接绑定问题。在Spring中务必使用SessionCallback或RedisTemplate.execute在Python中务必使用同一个Pipeline对象。如果你在事务里发现命令时灵时不灵先怀疑连接是否被复用再怀疑key的序列化是否一致这两个问题占了八成故障原因。第三如果你们团队已经在用Redis Cluster那么多key事务基本是用不了的。我的替代方案是优先用Hash Tag解决slot限制如果业务关系复杂就改用Lua脚本并在脚本内做数据校验如果你需要跨多个Redis分片甚至跨数据库的强一致坦白说Redis事务就不是正确选项了去研究可靠消息或Seata这类方案吧。还有一个小技巧在压测的时候不要只看吞吐量要关注事务的排队长度。当你的大量并发请求都带着MULTI/EXEC压向Redis时单线程串行会导致命令在队列中堆积延迟会线性上升。所以事务适合偶尔抢购、批量操作这种中低频场景不适合所有请求都走事务的超高频路径。如果让我给一条最核心的建议那就是把Redis事务当作一个“批量执行乐观锁”的组合工具而不是当作一个“数据库事务”的替代品。调整了预期之后你会发现它在很多场景里都能用得恰到好处。最后的最后再留一个扩展思考如果你想要在Redis事务之上做最终一致可以怎么结合我自己的方向是“事务Lua异步对账”把事务里的多个key视为一个短期状态由异步任务扫描并校验一致性发现问题再补偿。这个思路在很多缓存治理项目里都能落地。后续如果想聊我可以专门写一篇“基于Redis实现轻量级最终一致性方案”的实战文章。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表