基于Spring Boot 3的Redis原子事务实现数据库插入幂等性需求
解决方案
先明确几个关键点:Lettuce完全支持Redis的MULTI/EXEC事务机制,Spring Data Redis也有封装好的API可以直接用。另外,你要的「检查键是否存在并原子创建」其实用SETNX(或SET ... NX)单命令就能实现原子性,比事务更轻量,但既然你倾向用事务方案,下面两种实现方式都给你:
方案一:Spring Data Redis事务模板(推荐,贴合Spring生态)
用RedisTemplate的回调方法结合WATCH命令,实现键的原子检查与创建:
工具类代码
import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisAtomicHelper { private final RedisTemplate<String, String> redisTemplate; public RedisAtomicHelper(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } // 原子检查键是否存在,不存在则创建(初始值设为"PENDING"标记DB插入中) public Boolean checkAndCreateKey(String uniqueKey) { return redisTemplate.execute((RedisCallback<Boolean>) connection -> { // 监控目标键,防止事务执行期间被其他客户端修改 connection.watch(uniqueKey.getBytes()); // 先检查键是否存在 boolean exists = connection.exists(uniqueKey.getBytes()); if (exists) { connection.unwatch(); return false; } // 开启事务 connection.multi(); // 创建键并设置临时值 connection.set(uniqueKey.getBytes(), "PENDING".getBytes()); // 提交事务 var execResults = connection.exec(); // 事务执行成功返回true,否则返回false(说明键被其他客户端抢先创建) return execResults != null && !execResults.isEmpty() && (Boolean) execResults.get(0); }); } // 更新Redis键值为实体ID public void updateKeyWithEntityId(String uniqueKey, String entityId) { redisTemplate.opsForValue().set(uniqueKey, entityId); } // 查询Redis中的实体ID public String getEntityId(String uniqueKey) { return redisTemplate.opsForValue().get(uniqueKey); } }
业务层整合代码
import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class EntityService { private final RedisAtomicHelper redisHelper; private final EntityRepository entityRepo; public EntityService(RedisAtomicHelper redisHelper, EntityRepository entityRepo) { this.redisHelper = redisHelper; this.entityRepo = entityRepo; } public String saveIfNotExists(String uniqueKey, Entity entity) { // 先查Redis,存在直接返回ID String existingId = redisHelper.getEntityId(uniqueKey); if (existingId != null && !"PENDING".equals(existingId)) { return existingId; } // 原子检查并创建键 boolean keyCreated = redisHelper.checkAndCreateKey(uniqueKey); if (!keyCreated) { // 其他客户端已经创建键,重新查询返回ID return redisHelper.getEntityId(uniqueKey); } try { // 插入数据库 Entity saved = entityRepo.save(entity); // 更新Redis键为实体ID redisHelper.updateKeyWithEntityId(uniqueKey, saved.getId().toString()); return saved.getId().toString(); } catch (Exception e) { // 插入失败,删除Redis临时键,避免遗留无效状态 redisTemplate.delete(uniqueKey); throw e; } } }
方案二:原生Lettuce API实现事务
如果直接用Lettuce原生客户端,手动执行MULTI/EXEC流程:
import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; import org.springframework.stereotype.Component; @Component public class LettuceAtomicHelper { private final RedisCommands<String, String> syncCommands; public LettuceAtomicHelper(RedisClient redisClient) { StatefulRedisConnection<String, String> connection = redisClient.connect(); this.syncCommands = connection.sync(); } public Boolean checkAndCreateKey(String uniqueKey) { try { // 监控键 syncCommands.watch(uniqueKey); // 检查存在性 boolean exists = syncCommands.exists(uniqueKey) > 0; if (exists) { syncCommands.unwatch(); return false; } // 开启事务 syncCommands.multi(); // 创建临时键 syncCommands.set(uniqueKey, "PENDING"); // 提交事务 var results = syncCommands.exec(); return results != null && !results.isEmpty() && (Boolean) results.get(0); } finally { // 确保释放监控,避免影响后续操作 syncCommands.unwatch(); } } // 其他方法同方案一... }
重要注意事项
- 临时状态处理:用"PENDING"标记DB插入中的状态,避免其他客户端读取到空值或错误数据。
- 异常回滚:DB插入失败时必须删除Redis临时键,否则会导致这个键一直处于"PENDING"状态,后续请求无法正常处理。
- 重试逻辑:Redis事务是乐观锁,若EXEC返回null(说明监控的键被其他客户端修改),业务层可以添加重试机制,保证最终一致性。
- 轻量替代方案:如果不需要严格用事务,直接用
setIfAbsent(等价于SETNX)更高效:
这个命令本身就是原子操作,一行代码就能完成检查+创建,性能比事务更好。Boolean keyCreated = redisTemplate.opsForValue().setIfAbsent(uniqueKey, "PENDING");
内容的提问来源于stack exchange,提问作者user2176576
相关产品推荐
相关产品推荐

