用Redis怎么确保查询结果同步,查询时数据不乱的那些事儿
- 问答
- 2026-01-01 17:19:28
- 1
主要基于对Redis官方文档、常见技术博客和实践社区讨论的归纳)
要搞清楚Redis怎么确保查询数据不乱,首先得明白为什么会“乱”,这个“乱”通常不是指Redis服务器自己把数据搞错了,而是指在多个客户端(也就是多个程序或者多个线程)同时跟Redis打交道的时候,由于操作顺序的问题,出现了我们意料之外的情况,比如经典的超卖问题:一件库存为1的商品,两个用户同时查询都看到还有库存,然后都下单成功,结果库存变成了负数,这就是数据“乱”了。
核心问题在于如何管理“并发”,也就是处理多个操作同时发生的顺序和时机,Redis本身是单线程执行命令的,这意味着在Redis服务器内部,它一次只处理一个命令,这个命令执行完了,才会处理下一个,这保证了单个命令的原子性,比如你执行一个SET命令或者一个HGET命令,这个命令在执行过程中是不会被其他命令打断的,所以你不会读到半个命令写进去的脏数据,但这并不能解决上面提到的超卖问题,因为“查询库存”和“减少库存”是两个独立的命令,在它们执行之间,完全有可能被其他客户端的命令插进来。
为了解决这个问题,让一系列操作能像一个大命令一样不被中断,Redis提供了几种主要的机制。
第一个重要的机制是“事务”(Transaction)。
Redis的事务跟我们平时理解的关系型数据库(比如MySQL)的事务不太一样,在Redis中,事务的本质是把多个命令打包,然后按顺序一次性执行,它通过MULTI命令开始,然后输入一系列命令,最后用EXEC命令来执行。
它的关键特点是:

- 顺序性:所有在
MULTI和EXEC之间的命令会按照发出的顺序被放入一个队列。 - 一次性执行:当服务器收到
EXEC命令时,它会依次执行队列中的所有命令,在执行EXEC的期间,不会被其他客户端的命令请求打断。
这解决了什么问题呢?它保证了这一批命令执行时,中间不会有“别人”的操作插队,回到超卖的例子,我们可以把“查询库存”和“减少库存”两个命令放到一个事务里,但这里有个细节,在Redis事务中,你无法在MULTI里面直接根据一个命令的结果来决定下一个命令的执行,因为那些命令只是被排队,并没有真正执行,所以通常的做法是,我们先用一个命令(在MULTI之外)查询库存,如果库存大于0,我们再开启一个事务,这个事务里包含一个DECR(减少1)命令,但这样还是没完全解决并发问题,因为“查询”和“开启事务”这两个动作之间还是有空隙。
Redis事务更适用于你明确知道要执行哪几个命令,并且不依赖于中间查询结果的场景,它主要提供了命令的批量执行和隔离性,但缺乏真正的“原子性”保障(因为失败不会回滚所有命令,Redis事务不支持回滚)和解决并发冲突的能力。
第二个,也是更强大的机制,是“乐观锁”(Optimistic Locking),Redis里通过WATCH命令来实现。
WATCH命令才是解决我们最开始那个超卖问题的关键,它可以监视一个或多个键(key),如果在执行EXEC命令之前,被监视的键被其他客户端修改了,那么当前客户端的事务将会被打断,EXEC命令会返回空值,表示事务执行失败。
这个过程很像我们生活中的乐观锁:我先假设在我操作之前没人会改这个数据,所以我先不加锁,大胆地去读数据,但在我要更新数据的时候,我会检查一下这期间数据是不是被别人动过,如果没动过,我就更新成功;如果动过了,我就放弃这次操作,可能选择重试或者报错。

具体到超卖的例子,正确的做法是:
- 客户端A使用
WATCH命令监视库存的key,比如stock:product_001。 - 客户端A读取当前库存值,假设是1。
- 客户端A开启事务(
MULTI)。 - 客户端A在事务中发送减少库存的命令(
DECR stock:product_001)。 - 客户端A执行事务(
EXEC)。
如果在第2步之后、第5步之前,没有任何其他客户端修改过stock:product_001,那么事务会成功执行,库存减为0。
但如果在这个时候,客户端B抢先一步修改了库存(比如也执行了一个DECR命令,把库存减到了0),那么当客户端A执行EXEC时,Redis会发现它监视的key已经被修改了,于是客户端A的事务就会失败,客户端A可以选择重新从头开始整个流程(监视、读取、判断、打包命令、执行),这就是“重试”机制。
通过WATCH+事务+重试,我们就能确保在高并发下,对同一个数据的更新不会混乱,最终结果是对的。
第三个机制是“Lua脚本”(Lua Scripting)。

这是确保数据同步和一致性的“终极武器”,Redis允许你将复杂的多个操作写成一个Lua脚本,然后一次性发送给服务器执行。
Lua脚本有什么好处?
- 原子性:整个Lua脚本在执行时会被当作一个独立的命令,在脚本执行期间,Redis服务器不会处理任何其他命令,这比事务的隔离性更强,是真正的、绝对的原子性。
- 减少网络开销:多个操作在一个脚本中完成,只需要一次网络通信。
- 复杂性封装:可以在脚本内实现复杂的逻辑,比如条件判断、循环等,并且能直接使用中间计算结果。
还用超卖举例,我们可以写一个Lua脚本,这个脚本的内容是:
local stock = redis.call('GET', KEYS[1])
if stock and tonumber(stock) > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 表示成功
else
return 0 -- 表示失败
end
然后把这个脚本一次性发送给Redis执行,因为脚本是原子执行的,所以在它检查库存和减少库存的中间,绝对不可能有其他的DECR命令插进来,完美地解决了并发问题,在实际应用中,通常会使用SCRIPT LOAD命令将脚本预加载到Redis,然后用EVALSHA来执行,以提高效率。
要让Redis查询结果同步、数据不乱,核心是对并发操作进行控制。
- 简单批量操作,用事务(MULTI/EXEC),它能保证一批命令不被干扰地顺序执行。
- 需要基于查询结果进行更新,且存在并发竞争,用乐观锁(WATCH) 配合事务和重试机制,这是应对并发修改的经典模式。
- 复杂逻辑且要求极高一致性,用Lua脚本,它提供了最强的原子性保证,是解决复杂并发问题的首选方案。
选择哪种方式,取决于你业务场景的具体需求和复杂程度。
本文由水靖荷于2026-01-01发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/72577.html
