一. 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. 注意事项

  1. 原子性:
    SETNX + EXPIRE 是非原子操作!推荐使用 SET key value EX time NX。
  2. 分布式锁续期:
    考虑用 Redisson 或 Lua 脚本实现锁续期(避免业务未完成锁已过期)。
  3. 值覆盖风险:
    解锁时需验证 value(如 UUID),防止误删其他客户端的锁。

4. 总结

命令作用典型场景
SETNXkey 不存在时设置值分布式锁、幂等操作
SET ... NX增强版 SETNX(支持过期时间)安全的分布式锁
INCR原子递增计数器限流器、计数器
EXPIRE设置 key 过期时间缓存管理、临时数据

二、分布式锁实现

通过SET key value NX EX time out实现原子性加锁和超时设置。

核心步骤

  1. 加锁:使用唯一值(如UUID)作为value,避免误删其他客户端的锁
  2. 解锁:通过Lua脚本校验value一致性,保证原子性删除
  3. 超时机制:自动释放锁防止死锁

代码示例(使用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标记已处理请求。

核心逻辑

  1. 客户端生成唯一请求ID(如订单ID+业务类型)
  2. 执行SET命令尝试写入Redis
  3. 返回OK表示首次请求,执行业务
  4. 返回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;
	}


七、生产环境注意事项

  1. 锁续期问题
    对于长任务,使用守护线程定时续期(类似Redisson的WatchDog)
  2. 集群环境
    Redis主从切换可能导致锁失效,考虑使用Redlock算法(但有争议)
  3. 幂等键设计
    业务:唯一ID格式(如 order:pay:1001)
  4. 网络超时
    添加重试机制(但需避免雪崩)
  5. 替代方案
    复杂场景建议使用成熟库:
    • 分布式锁:Redisson
    • 幂等处理:Spring的@Idempotent注解 + 中间件

八、两种场景对比

特性分布式锁幂等操作
目的互斥访问共享资源防止重复提交/处理
核心命令SET key uuid NX EXSET key requestId NX EX
数据存储要求临时数据(自动过期)需保留至业务周期结束(设置长TTL)
典型应用场景库存扣减、订单创建支付回调、消息去重

通过合理组合这两种机制,可解决大多数分布式并发问题。

更多推荐