You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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();
        }
    }

    // 其他方法同方案一...
}

重要注意事项

  1. 临时状态处理:用"PENDING"标记DB插入中的状态,避免其他客户端读取到空值或错误数据。
  2. 异常回滚:DB插入失败时必须删除Redis临时键,否则会导致这个键一直处于"PENDING"状态,后续请求无法正常处理。
  3. 重试逻辑:Redis事务是乐观锁,若EXEC返回null(说明监控的键被其他客户端修改),业务层可以添加重试机制,保证最终一致性。
  4. 轻量替代方案:如果不需要严格用事务,直接用setIfAbsent(等价于SETNX)更高效:
    Boolean keyCreated = redisTemplate.opsForValue().setIfAbsent(uniqueKey, "PENDING");
    
    这个命令本身就是原子操作,一行代码就能完成检查+创建,性能比事务更好。

内容的提问来源于stack exchange,提问作者user2176576

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 09:58:14