关于stringRedisTemplate.setIfAbsent()并设置过期时间遇到的问题

在使用Spring Data Redis的stringRedisTemplate.setIfAbsent()进行键值存储时,遇到并发导致的脏数据问题。通过加入事务管理解决了setIfAbsent后设置过期时间的问题,但发现事务中返回值为null,影响后续流程判断。最后通过升级redis版本到2.1以上并直接在setIfAbsent中设置过期时间,以及正确使用事务避免死锁,实现了数据一致性与有效过期。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

spring-date-redis版本:1.6.2
场景:在使用setIfAbsent(key,value)时,想对key设置一个过期时间,同时需要用到setIfAbsent的返回值来指定之后的流程,所以使用了以下代码:

boolean store = stringRedisTemplate.opsForValue().setIfAbsent(key,value);
if(store){
  stringRedisTemplate.expire(key,timeout); 
  // todo something...  
}

这段代码是有问题的:当setIfAbsent成功之后断开连接,下面设置过期时间的代码 stringRedisTemplate.expire(key,timeout); 是无法执行的,这时候就会有大量没有过期时间的数据存在数据库。想到一个办法就是添加事务管理,修改后的代码如下:

stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
boolean store = stringRedisTemplate.opsForValue().setIfAbsent(key,value);
if(store){
  stringRedisTemplate.expire(key,timeout);   
}
stringRedisTemplate.exec();
if(store){
    // todo something...
}

这样就保证了整个流程的一致性。本因为这样就可以了,可是事实总是不尽人意,因为我在文档中发现了以下内容:
图片描述

加了事务管理之后,setIfAbsent的返回值竟然是null,这样就没办法再进行之后的判断了。

好吧,继续解决:

stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
String result = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isNotBlank(result)){
    return false;
}
// 锁的过期时间为1小时
stringRedisTemplate.opsForValue().set(key, value,timeout);
stringRedisTemplate.exec();

// todo something...

上边的代码其实还是有问题的,当出现并发时,String result = stringRedisTemplate.opsForValue().get(key); 这里就会有多个线程同时拿到为空的key,然后同时写入脏数据。

最终解决方法:

  1. 使用stringRedisTemplate.exec();的返回值判断setIfAbsent是否成功
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.opsForValue().setIfAbsent(lockKey,JSON.toJSONString(event));
stringRedisTemplate.expire(lockKey,Constants.REDIS_KEY_EXPIRE_SECOND_1_HOUR, TimeUnit.SECONDS);
List result = stringRedisTemplate.exec(); // 这里result会返回事务内每一个操作的结果,如果setIfAbsent操作失败后,result[0]会为false。
if(true == result[0]){
  // todo something...
}
  1. 将redis版本升级到2.1以上,然后使用

图片描述
直接在setIfAbsent中设置过期时间

update :
java 使用redis的事务时不能直接用Api中的multi()和exec(),这样multi()和exec()两次使用的stringRedisTemplate不是一个connect,会导致死锁,正确方式如下:

    private Boolean setLock(RecordEventModel event) {
        String lockKey = event.getModel() + ":" + event.getAction() + ":" + event.getId() + ":" + event.getMessage_id();
        log.info("lockKey : {}" , lockKey);
        SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
            List<Object> exec = null;
            @Override
            @SuppressWarnings("unchecked")
            public Boolean execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                stringRedisTemplate.opsForValue().setIfAbsent(lockKey,JSON.toJSONString(event));
                stringRedisTemplate.expire(lockKey,Constants.REDIS_KEY_EXPIRE_SECOND_1_HOUR, TimeUnit.SECONDS);
                exec = operations.exec();
                if(exec.size() > 0) {
                    return (Boolean) exec.get(0);
                }
                return false;
            }
        };
        return stringRedisTemplate.execute(sessionCallback);
    }
<think>我们正在讨论的是在Java中使用StringRedisTemplatesetIfAbsent方法设置键值对指定过期时间,以及过期后键值是否会被自动删除的问题。根据之前的讨论,我们知道Redis的键过期删除机制涉及惰性删除和定期删除,但这里我们更关注在Java中通过StringRedisTemplate.setIfAbsent方法设置过期时间后,键值对在过期后的行为。首先,我们来看一下setIfAbsent方法在StringRedisTemplate中的使用。根据引用[2]:redisTemplate.opsForValue().setIfAbsent(key,value,exptime,TimeUnit.SECONDS)是SpringDataRedis提供的一个原子性操作方法,其底层实现基于Redis的SET命令的特定参数组合。具体来说,setIfAbsent方法在设置键值对时,如果键存在,则设置同时设置过期时间,这是一个原子操作。它对应的Redis命令是:SETkeyvalueNXEXseconds(或者使用PX表示毫秒)那么,当这个键值对过期后,Redis会如何处理呢?根据Redis的机制,过期后的键值对会被Redis自动删除。但是,删除的时机取决于Redis的过期键删除策略(惰性删除和定期删除):1.惰性删除:当客户端尝试访问一个已经过期的键时,Redis会立即将其删除,返回空值。2.定期删除:Redis会周期性地随机检查一些设置过期时间的键,如果发现它们已经过期,就会删除它们。因此,过期后的键值对会立即从内存中移除,但会在以下两种情况下被删除:-当有客户端尝试读取这个键时(惰性删除)-当Redis的定期删除任务运行到这个键时所以,从用户的角度来看,一旦键过期,它就会再被正常访问到(即使用户在过期后立即尝试读取,也会得到null),且最终会被Redis从内存中删除。在Java中,使用StringRedisTemplatesetIfAbsent方法设置过期时间后,键值对在过期后会被Redis自动删除,但删除的时间是精确的,而是由上述机制决定。另外,引用[1]中提到了使用set方法设置过期时间(覆盖或新增),其行为也是类似的。总结:使用StringRedisTemplatesetIfAbsent方法设置键值对指定过期时间过期后键值对会被Redis自动删除,但删除的时间是精确的,而是依赖于Redis的过期键删除策略。注意事项:1.确保使用的是原子操作(setIfAbsent方法已经保证原子性,避免了先setnx再expire的非原子操作问题)。2.过期后,键值对会立即从内存中消失,但已经无法被获取到(get操作会返回null)。相关问题:1.Java中,除了setIfAbsent,还有哪些方法可以原子性地设置键值对和过期时间?2.如何确保分布式环境下使用Redis过期键的可靠性?3.Redis的过期删除策略是如何影响内存占用的?如何监控过期键的删除情况?4.在SpringDataRedis中,StringRedisTemplate和RedisTemplate的主要区别是什么?[^3]5.在Redis中,如何手动删除已经过期的键?</think>在 Java 中使用 `StringRedisTemplate` 的 `setIfAbsent` 方法设置键值对指定过期时间后,**键值对在过期后会被 Redis 自动删除**。以下是关键原理和注意事项: ### ✅ 键值对过期后的删除机制 1. **原子性操作保证** `setIfAbsent(key, value, timeout, unit)` 底层通过 Redis 的原子命令实现: ```redis SET key value NX EX seconds # EX表示秒级过期时间 ``` 该命令在键存在时设置值**同步设置过期时间**,确保两个操作是原子的[^2]。 2. **Redis 的过期删除策略** - **惰性删除**:当客户端尝试访问已过期的键时,Redis 会立即删除它返回 `null`。 - **定期删除**:Redis 每秒随机扫描部分过期删除(默认每秒10次)。 ```java // 示例:设置键"lock"(仅当存在),有效期30秒 stringRedisTemplate.opsForValue().setIfAbsent("lock", "1", 30, TimeUnit.SECONDS); ``` 3. **验证删除效果** 过期后尝试读取该键将返回 `null`,证明键已被删除: ```java // 30秒后执行 String value = stringRedisTemplate.opsForValue().get("lock"); System.out.println(value); // 输出 null ``` ### ⚠ 注意事项 1. **非实时删除** 键过期后**会立即从内存清除**,实际删除时间取决于 Redis 的定期扫描周期(通常几秒内)。可通过 `INFO stats` 查看 `expired_keys` 计数确认删除情况。 2. **主从架构延迟** 若使用 Redis 主从集群,从节点可能短暂返回已过期但未同步删除的键值(强一致性场景建议直连主节点)[^2]。 3. **序列化一致性** `StringRedisTemplate` 使用字符串序列化(而非字节数组),确保键值可读性且避免序列化问题[^3]: ```java // 正确用法:直接操作字符串 StringRedisTemplate stringRedisTemplate; // 注入实例 ``` ### 🔍 验证建议 1. 使用 `TTL` 命令检查剩余时间: ```java Long ttl = stringRedisTemplate.getExpire("lock"); // ttl = -2 表示键存在(已删除) ``` 2. 监控内存变化: ```bash redis-cli info memory | grep used_memory ``` ### 📚 相关问题 1. `setIfAbsent` 与 `set` + `expire` 在分布式锁场景中的区别是什么?如何避免锁误删问题? 2. Redis 的惰性删除策略在内存足时会产生什么影响?如何优化过期键清理效率? 3. 在 Spring Boot 中如何配置 `StringRedisTemplate` 连接 Redis 集群?主从延迟如何处理? 4. 除了 `setIfAbsent`,还有哪些原子性操作可以实现带过期时间的分布式锁? 5. 如何通过 Redis 的 `INFO` 命令监控过期键删除数量和内存碎片率?
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值