redis数量累加api,能够保证请求时间超时和保证并发安全,比如因网络抖动导致的重试,进而出现累加的幂等性问题

Redis 高并发安全累加方案:解决超时重试与幂等性问题

针对网络抖动导致的重试和并发安全需求,我将设计一个完整的幂等累加方案,确保在超时、重试和高并发场景下的数据一致性。

整体解决方案设计

核心思路

  1. 幂等控制:使用唯一请求ID防止重试导致重复累加

  2. 原子操作:Lua脚本保证操作的原子性

  3. 超时管理:客户端+服务端双重超时控制

  4. 并发安全:Redis单线程特性保证操作顺序性

deepseek_mermaid_20250804_b42aff

 

 

完整实现方案

1. 幂等累加Lua脚本

lua
 
-- KEYS[1]: 计数器key
-- KEYS[2]: 请求ID存储key (格式: idempotent:<request_id>)
-- ARGV[1]: 请求ID
-- ARGV[2]: 增量值
-- ARGV[3]: 请求记录TTL (秒)
-- ARGV[4]: 计数器TTL (秒)

-- 检查请求是否已处理
local handled = redis.call('GET', KEYS[2])
if handled then
    return {tonumber(handled), 1}  -- 返回结果和状态码1(已处理)
end

-- 处理计数器逻辑
local current
if redis.call('EXISTS', KEYS[1]) == 1 then
    current = redis.call('INCRBY', KEYS[1], ARGV[2])
else
    current = tonumber(ARGV[2])
    redis.call('SET', KEYS[1], current, 'EX', tonumber(ARGV[4]))
end

-- 记录请求结果
redis.call('SET', KEYS[2], current, 'EX', tonumber(ARGV[3]))
return {current, 0}  -- 返回结果和状态码0(新处理)

2. Java客户端实现(带重试策略)

java
 
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.util.*;

public class IdempotentRedisIncrementer {
    private final JedisPool jedisPool;
    private final int maxRetries;
    private final long retryDelayMs;
    private final int requestIdTtl;
    private final int counterTtl;

    public IdempotentRedisIncrementer(JedisPool jedisPool, int maxRetries, 
                                     long retryDelayMs, int requestIdTtl, 
                                     int counterTtl) {
        this.jedisPool = jedisPool;
        this.maxRetries = maxRetries;
        this.retryDelayMs = retryDelayMs;
        this.requestIdTtl = requestIdTtl;
        this.counterTtl = counterTtl;
    }

    public IncrementResult safeIncrement(String counterKey, long delta) {
        String requestId = UUID.randomUUID().toString();
        String requestKey = "idempotent:" + requestId;
        
        int attempt = 0;
        Jedis jedis = null;
        
        while (attempt <= maxRetries) {
            attempt++;
            try {
                jedis = jedisPool.getResource();
                
                // Lua脚本参数
                List<String> keys = Arrays.asList(counterKey, requestKey);
                List<String> args = Arrays.asList(
                    requestId,
                    String.valueOf(delta),
                    String.valueOf(requestIdTtl),
                    String.valueOf(counterTtl)
                );
                
                // 加载Lua脚本(生产环境应预加载)
                String script = loadLuaScript();
                
                // 执行脚本
                List<Long> result = (List<Long>) jedis.eval(
                    script, 
                    keys, 
                    args
                );
                
                long newValue = result.get(0);
                int status = result.get(1).intValue();
                
                return new IncrementResult(newValue, status, requestId);
            } catch (JedisConnectionException e) {
                if (jedis != null) {
                    jedis.close();
                    jedis = null;
                }
                
                if (attempt > maxRetries) {
                    throw new RedisOperationException("Redis connection failed after " + 
                                                     maxRetries + " attempts", e);
                }
                
                // 指数退避重试
                try {
                    Thread.sleep(retryDelayMs * (long) Math.pow(2, attempt - 1));
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
        throw new RedisOperationException("Unexpected error in increment operation");
    }

    private String loadLuaScript() {
        // 实际项目中应预加载脚本并缓存SHA1值
        // 此处返回脚本内容
        return "local handled = redis.call('GET', KEYS[2]) " +
               "if handled then return {tonumber(handled), 1} end " +
               "local current " +
               "if redis.call('EXISTS', KEYS[1]) == 1 then " +
               "    current = redis.call('INCRBY', KEYS[1], ARGV[2]) " +
               "else " +
               "    current = tonumber(ARGV[2]) " +
               "    redis.call('SET', KEYS[1], current, 'EX', tonumber(ARGV[4])) " +
               "end " +
               "redis.call('SET', KEYS[2], current, 'EX', tonumber(ARGV[3])) " +
               "return {current, 0}";
    }

    public static class IncrementResult {
        private final long newValue;
        private final int status; // 0:新处理, 1:幂等返回
        private final String requestId;

        public IncrementResult(long newValue, int status, String requestId) {
            this.newValue = newValue;
            this.status = status;
            this.requestId = requestId;
        }

        // Getters...
    }

    public static class RedisOperationException extends RuntimeException {
        public RedisOperationException(String message) {
            super(message);
        }
        
        public RedisOperationException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

3. 客户端使用示例

java
 
public static void main(String[] args) {
    // 配置连接池
    JedisPoolConfig poolConfig = new JedisPoolConfig();
    poolConfig.setMaxTotal(50);
    poolConfig.setMaxIdle(10);
    poolConfig.setMinIdle(5);
    poolConfig.setMaxWaitMillis(1000);
    
    JedisPool jedisPool = new JedisPool(poolConfig, "redis-host", 6379);
    
    // 创建累加器实例
    IdempotentRedisIncrementer incrementer = new IdempotentRedisIncrementer(
        jedisPool, 
        3,      // 最大重试次数
        100,    // 初始重试延迟(ms)
        3600,   // 请求ID记录TTL(1小时)
        86400   // 计数器TTL(1天)
    );
    
    // 执行安全累加
    try {
        String counterKey = "user:1234:points";
        long delta = 10;
        
        IdempotentRedisIncrementer.IncrementResult result = 
            incrementer.safeIncrement(counterKey, delta);
        
        System.out.println("累加结果: " + result.getNewValue());
        System.out.println("状态: " + (result.getStatus() == 0 ? "新处理" : "幂等返回"));
        System.out.println("请求ID: " + result.getRequestId());
    } catch (IdempotentRedisIncrementer.RedisOperationException e) {
        System.err.println("操作失败: " + e.getMessage());
    }
}

关键机制解析

1. 幂等性保障

  • 唯一请求ID:每个请求生成唯一UUID

  • 请求记录存储:使用idempotent:<request_id>存储处理结果

  • 自动过期清理:设置TTL避免存储无限增长

  • 结果缓存:对重复请求直接返回缓存结果

2. 超时与重试处理

  • 指数退避重试:retryDelayMs * 2^(attempt-1)

  • 连接池配置:合理设置maxWaitMillis防止线程阻塞

  • 最大重试次数:避免无限重试导致系统雪崩

  • 异常分类处理:区分网络异常与业务异常

3. 并发安全机制

  • Lua脚本原子性:所有操作在单脚本中完成

  • Redis单线程:天然避免并发冲突

  • 连接池线程安全:确保多线程访问安全

  • 键空间分离:计数器与幂等记录使用不同键空间

性能优化建议

  1. 脚本预加载

    java
     
    // 预加载Lua脚本
    String sha1 = jedis.scriptLoad(script);
    // 执行时使用evalsha
    jedis.evalsha(sha1, keys, args);
  2. 连接池优化

    java
     
    config.setTestWhileIdle(true);
    config.setMinEvictableIdleTimeMillis(60000);
    config.setTimeBetweenEvictionRunsMillis(30000);
  3. 批量处理

    lua
     
    -- 批量累加示例
    for i, key in ipairs(KEYS) do
        redis.call('INCRBY', key, ARGV[i])
    end
  4. 分片计数

    java
     
    // 分片键生成
    int shard = key.hashCode() % SHARD_COUNT;
    String shardedKey = "counter:" + shard + ":" + key;

监控与告警

  1. Redis监控指标

    bash
     
    # 监控命令执行情况
    redis-cli info stats | grep total_commands_processed
    
    # 内存使用情况
    redis-cli info memory | grep used_memory
    
    # 键空间信息
    redis-cli info keyspace
  2. 客户端监控

    java
     
    // 使用Micrometer监控
    Metrics.counter("redis.operation", "type", "increment")
           .increment();
    
    Timer timer = Metrics.timer("redis.latency");
    timer.record(() -> safeIncrement(key, delta));
  3. 告警规则

    • 连续重试率 > 5%

    • P99延迟 > 500ms

    • 错误率 > 1%

    • 内存使用率 > 80%

方案对比

特性基础INCR方案事务方案本幂等方案
并发性能 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
幂等性
重试安全性 ⚠️ (可能重复)
实现复杂度 ⭐⭐⭐ ⭐⭐
超时控制 ⚠️ (客户端) ✅ (双重控制)
状态存储开销 中等 (可管理)

适用场景

  1. 金融交易:账户余额变动、积分累加

  2. 库存管理:商品库存增减

  3. 实时统计:PV/UV计数

  4. 分布式任务:任务进度跟踪

  5. 限流控制:API请求计数

本方案通过幂等控制、原子脚本和智能重试机制,完美解决了Redis累加操作中的并发安全和超时重试问题,适用于高并发、高可靠性要求的业务场景。

posted @ 2025-08-04 12:00  飘来荡去evo  阅读(22)  评论(0)    收藏  举报