Redis的常用命令及`SETNX`实现分布式锁、幂等操作
·
文章目录
一. 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_001
if (isFirstRequest(jedis, requestKey)) {
// 首次请求,执行业务
System.out.println("处理支付: " + amount);
// TODO: 数据库操作等
} else {
// 重复请求
System.out.println("重复请求,直接返回");
}
}
}
}
关键点
- 唯一请求标识:使用业务唯一键(如订单ID)
- 自动过期:设置合理过期时间清理旧数据
- 原子操作:
SET NX保证并发安全
四、Java中用reids实现限流
实现工具类:RedisLimitUtil.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* 接口限流
*
*/
@Slf4j
@Component
public class RedisLimitUtil {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 限流
* @param key 键
* @param count 限流次数
* @param times 限流时间
* @return
*/
public boolean limit(String key, int count, int times) {
try {
String script = "local lockKey = KEYS[1]\n" +
"local lockCount = KEYS[2]\n" +
"local lockExpire = KEYS[3]\n" +
"local currentCount = tonumber(redis.call('get', lockKey) or \"0\")\n" +
"if currentCount < tonumber(lockCount)\n" +
"then\n" +
"redis.call(\"INCRBY\", lockKey, \"1\")\n" +
"redis.call(\"expire\", lockKey, lockExpire)\n" +
"return true\n" +
"else\n" +
"return false\n" +
"end";
RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
List<String> keys = Arrays.asList(key, String.valueOf(count), String.valueOf(times));
log.info("key:{}",key);
return redisTemplate.execute(redisScript, keys);
} catch (Exception e) {
log.error("限流脚本执行失败:{}", e.getMessage());
}
return false;
}
}
五、Java中Redis过期监控
实现工具类:RedisListenerConfig .java
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 事件以__keyevent@<db>__为前缀进行发布
container.addMessageListener(new RedisKeyExpirationListener(container), new PatternTopic("__keyevent@2__:expired"));
return container;
}
}
六、Java中Redis命令setIfAbsent
回调事件处理幂setIfAbsent
private Boolean isHaveDealtWith(String callId, String eventType) {
// 回调事件处理幂等
String aiccCallbackIdempotentKey = PigRedisKeys.AICC_CALLBACK_IDEMPOTENT.concat(callId+":"+eventType);
Boolean result = redisTemplate.opsForValue().setIfAbsent(aiccCallbackIdempotentKey, "1", PigRedisKeys.AICC_CALLBACK_TIMEOUT, TimeUnit.MINUTES);
if (!result){
log.error("Receive 执行任务完成逻辑处理, 该回调事件已经执行过,不再重复执行===>丢弃:callId:{}, eventType:{}", callId, eventType);
return Boolean.FALSE;
}
return Boolean.TRUE;
}
七、生产环境注意事项
- 锁续期问题
对于长任务,使用守护线程定时续期(类似Redisson的WatchDog) - 集群环境
Redis主从切换可能导致锁失效,考虑使用Redlock算法(但有争议) - 幂等键设计
业务:唯一ID格式(如order:pay:1001) - 网络超时
添加重试机制(但需避免雪崩) - 替代方案
复杂场景建议使用成熟库:- 分布式锁:Redisson
- 幂等处理:Spring的@Idempotent注解 + 中间件
八、两种场景对比
| 特性 | 分布式锁 | 幂等操作 |
|---|---|---|
| 目的 | 互斥访问共享资源 | 防止重复提交/处理 |
| 核心命令 | SET key uuid NX EX | SET key requestId NX EX |
| 数据存储要求 | 临时数据(自动过期) | 需保留至业务周期结束(设置长TTL) |
| 典型应用场景 | 库存扣减、订单创建 | 支付回调、消息去重 |
通过合理组合这两种机制,可解决大多数分布式并发问题。
更多推荐


所有评论(0)