【JAVA架构师成长之路】【Redis】第18集:Redis实现分布式高并发加减计数器

30分钟自学教程:Redis实现分布式高并发加减计数器

目标

  1. 掌握Redis原子操作实现分布式计数器的原理。
  2. 学会使用Lua脚本和Redisson处理高并发加减。
  3. 能够设计应对网络抖动、节点故障的容错方案。

教程内容

0~2分钟:分布式计数器的核心需求
  • 场景:秒杀库存扣减、实时投票计数、API调用限流等。
  • 挑战
    • 原子性:多节点并发操作需保证结果正确。
    • 高性能:支持万级QPS。
    • 容错:网络异常时数据不丢失。

2~5分钟:基础实现------INCR/DECR命令(Java示例)
java 复制代码
// 初始化计数器  
redisTemplate.opsForValue().set("product:stock:1001", "1000");  

// 原子增加  
public Long incr(String key, long delta) {  
    return redisTemplate.opsForValue().increment(key, delta);  
}  

// 原子减少  
public Long decr(String key, long delta) {  
    return redisTemplate.opsForValue().decrement(key, delta);  
}  

// 扣减库存示例  
public boolean deductStock(String productId, int quantity) {  
    Long stock = decr("product:stock:" + productId, quantity);  
    return stock != null && stock >= 0;  
}  

问题 :直接使用DECR可能扣减到负数(需结合Lua脚本解决)。


5~12分钟:解决方案1------Lua脚本保证原子性
  • 原理:通过Lua脚本在Redis服务端执行多个命令,确保原子性。
java 复制代码
// Lua脚本:仅当库存充足时扣减  
String script =  
    "local current = tonumber(redis.call('GET', KEYS[1]))\n" +  
    "if current >= tonumber(ARGV[1]) then\n" +  
    "   return redis.call('DECRBY', KEYS[1], ARGV[1])\n" +  
    "else\n" +  
    "   return -1\n" +  
    "end";  

// 执行脚本  
public Long safeDeduct(String key, int quantity) {  
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();  
    redisScript.setScriptText(script);  
    redisScript.setResultType(Long.class);  
    return redisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(quantity));  
}  

验证 :调用safeDeduct("product:stock:1001", 2),库存不足时返回-1


12~20分钟:解决方案2------Redisson分布式锁
  • 适用场景:复杂计数逻辑(如先查询再计算)。
java 复制代码
// Redisson分布式锁实现  
public boolean deductWithLock(String productId, int quantity) {  
    String key = "product:stock:" + productId;  
    RLock lock = redissonClient.getLock(key + ":lock");  
    try {  
        if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {  
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(key));  
            if (stock >= quantity) {  
                redisTemplate.opsForValue().set(key, String.valueOf(stock - quantity));  
                return true;  
            }  
            return false;  
        }  
    } finally {  
        lock.unlock();  
    }  
    return false;  
}  

注意:锁粒度应精确到资源级别(如按商品ID加锁)。


20~25分钟:解决方案3------集群模式与分片计数
  • 分片设计:将计数器按哈希分片,分散压力。
java 复制代码
// 分片计数(伪代码)  
public void incrSharded(String key, int shards, long delta) {  
    int shardId = Math.abs(key.hashCode()) % shards;  
    String shardKey = key + ":shard_" + shardId;  
    redisTemplate.opsForValue().increment(shardKey, delta);  
}  

// 总分计算(遍历所有分片求和)  
public long getTotal(String key, int shards) {  
    long total = 0;  
    for (int i = 0; i < shards; i++) {  
        String shardKey = key + ":shard_" + i;  
        String value = redisTemplate.opsForValue().get(shardKey);  
        total += Long.parseLong(value == null ? "0" : value);  
    }  
    return total;  
}  

适用场景:超高并发(如百万QPS)的全局计数器。


25~28分钟:应急处理方案
  1. 重试机制:网络抖动时自动重试。
java 复制代码
public boolean safeDeductWithRetry(String key, int quantity, int maxRetries) {  
    int retries = 0;  
    while (retries < maxRetries) {  
        try {  
            Long result = safeDeduct(key, quantity);  
            return result != null && result >= 0;  
        } catch (RedisConnectionFailureException e) {  
            retries++;  
            Thread.sleep(100);  
        }  
    }  
    return false;  
}  
  1. 降级策略:Redis不可用时降级到本地计数器。
java 复制代码
// 本地Guava缓存降级  
private Cache<String, Long> localCache = CacheBuilder.newBuilder().build();  

public long getStockWithFallback(String key) {  
    try {  
        return Long.parseLong(redisTemplate.opsForValue().get(key));  
    } catch (Exception e) {  
        return localCache.get(key, () -> 0L);  
    }  
}  
  1. 监控告警:通过Prometheus监控计数器异常。
yaml 复制代码
# Prometheus配置示例  
- job_name: 'redis_counters'  
  static_configs:  
    - targets: ['redis_host:9121']  # Redis Exporter端口  

28~30分钟:总结与优化方向
  • 核心原则:原子操作优先、分片减压、降级容灾。
  • 高级优化
    • 使用Redis Streams实现异步计数。
    • 结合数据库持久化最终结果。
    • 使用Pipelining减少网络往返。

练习与拓展

练习

  1. 实现一个分片计数器,支持动态增加分片数量。
  2. 使用Redisson锁实现"先查询库存再扣减"的原子操作。

推荐拓展

  1. 研究Redis的HyperLogLog实现基数统计。
  2. 学习Redis事务的WATCH/MULTI/EXEC机制。
  3. 探索分布式协调框架(如ZooKeeper)的计数器实现。
相关推荐
IT大白鼠2 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz2 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙2 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除2 天前
redis--018
redis
两锭子2 天前
Redis基础
redis
烟沙九洲2 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索2 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞2 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路3 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog3 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis