Redis事务详解
1、概念Redis 事务的本质是将多个命令打包,然后按顺序、一次性、隔离地执行。它通过 MULTI, EXEC, DISCARD, WATCH 四个核心命令实现。
[*]MULTI:开启一个事务,之后的命令都会放入队列,而不是立即执行。
[*]EXEC:执行事务队列中的所有命令。
[*]DISCARD:取消事务,清空队列。
[*]WATCH:在 MULTI 之前,监视一个或多个 key。如果在 EXEC 执行前,这些被监视的 key 被其他客户端修改,则整个事务会失败(EXEC 返回 nil)。这是实现乐观锁(Optimistic Locking) 的基础。
与传统事务的差异
[*]无回滚:Redis 设计哲学是简单和高效,失败的命令通常是编程错误(命令用错、数据类型错误),应该在开发阶段被发现,而不是在生产环境通过回滚来挽救。回滚会增加复杂性和性能开销。
[*]隔离性:事务中的所有命令都会按顺序串行执行,并且在 EXEC 执行前,事务中的命令不会被其他客户端的命令插队。这保证了事务执行过程中不会受到其他客户端的干扰。
[*]持久化无关:事务的 “执行成功” 仅表示命令入队并执行,不保证已持久化到磁盘(需配合 AOF 配置 appendfsync always 提升可靠性,但会牺牲性能)。
[*]不保证原子性(Atomicity):这是最大的误区!Redis 事务保证的是隔离性(Isolation) 和顺序执行,但不保证原子性。这意味着如果事务中某个命令执行失败(例如对字符串执行了 HINCRBY),其他命令依然会继续执行,且不会回滚(Rollback)
2、最佳实践详解与示例
2.1 一个失败的事务会发生什么?
127.0.0.1:6379> SET key1 "hello"
OK
127.0.0.1:6379> SET key2 100
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR key1 # 错误:对字符串 'hello' 执行 INCR,这会失败!
QUEUED
127.0.0.1:6379> INCR key2 # 正确:对数字 100 执行 INCR
QUEUED
127.0.0.1:6379> EXEC
1) (error) ERR value is not an integer or out of range # 第一条命令报错
2) (integer) 101 # 第二条命令成功执行!
127.0.0.1:6379> GET key2
"101"2.2 使用WATCH实现乐观锁
工作流程:
[*]WATCH 需要监控的 key。
[*]读取 key 的值并进行业务逻辑计算。
[*]开启 MULTI。
[*]将写命令(如 SET, INCRBY)放入队列。
[*]执行 EXEC。
[*]如果成功:说明在 WATCH 到 EXEC 期间,被监控的 key 没有被其他客户端改动,事务执行成功,WATCH 可以监控多个 key,只要其中任何一个被修改,事务都会失败。
[*]如果返回 nil:说明 key 已被修改,事务执行失败。通常的处理方式是重试整个流程。
import redis
import time
r = redis.Redis(host='localhost', port=6379, db=0)
def deduct_balance(user_id, amount):
""" 扣减用户余额,使用乐观锁保证并发安全 """
balance_key = f"user:{user_id}:balance"
# 重试次数,避免死循环
max_retries = 5
retry_count = 0
while retry_count < max_retries:
retry_count += 1
try:
# 1. 开启 Pipeline(包含 WATCH 命令,Python redis库中,pipeline 默认隐含 MULTI/EXEC)
# `transaction=True` 表示我们要使用事务,它内部会处理 WATCH/MULTI/EXEC
pipe = r.pipeline(transaction=True)
# 2. WATCH 要监控的 key
pipe.watch(balance_key)
# 3. 读取并检查余额
current_balance = int(pipe.get(balance_key) or 0)
if current_balance < amount:
pipe.unwatch() # 解除监控,可选,EXEC/DISCARD后也会自动解除
print("Insufficient balance!")
return False
# 4. 开启事务,组装命令
pipe.multi()
pipe.decrby(balance_key, amount)
# 5. 执行事务
# 如果 balance_key 在 WATCH 后被执行了,这里会抛出 WatchError 异常
result = pipe.execute()
# execute() 成功返回命令执行结果的列表,例如
print(f"Deduction successful! New balance: {result}")
return True
except redis.WatchError:
# 6. 捕获 WatchError,表示事务失败,进行重试
print(f"Transaction conflicted. Retrying... ({retry_count}/{max_retries})")
time.sleep(0.1) # 稍等片刻再重试,避免激烈竞争
continue
except Exception as e:
# 处理其他异常
print(f"Unexpected error: {e}")
break
print("Failed after max retries.")
return False
# 初始化用户余额
r.set("user:100:balance", 100)
# 模拟并发扣减
deduct_balance(100, 50)3、使用 Lua 脚本替代复杂事务
对于非常复杂的逻辑,使用 WATCH 和 MULTI 会使得重试循环非常笨重。此时,Lua 脚本是更好的选择。
Lua 脚本的优势:
[*]真正的原子性:整个脚本在执行时是以单线程、原子性的方式运行的,中间不会被执行任何其他命令,无需使用 WATCH。
[*]减少网络开销:逻辑在服务器端执行,避免了多次网络往返。
[*]复杂性封装:将复杂逻辑封装在服务器端的一个脚本中。
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 定义Lua脚本代码
LUA_DEDUCT_SCRIPT = """
local balance_key = KEYS
local amount = tonumber(ARGV)
local current_balance = tonumber(redis.call('get', balance_key) or 0)
if current_balance < amount then
return false -- 余额不足,返回false
end
redis.call('decrby', balance_key, amount)
return true -- 扣减成功,返回true
"""
# 预先加载脚本,获取一个sha1哈希摘要,后续可以用这个摘要来执行,效率更高。
script_sha = r.script_load(LUA_DEDUCT_SCRIPT)
def deduct_balance_lua(user_id, amount):
balance_key = f"user:{user_id}:balance"
# 使用 evalsha 执行脚本
# 1 表示后面有1个KEY, amount 是ARGV参数
success = r.evalsha(script_sha, 1, balance_key, amount)
return bool(success)
# 初始化
r.set("user:100:balance", 100)
# 执行扣减
result = deduct_balance_lua(100, 50)
print(f"Deduction result: {result}")
print(f"Remaining balance: {r.get('user:100:balance')}")最佳实践:
[*]对于复杂的、需要原子性执行的逻辑,优先使用 Lua 脚本。
[*]使用 SCRIPT LOAD 预加载脚本,然后用 EVALSHA 通过脚本的 SHA1 摘要来执行,可以节省每次传输完整脚本的网络带宽。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]