Redis的常用命令及`SETNX`实现分布式锁、幂等操作
文章目录
- 一. Redis相关命令详解
- 1. 相关命令详解
- (1)SET(基础设置命令)
- (2)GET(获取值)
- (3)DEL(删除键)
- (4)EXPIRE(设置过期时间)
- (5)TTL(查看剩余生存时间)
- (6)INCR(原子递增)
- 1. 命令使用场景
- (1)分布式锁
- (2)限流器
- (3)缓存防穿透
- 3. 注意事项
- 4. 总结
- 二、分布式锁实现
- 核心步骤
- 代码示例(使用Jedis)
- 关键点
- 三、幂等操作实现
- 核心逻辑
- 代码示例:
- 关键点
- 四、生产环境注意事项
- 五、两种场景对比
一. Redis相关命令详解
1. 相关命令详解
(1)SET(基础设置命令)
- 作用:设置键值对(会覆盖旧值)。
- 扩展选项(Redis 2.6.12+):
EX seconds
:设置过期时间(秒)。PX milliseconds
:设置过期时间(毫秒)。NX
:等价于SETNX
(key 不存在才设置)。XX
:key 存在时才设置。
- 示例:
SET lock:res001 "token" EX 10 NX # 设置分布式锁(10秒后自动释放)
(2)GET(获取值)
- 命令格式:
GET key
- 返回值:key 的值;若 key 不存在,返回
nil
。 - 示例:
GET mykey # 返回 "Hello"
(3)DEL(删除键)
- 命令格式:
DEL key [key ...]
- 作用:删除一个或多个 key。
- 返回值:被删除 key 的数量。
- 示例:
DEL mykey # 删除 mykey
(4)EXPIRE(设置过期时间)
- 命令格式:
EXPIRE key seconds
- 作用:为 key 设置生存时间(秒)。
- 返回值:
1
:设置成功。0
:key 不存在或设置失败。
- 示例:
EXPIRE mykey 60 # 60秒后 mykey 自动删除
(5)TTL(查看剩余生存时间)
- 命令格式:
TTL key
- 返回值:
- 剩余生存时间(秒)。
-2
:key 不存在。-1
:key 存在但未设置过期时间。
- 示例:
TTL mykey # 返回剩余秒数
(6)INCR(原子递增)
- 命令格式:
INCR key
- 作用:将 key 储存的整数值增加 1。若 key 不存在,先初始化为 0 再执行。
- 返回值:递增后的值。
- 示例:
SET counter 10 INCR counter # 返回 11
1. 命令使用场景
(1)分布式锁
# 加锁(设置锁 + 10秒过期)
SET lock:order123 "uuid" EX 10 NX# 解锁(先检查值再删除)
if GET lock:order123 == "uuid":DEL lock:order123
注意:更推荐使用 Redlock 算法或 Lua 脚本保证原子性。
(2)限流器
利用 INCR
和 EXPIRE
实现简单限流:
# 每秒限流 10 次
KEY = "rate_limit:user123"
COUNT = INCR KEY
if COUNT == 1:EXPIRE KEY 1 # 第一次设置过期时间
if COUNT > 10:return "请求过多"
(3)缓存防穿透
# 查询数据库前,用 SETNX 设置空值标记
if SETNX cache:key:empty "":EXPIRE cache:key:empty 300 # 短期缓存空值
3. 注意事项
- 原子性:
SETNX + EXPIRE
是非原子操作!推荐使用SET key value EX time NX
。 - 分布式锁续期:
考虑用 Redisson 或 Lua 脚本实现锁续期(避免业务未完成锁已过期)。 - 值覆盖风险:
解锁时需验证 value(如 UUID),防止误删其他客户端的锁。
4. 总结
命令 | 作用 | 典型场景 |
---|---|---|
SETNX | key 不存在时设置值 | 分布式锁、幂等操作 |
SET ... NX | 增强版 SETNX(支持过期时间) | 安全的分布式锁 |
INCR | 原子递增计数器 | 限流器、计数器 |
EXPIRE | 设置 key 过期时间 | 缓存管理、临时数据 |
二、分布式锁实现
通过SET key value NX EX time out
实现原子性加锁和超时设置。
核心步骤
- 加锁:使用唯一值(如UUID)作为value,避免误删其他客户端的锁
- 解锁:通过Lua脚本校验value一致性,保证原子性删除
- 超时机制:自动释放锁防止死锁
代码示例(使用Jedis)
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.UUID;public class RedisDistributedLock {private static final String LOCK_SUCCESS = "OK";private static final Long RELEASE_SUCCESS = 1L;private static final int DEFAULT_EXPIRE_TIME = 30; // 默认锁超时30秒// 获取锁public static String acquireLock(Jedis jedis, String lockKey, int expireTime) {String requestId = UUID.randomUUID().toString();SetParams params = SetParams.setParams().nx().ex(expireTime);String result = jedis.set(lockKey, requestId, params);return LOCK_SUCCESS.equals(result) ? requestId : null;}// 释放锁(Lua脚本保证原子性)public static boolean releaseLock(Jedis jedis, String lockKey, String requestId) {String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) " +"else return 0 end";Object result = jedis.eval(luaScript, 1, lockKey, requestId);return RELEASE_SUCCESS.equals(result);}// 使用示例public static void main(String[] args) {try (Jedis jedis = new Jedis("localhost")) {String lockKey = "order_lock_123";String requestId = acquireLock(jedis, lockKey, DEFAULT_EXPIRE_TIME);if (requestId != null) {try {// 执行业务操作System.out.println("执行业务逻辑...");} finally {releaseLock(jedis, lockKey, requestId);}} else {System.out.println("获取锁失败");}}}
}
关键点
- 唯一value:使用UUID防止误删其他请求的锁
- 原子解锁:Lua脚本确保
GET + DEL
的原子性 - 超时兜底:即使业务崩溃,锁也会自动释放
三、幂等操作实现
利用SET key unique_token NX EX timeout
标记已处理请求。
核心逻辑
- 客户端生成唯一请求ID(如订单ID+业务类型)
- 执行
SET
命令尝试写入Redis - 返回
OK
表示首次请求,执行业务 - 返回
null
表示重复请求,直接跳过
代码示例:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;public class IdempotentProcessor {private static final int TOKEN_EXPIRE = 24 * 3600; // 令牌过期时间24小时public static boolean isFirstRequest(Jedis jedis, String requestId) {// 尝试设置键值(存在则失败)SetParams params = SetParams.setParams().nx().ex(TOKEN_EXPIRE);return "OK".equals(jedis.set(requestId, "1", params));}// 使用示例(支付幂等校验)public static void processPayment(String orderId, double amount) {try (Jedis jedis = new Jedis("localhost")) {String requestKey = "pay:" + orderId; // 如: pay:ORDER_20230710_001if (isFirstRequest(jedis, requestKey)) {// 首次请求,执行业务System.out.println("处理支付: " + amount);// TODO: 数据库操作等} else {// 重复请求System.out.println("重复请求,直接返回");}}}
}
关键点
- 唯一请求标识:使用业务唯一键(如订单ID)
- 自动过期:设置合理过期时间清理旧数据
- 原子操作:
SET NX
保证并发安全
四、生产环境注意事项
- 锁续期问题
对于长任务,使用守护线程定时续期(类似Redisson的WatchDog) - 集群环境
Redis主从切换可能导致锁失效,考虑使用Redlock算法(但有争议) - 幂等键设计
业务:唯一ID
格式(如order:pay:1001
) - 网络超时
添加重试机制(但需避免雪崩) - 替代方案
复杂场景建议使用成熟库:- 分布式锁:Redisson
- 幂等处理:Spring的@Idempotent注解 + 中间件
五、两种场景对比
特性 | 分布式锁 | 幂等操作 |
---|---|---|
目的 | 互斥访问共享资源 | 防止重复提交/处理 |
核心命令 | SET key uuid NX EX | SET key requestId NX EX |
数据存储要求 | 临时数据(自动过期) | 需保留至业务周期结束(设置长TTL) |
典型应用场景 | 库存扣减、订单创建 | 支付回调、消息去重 |
通过合理组合这两种机制,可解决大多数分布式并发问题。