30分钟自学教程:Redis实现分布式高并发加减计数器
目标
- 掌握Redis原子操作实现分布式计数器的原理。
- 学会使用Lua脚本和Redisson处理高并发加减。
- 能够设计应对网络抖动、节点故障的容错方案。
教程内容
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分钟:应急处理方案
- 重试机制:网络抖动时自动重试。
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;
}
- 降级策略: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);
}
}
- 监控告警:通过Prometheus监控计数器异常。
yaml
# Prometheus配置示例
- job_name: 'redis_counters'
static_configs:
- targets: ['redis_host:9121'] # Redis Exporter端口
28~30分钟:总结与优化方向
- 核心原则:原子操作优先、分片减压、降级容灾。
- 高级优化 :
- 使用Redis Streams实现异步计数。
- 结合数据库持久化最终结果。
- 使用Pipelining减少网络往返。
练习与拓展
练习
- 实现一个分片计数器,支持动态增加分片数量。
- 使用Redisson锁实现"先查询库存再扣减"的原子操作。
推荐拓展
- 研究Redis的HyperLogLog实现基数统计。
- 学习Redis事务的WATCH/MULTI/EXEC机制。
- 探索分布式协调框架(如ZooKeeper)的计数器实现。