1、问题
单机模式下,执行lua脚本没有问题。放到集群执行后,报错
ERR Error running script (call to f_4a610f5543b3c3450220da7bd47825d3b6bffae8): @user_script:1: @user_script: 1: Lua script attempted to access a non local key in a cluster node
2、解决
因为Redis要求单个Lua脚本操作的key必须在同一个节点上,但是Cluster会将数据自动分布到不同的节点(虚拟的16384个slot,具体看官方文档)。
Redis cluster对多key操作有限制,要求命令中所有的key都属于一个slot,才可以被执行。
> CLUSTER KEYSLOT somekey
11058
> CLUSTER KEYSLOT foo{hash_tag}
(integer) 2515
> CLUSTER KEYSLOT bar{hash_tag}
(integer) 2515
keySlot算法中,如果key包含{},就会使用第一个{}内部的字符串作为hash key,这样就可以保证拥有同样{}内部字符串的key就会拥有相同slot。
3、原子性
在 Redis 中执行 Lua 脚本具有原子性。Redis 使用单线程模型来处理命令,这意味着 Redis 在执行命令时不会被其他客户端的命令中断。这一点同样适用于 Lua 脚本的执行。
当你在 Redis 中运行 Lua 脚本时,整个脚本会以原子方式在服务器上执行。这意味着脚本内的多个命令将作为一个单一的、原子性的事务来执行。原子性是指脚本中的所有命令都将在不被其他客户端干扰的情况下执行,要么全部成功,要么全部失败。
这种原子性对于执行复杂的操作或需要多个 Redis 命令组成的事务非常有用。这确保了在脚本执行期间不会发生并发冲突或竞态条件。
然而,需要注意的是,尽管 Lua 脚本的执行是原子的,但它并不提供锁。如果你的脚本包含多个 Redis 命令,并且需要确保这些命令在其他客户端的干扰下不被打断,你可能需要使用 WATCH
和 MULTI/EXEC
等事务控制命令来实现更复杂的原子性操作(全部成功或全部失败)。
总体而言,Lua 脚本在 Redis 中的执行是原子的,但具体的原子性要根据脚本中包含的操作和逻辑来判断。
示例-下单减库存
在 Lua 脚本中实现下单扣减库存的操作时,可以使用 Redis 的 WATCH 和 MULTI/EXEC 事务机制来确保原子性,从而防止超卖(即库存被超过实际数量的订单扣减)的问题。
以下是一个简单的 Lua 脚本示例,用于下单并扣减库存:
-- 商品库存键名
local product_key = "product:inventory"
-- 订单号
local order_id = "order:123"
-- 要购买的数量
local quantity_to_purchase = 1
-- 开启 WATCH
redis.call("WATCH", product_key)
-- 获取当前库存
local current_inventory = tonumber(redis.call("GET", product_key) or 0)
-- 检查库存是否足够
if current_inventory >= quantity_to_purchase then
-- 开启事务
redis.call("MULTI")
-- 扣减库存
redis.call("SET", product_key, current_inventory - quantity_to_purchase)
-- 生成订单等其他操作...
-- 执行事务
local result = redis.call("EXEC")
-- 检查事务执行是否成功
if result then
return "Order placed successfully!"
else
return "Failed to place order. Please try again."
end
else
return "Insufficient inventory!"
end
此脚本的关键点包括:
- 使用
WATCH
命令来监视商品库存键。 - 在
MULTI
和EXEC
之间执行一系列 Redis 命令,这些命令将作为一个原子事务执行。 - 在事务中,首先检查库存是否足够。如果足够,则扣减库存和执行其他相关操作;否则,返回相应的提示。
这种方式可以确保在多个客户端同时尝试购买商品时,只有一个事务会成功,从而防止超卖的问题。如果在 WATCH 和 EXEC 之间有其他客户端修改了被 WATCH 的键,事务将失败,并且你可以在脚本中处理这种情况。在此示例中,如果事务失败,脚本返回一个失败的消息,提示客户端重新尝试下单。
4、事务注意事项
在使用 Redis 事务时,特别是在 Redis 集群中,有一些需要注意的事项:
-
槽分区: Redis 集群使用槽分区机制,确保所有的键都映射到一个槽上。在事务中,所有涉及的键都应该在同一个槽上,以避免
CROSSSLOT
错误。-
规范:为避免
CROSSSLOT错误,事务中只能操作一个key。
-
-
WATCH 命令: WATCH 命令用于实现乐观锁,但在 Redis 集群中,WATCH 只能监视同一节点上的键,不能跨节点监视。确保 WATCH 监视的键在同一个槽上,否则可能导致 WATCH 失效。
-
节点间通信: 如果事务中的键位于不同的槽,Redis 集群会在节点之间进行协调通信。这可能导致性能上的开销,因此在设计数据模型时,可以考虑将相关的键映射到同一个槽上。
-
原子性保证: Redis 事务的原子性保证是基于槽的,即在同一个槽上的操作是原子的。如果跨越多个槽,事务在不同的节点上可能并非原子执行,需要谨慎设计事务。
-
事务回滚: 事务中的某个命令执行失败,整个事务会回滚。在 Redis 中,一旦事务开始(MULTI 命令被调用),Redis 将放弃所有的错误检测,并继续执行后续的命令。只有在 EXEC 命令执行时才能捕获到事务中的错误。
-
性能开销: 在 Redis 集群中,由于可能涉及节点间的通信和协调,事务可能会导致性能开销。尤其是跨节点的事务操作。
-
WATCH 和 MULTI/EXEC 的嵌套使用: 在 Redis 集群中,WATCH 和 MULTI/EXEC 可以嵌套使用,但要谨慎,确保逻辑正确,以避免潜在的问题。
总体来说,Redis 事务在集群中仍然提供了一定程度的原子性,但需要注意上述的限制和特点。设计数据模型和事务操作时,考虑到槽的分布、节点间通信以及性能开销是很重要的。