Redis乐观锁实现与高效应用,分享并发控制技巧,提升系统性能

文章导读
Redis乐观锁是通过WATCH命令监视关键数据,配合MULTI/EXEC事务,在数据未被他人修改时完成操作,否则失败重试,从而实现高效并发控制。
📋 目录
  1. Redis乐观锁实现与高效应用,分享并发控制技巧,提升系统性能
  2. 乐观锁是什么?
  3. 在Redis里怎么实现?
  4. 如何高效应用并提升性能?
  5. 一个简单的代码示例
  6. FAQ
A A

Redis乐观锁实现与高效应用,分享并发控制技巧,提升系统性能

Redis乐观锁是通过WATCH命令监视关键数据,配合MULTI/EXEC事务,在数据未被他人修改时完成操作,否则失败重试,从而实现高效并发控制。

乐观锁是什么?

乐观锁就像一种“先相信,后核查”的做事方式。它假设在大多数时候,人们不会同时去修改同一份数据,因此它不在一开始就上锁。而是在真正要修改数据的时候,检查一下数据是否和最初看到的一样。如果一样,就说明没人动过,可以放心修改;如果不一样,说明被别人改过了,那么这次修改就放弃,等会儿再重试。这种方式特别适合读多写少、冲突不激烈的场景。

在Redis里怎么实现?

Redis提供了非常简单的工具来实现乐观锁,主要就是WATCH、MULTI和EXEC这三个命令。我来用一个简单的例子说明。假设我们有一个计数器,存储在键“my_counter”里,我们想安全地给它增加1。

第一步,用WATCH命令盯住这个键:WATCH my_counter。这告诉Redis:“帮我看着点my_counter,如果别人改了它,记得告诉我。”

第二步,读取这个键当前的值,比如是10。第三步,开启一个事务块:MULTI

第四步,在事务里写下你要做的操作:INCR my_counter。这个命令现在不会立刻执行,只是被放进了队列。

第五步,尝试提交事务:EXEC。这是最关键的一步。Redis在执行EXEC时,会去检查所有被WATCH的键(这里就是my_counter)从WATCH之后到现在这个时刻,有没有被其他客户端修改过。如果没有,太好了,事务里的命令(INCR my_counter)会顺利执行,计数器变成11。如果发现my_counter已经被其他客户端改动了(比如变成了20),那么整个事务立刻失败,EXEC会返回一个空值(nil),表示“你的操作被拒绝了,因为数据变了”。

第六步,检查EXEC的返回结果。如果是空值,说明失败了,我们就需要重新开始整个流程(重试)。如果成功了,就继续后续逻辑。

如何高效应用并提升性能?

直接用WATCH/MULTI/EXEC虽然简单,但有时候会影响性能。如果冲突频繁,重试次数太多,体验就不好。这里分享几个小技巧:

第一,缩小WATCH范围。只WATCH最核心、最可能被改的那个键,而不是一大堆相关键。这可以减少事务因无关改动而失败的概率。

Redis乐观锁实现与高效应用,分享并发控制技巧,提升系统性能

第二,设计重试策略。失败后不要无脑马上重试,可以等待一小段时间(比如几十毫秒),或者限制最大重试次数,避免无限循环。

第三,结合Lua脚本。Redis允许你把一系列命令写成一个Lua脚本,服务器会原子性地执行整个脚本。脚本在执行过程中,数据不会被其他命令打断,这天然就是一种更高效的“锁”。对于复杂的更新逻辑,用Lua脚本比用WATCH事务往往更快、更可靠。比如,你可以把读取键值、判断条件、执行增减的操作全部写在一个脚本里,然后用EVAL命令一次执行。

第四,理解适用场景。乐观锁不是万金油。如果你的业务场景是写操作非常密集,冲突是常态,那么乐观锁会带来大量的失败重试,反而不如用更严格的锁(比如分布式锁)直接控制。它最适合那些大部分时间是读取,偶尔有更新的场景,比如库存扣减(在秒杀初期,大量是检查库存的操作,真正下单扣减时再去乐观更新)、用户积分累加等。

一个简单的代码示例

下面用一段伪代码演示一下完整流程,你可以用你熟悉的编程语言(如Python、Java)的Redis客户端来实现它:
1. 连接Redis服务器。
2. 设置最大重试次数,比如5次。
3. 循环开始重试:
a. 使用 `WATCH key` 监视目标键。
b. 读取键的当前值 old_value。
c. 开始事务 `MULTI`。
d. 在事务中设置新值,例如 `SET key new_value`(根据old_value计算而来)。
e. 执行事务 `EXEC`。
f. 如果EXEC返回的结果不是nil,说明成功,跳出循环。
g. 如果EXEC返回nil,说明失败,重试次数减一;如果次数用完则报错退出,否则等待片刻后继续循环。

FAQ

Q1: Redis乐观锁和数据库里的乐观锁有什么区别?
A1: 核心思想完全一样,都是“先验后改”。区别在于实现工具。数据库乐观锁通常依赖数据表里的一个版本号字段(version)或时间戳,每次更新时比较这个字段。而Redis乐观锁依赖的是WATCH命令对键值本身的监视,以及事务的原子性检查。Redis的实现更加轻量和直接。

Q2: 为什么有时候用WATCH事务会失败,换成Lua脚本就成功了?
A2: 这是因为WATCH事务在EXEC执行前的准备阶段(读取值、开启事务)和最终提交阶段之间,存在一个时间窗口。如果这个窗口期很长(比如你的业务逻辑很复杂),其他客户端修改数据的风险就很高。而Lua脚本在Redis服务器上是作为一个整体、原子性执行的,脚本执行期间不会穿插任何其他命令,所以不存在这个时间窗口,自然就避免了大部分的失败冲突。

Q3: 用乐观锁做秒杀库存扣减,如果一直失败重试,用户会不会等很久?
A3: 这是个很实际的问题。单纯依赖客户端重试,在高并发秒杀下确实可能导致用户体验不佳(一直转圈)。常见的优化方案是结合“预扣减”和“异步处理”。例如,可以先用Redis的原子命令(如DECR)快速扣减一个内存中的库存,如果成功,立即返回用户“抢购排队中”,然后把真正的订单创建和库存持久化等复杂操作放到消息队列里异步慢慢处理。这样前端响应极快,系统的并发能力也大大提升。

引用来源:本文内容基于Redis官方文档关于事务(Transactions)和WATCH命令的说明,以及常见的分布式系统并发控制实践。具体可参考:https://redis.io/docs/latest/develop/interact/transactions/