【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)的计数器实现。
相关推荐
笑远2 小时前
MySQL 主主复制与 Redis 环境安装部署
redis·mysql·adb
小斌的Debug日记4 小时前
框架基本知识总结 Day16
redis·spring
morris1315 小时前
【redis】布隆过滤器的Java实现
java·redis·布隆过滤器
椰椰椰耶5 小时前
【redis】全局命令set、get、keys
数据库·redis·缓存
月落星还在5 小时前
Redis 内存淘汰策略深度解析
数据库·redis·缓存
五行星辰6 小时前
Java链接redis
java·开发语言·redis
左灯右行的爱情6 小时前
Redis- 切片集群
数据库·redis·缓存
周小闯6 小时前
Easyliev在线视频分享平台项目总结——SpringBoot、Mybatis、Redis、ElasticSearch、FFmpeg
spring boot·redis·mybatis
左灯右行的爱情11 小时前
Redis-主从架构
数据库·redis·架构