# Redis 事务与 Lua 脚本# 概述在 Redis 中,事务和 Lua 脚本是实现原子性操作的重要机制。它们可以确保一组命令要么全部执行,要么全部不执行,从而保证数据的一致性。
本文将详细介绍 Redis 事务的机制、Lua 脚本的编程方法,以及它们在实际应用中的使用场景。
# Redis 事务# 什么是 Redis 事务Redis 事务是一组命令的集合,这些命令会按照顺序执行,并且在执行过程中不会被其他客户端的命令打断。Redis 事务通过以下命令实现:
MULTI :标记事务开始EXEC :执行事务中的所有命令DISCARD :取消事务WATCH :监视一个或多个 key# Redis 事务基本使用# 基本事务流程
1 2 3 4 5 6 7 8 9 10 MULTI SET key1 value1 SET key2 value2 GET key1 EXEC
# 取消事务
1 2 3 4 5 6 7 8 9 MULTI SET key1 value1 SET key2 value2 DISCARD
# Redis 事务特性# 1. 原子性Redis 事务中的命令要么全部执行,要么全部不执行。
1 2 3 4 5 MULTI DECR user1:balance 100 INCR user2:balance 100 EXEC
# 2. 隔离性事务执行过程中,其他客户端的命令不会插入执行。
1 2 3 4 5 6 MULTI SET key1 value1 SET key2 value2 EXEC
# 3. 不支持回滚Redis 事务不支持命令执行失败后的回滚操作。
1 2 3 4 5 MULTI SET key1 value1 INCR non_existent_key SET key2 value2 EXEC
# WATCH 命令# WATCH 机制原理WATCH 命令用于实现乐观锁,监视一个或多个 key,如果在事务执行前这些 key 被其他客户端修改,事务将不会执行。
1 2 3 4 5 6 7 WATCH balance MULTI DECR balance 100 EXEC
# WATCH 使用示例
1 2 3 4 5 6 7 WATCH balance GET balance MULTI DECR balance 100 EXEC
# UNWATCH 命令
# Redis 事务实战# 1. 库存扣减
1 2 3 4 5 6 WATCH product:1001:stock GET product:1001:stock MULTI DECR product:1001:stock EXEC
# 2. 积分兑换
1 2 3 4 5 6 7 WATCH user:1001:points GET user:1001:points MULTI DECRBY user:1001:points 100 INCR user:1001:coupons 1 EXEC
# 3. 限流器实现
1 2 3 4 5 6 7 WATCH rate_limit:user:1001 GET rate_limit:user:1001 MULTI INCR rate_limit:user:1001 EXPIRE rate_limit:user:1001 60 EXEC
# Lua 脚本# 什么是 Lua 脚本Lua 脚本是一种轻量级的脚本语言,Redis 从 2.6 版本开始支持 Lua 脚本。Lua 脚本在 Redis 中具有以下优势:
原子性执行 :脚本执行过程中不会被其他命令打断减少网络开销 :多个命令可以在一次网络调用中完成复用性 :脚本可以被多次调用性能优化 :脚本在 Redis 服务器端执行# Lua 脚本基本语法# 1. 脚本执行
1 2 3 4 5 6 7 8 EVAL 'return "Hello, Redis!"' 0 EVAL 'return ARGV[1] .. " " .. ARGV[2]' 0 Hello Redis EVAL 'return redis.call("SET", KEYS[1], ARGV[1])' 1 mykey myvalue
# 2. 脚本参数说明KEYS[1], KEYS[2], ... :Redis key 参数ARGV[1], ARGV[2], ... :普通参数第一个数字参数表示 KEYS 的数量 # 3. 基本语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 local key = KEYS[1 ]local value = ARGV[1 ]if redis.call("EXISTS" , key) == 1 then return redis.call("GET" , key) else return "not exists" end for i = 1 , tonumber (ARGV[1 ]) do redis.call("INCR" , key) end return result
# Lua 脚本 Redis API# 1. redis.call()
1 2 3 local result = redis.call("GET" , "mykey" )local count = redis.call("INCR" , "counter" )
# 2. redis.pcall()
1 2 3 4 5 6 7 local result = redis.pcall ("GET" , "mykey" )if result then else end
# 3. 返回值处理
1 2 3 4 5 6 7 8 9 10 11 return "success" return 100 return {1 , 2 , 3 }return redis.status_reply("OK" )
# Lua 脚本实战# 1. 原子性库存扣减
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 local stock_key = KEYS[1 ]local order_count = tonumber (ARGV[1 ])local current_stock = tonumber (redis.call("GET" , stock_key))if current_stock >= order_count then redis.call("DECRBY" , stock_key, order_count) return 1 else return 0 end
# 2. 限流器实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 local key = KEYS[1 ]local window = tonumber (ARGV[1 ]) local limit = tonumber (ARGV[2 ]) local current_time = tonumber (redis.call("TIME" )[1 ])local window_start = current_time - windowredis.call("ZREMRANGEBYSCORE" , key, 0 , window_start) local current_count = redis.call("ZCARD" , key)if current_count < limit then redis.call("ZADD" , key, current_time, current_time) redis.call("EXPIRE" , key, window) return 1 else return 0 end
# 3. 分布式锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 local lock_key = KEYS[1 ]local lock_value = ARGV[1 ]local expire_time = tonumber (ARGV[2 ])local result = redis.call("SET" , lock_key, lock_value, "NX" , "EX" , expire_time)if result then return 1 else local current_value = redis.call("GET" , lock_key) if current_value == lock_value then redis.call("EXPIRE" , lock_key, expire_time) return 1 else return 0 end end
# 4. 有序集合排行榜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 local leaderboard_key = KEYS[1 ]local user_id = ARGV[1 ]local score = tonumber (ARGV[2 ])redis.call("ZADD" , leaderboard_key, score, user_id) local rank = redis.call("ZREVRANK" , leaderboard_key, user_id)local user_info = { rank = rank + 1 , score = score } return user_info
# 5. 批量操作优化
1 2 3 4 5 6 7 8 9 10 11 12 13 local pattern = ARGV[1 ]local expire_time = tonumber (ARGV[2 ])local keys = redis.call("KEYS" , pattern)for i = 1 , #keys do redis.call("EXPIRE" , keys[i], expire_time) end return #keys
# 脚本管理# 1. 脚本加载和执行# SCRIPT LOAD
1 2 3 SCRIPT LOAD 'return "Hello, Redis!"'
# EVALSHA
1 2 EVALSHA 5b203c6a3d4f8a4c9e8b0c1a2d3e4f5a6b7c8d9e 0
# 2. 脚本管理命令
1 2 3 4 5 6 7 8 SCRIPT EXISTS 5b203c6a3d4f8a4c9e8b0c1a2d3e4f5a6b7c8d9e SCRIPT FLUSH SCRIPT KILL
# 3. 脚本调试
1 2 3 4 5 6 7 8 redis-cli --ldb --eval script.lua key1 key2 , arg1 arg2
# SpringDataRedis 中的 Lua 脚本# 1. 使用 RedisTemplate 执行 Lua 脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Autowired private RedisTemplate<String, Object> redisTemplate;public boolean atomicDecrementStock (String key, int count) { String script = "local stock = tonumber(redis.call('GET', KEYS[1]))\n" + "if stock >= tonumber(ARGV[1]) then\n" + " redis.call('DECRBY', KEYS[1], ARGV[1])\n" + " return 1\n" + "else\n" + " return 0\n" + "end" ; DefaultRedisScript<Long> redisScript = new DefaultRedisScript <>(script, Long.class); Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), count); return result != null && result == 1 ; }
# 2. 使用 RedisScript 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Configuration public class LuaScriptConfig { @Bean public RedisScript<Long> stockDecrementScript () { String script = "local stock = tonumber(redis.call('GET', KEYS[1]))\n" + "if stock >= tonumber(ARGV[1]) then\n" + " redis.call('DECRBY', KEYS[1], ARGV[1])\n" + " return 1\n" + "else\n" + " return 0\n" + "end" ; return new DefaultRedisScript <>(script, Long.class); } } @Service public class StockService { @Autowired private RedisScript<Long> stockDecrementScript; @Autowired private RedisTemplate<String, Object> redisTemplate; public boolean decrementStock (String key, int count) { Long result = redisTemplate.execute( stockDecrementScript, Collections.singletonList(key), count ); return result != null && result == 1 ; } }
# 3. 从文件加载 Lua 脚本
1 2 3 4 5 6 7 8 9 10 11 @Component public class LuaScriptLoader { @Value("classpath:scripts/rate_limit.lua") private Resource rateLimitScript; public RedisScript<Long> loadRateLimitScript () throws IOException { String scriptText = StreamUtils.copyToString(rateLimitScript.getInputStream(), StandardCharsets.UTF_8); return new DefaultRedisScript <>(scriptText, Long.class); } }
# 性能优化# 1. 脚本缓存
1 2 3 SCRIPT LOAD 'return redis.call("GET", KEYS[1])' SCRIPT LOAD 'return redis.call("SET", KEYS[1], ARGV[1])'
# 2. 批量操作
1 2 3 4 5 6 local results = {}for i = 1 , #KEYS do results[i] = redis.call("GET" , KEYS[i]) end return results
# 3. 管道化执行
1 2 3 4 5 6 7 8 9 10 List<Object> results = redisTemplate.executePipelined(new SessionCallback <Object>() { @Override public Object execute (RedisOperations operations) throws DataAccessException { for (int i = 0 ; i < 100 ; i++) { operations.execute(redisScript, Collections.singletonList("key:" + i), i); } return null ; } });
# 最佳实践# 1. 脚本设计原则保持简洁 :避免复杂的逻辑和循环错误处理 :使用 pcall 处理可能的错误参数验证 :验证输入参数的有效性资源控制 :避免死循环和资源泄漏# 2. 错误处理
1 2 3 4 5 6 7 8 9 10 local ok, result = pcall (function () return redis.call("GET" , KEYS[1 ]) end )if ok then return result else return redis.error_reply("Script execution failed" ) end
# 3. 调试技巧
1 2 3 4 5 local debug = true if debug then redis.call("SET" , "debug:log" , "Current value: " .. tostring (value)) end
# 总结Redis 事务和 Lua 脚本是实现原子性操作的重要工具:
Redis 事务 :适合简单的原子性操作,使用简单但功能有限Lua 脚本 :功能强大,支持复杂逻辑,性能更好选择建议 :简单操作使用事务,复杂逻辑使用 Lua 脚本合理使用这些特性,可以有效提高应用的性能和数据一致性。