Spring/Jedis环境下如何配置避免Redis写入键时读取失败,返回旧数据?
首先,你遇到的问题不是Jedis或Spring连接配置的问题,根源在于Spring Data Redis对@RedisHash实体的默认save操作不具备原子性:当调用repository.save()时,底层会先执行DEL DataBean:{id}删除整个Hash键,再执行HSET DataBean:{id} data {value}写入新字段。这两个命令之间存在微小的时间窗口,此时如果有读取操作,就会发现键不存在,导致日志中的“Not found”错误。
因为你可以接受读取旧数据,我们可以从保证操作原子性入手,提供几个简洁的解决方案:
方案1:改用String类型存储(最推荐,最简洁)
你的DataBean只有一个字段,完全没必要用Redis Hash结构。改用String类型存储的话,SET命令本身是原子的,写入时读取会直接返回旧值,完美符合你的需求。
修改步骤:
- 调整实体类(或直接用String作为值):
public class DataBean { private String data; public DataBean(String data) { this.data = data; } public String getData() { return data; } }
- 自定义Repository的String类型操作方法:
@Repository public interface DataBeanRepository extends CrudRepository<DataBean, String> { @Modifying @Query("SET DataBean:#id :#{#entity.data}") void saveAsString(DataBean entity); @Query("GET DataBean:#id") String findAsStringById(String id); }
- 生产者线程调用
saveAsString,消费者线程调用findAsStringById即可。
方案2:自定义原子更新Hash的方法(保留Hash结构)
如果必须使用Hash结构,不要用默认的save方法,而是自定义一个直接更新Hash字段的方法,跳过删除整个键的步骤。HSET命令是原子的,不会删除键,读取时始终能拿到旧值或新值。
修改Repository:
@Repository public interface DataBeanRepository extends CrudRepository<DataBean, String> { @Modifying @Query("HSET DataBean:#id data :#{#entity.data}") void update(DataBean entity); }
修改生产者线程:
把repository.save(new DataBean(data))替换为repository.update(new DataBean(data)),这样就不会删除整个Hash键,而是直接更新字段,彻底消除键不存在的时间窗口。
为什么没有“连接配置”能解决这个问题?
Redis本身的读写是单线程串行执行的,但你的问题是客户端执行了两个独立的非原子命令(DEL + HSET),中间可能被读取命令插入。Jedis或Spring的连接配置(比如连接池、超时等)无法改变Redis命令的执行逻辑,所以必须从操作本身的原子性入手解决,这才是最根本的方案。
内容的提问来源于stack exchange,提问作者edtrcd

