Redis中原子性操作的的实现
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、Redis 原子性操作的本质:为什么 Redis 能保证原子性?</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">1.1 底层原理:单线程模型 + 命令队列</a></li><li><a href="#_lab2_0_1">1.2 原子性的边界:单个命令 vs 多个命令</a></li><ul class="third_class_ul"><li><a href="#_label3_0_1_0">典型问题示例</a></li><li><a href="#_label3_0_1_1">解决方案对比</a></li></ul></ul><li><a href="#_label1">二、Redis 核心原子操作分类与实践</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_2">2.1 基础数据结构的原子操作</a></li><ul class="third_class_ul"><li><a href="#_label3_1_2_2">数据结构详解与扩展应用场景</a></li><li><a href="#_label3_1_2_3">用户登录状态存储的进阶实现</a></li></ul><li><a href="#_lab2_1_3">2.2 计数器与自增操作</a></li><ul class="third_class_ul"><li><a href="#_label3_1_3_4">INCR系列命令的底层原理</a></li><li><a href="#_label3_1_3_5">计数器的高级应用模式</a></li></ul><li><a href="#_lab2_1_4">2.3 分布式锁的完整实现方案</a></li><ul class="third_class_ul"><li><a href="#_label3_1_4_6">分布式锁的演进过程</a></li><li><a href="#_label3_1_4_7">生产环境最佳实践</a></li><li><a href="#_label3_1_4_8">集群环境特殊考量</a></li></ul></ul><li><a href="#_label2">三、多命令原子性实现:事务与 Lua 脚本</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_5">3.1 MULTI/EXEC 事务:弱一致性的批量执行</a></li><ul class="third_class_ul"><li><a href="#_label3_2_5_9">事务执行流程详解</a></li><li><a href="#_label3_2_5_10">事务的局限性详解</a></li><li><a href="#_label3_2_5_11">适用场景</a></li></ul><li><a href="#_lab2_2_6">3.2 Lua 脚本:强一致性的原子执行</a></li><ul class="third_class_ul"><li><a href="#_label3_2_6_12">Lua 脚本的核心优势</a></li><li><a href="#_label3_2_6_13">实践案例:库存扣减(避免超卖)</a></li><li><a href="#_label3_2_6_14">脚本缓存优化</a></li><li><a href="#_label3_2_6_15">适用场景</a></li><li><a href="#_label3_2_6_16">注意事项</a></li></ul></ul><li><a href="#_label3">四、Redis 原子操作的常见问题与避坑指南</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_7">4.1 坑点 1:混淆 "单命令原子性" 与 "多命令原子性"</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_3_8">4.2 坑点 2:分布式锁未设置过期时间</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_3_9">4.3 坑点 3:Lua 脚本执行效率低下</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_3_10">4.4 坑点 4:使用 INCR 实现分布式 ID 时的溢出问题</a></li><ul class="third_class_ul"></ul></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一、Redis 原子性操作的本质:为什么 Redis 能保证原子性?</h2><p>首先需要明确一个关键概念:Redis 的原子性是指单个命令的执行是 "不可中断" 的—— 当一个命令开始执行后,直到其执行完毕,Redis 不会中断它去执行其他命令。这种特性并非 Redis 独创,而是基于其单线程模型的天然优势。</p>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>1.1 底层原理:单线程模型 + 命令队列</h3>
<p>Redis 采用单线程事件循环模型处理客户端请求,这种设计架构主要由以下几个关键组件构成:</p>
<ol><li><strong>I/O 多路复用</strong>:Redis 使用 epoll/kqueue/select 等系统调用来高效处理大量网络连接</li><li><strong>命令队列</strong>:所有客户端请求都会被序列化到一个全局内存队列中</li><li><strong>单线程事件循环</strong>:主线程按 "先进先出(FIFO)" 的顺序从队列中取出命令执行</li></ol>
<p>这种设计从根本上保证了:</p>
<ul><li><strong>命令执行的独占性</strong>:每个命令在执行期间独占 CPU 资源</li><li><strong>状态一致性</strong>:命令执行的结果不会出现 "部分完成" 的中间状态</li><li><strong>操作完整性</strong>:完整的操作序列不会被其他命令打断</li></ul>
<p><strong>典型应用场景示例</strong>: 当执行 <code>INCR key</code> 命令时,Redis 会严格按照以下顺序完整执行:</p>
<ol><li>从内存中读取 key 的当前值(假设为 5)</li><li>在 CPU 寄存器中执行加 1 操作(5 → 6)</li><li>将新值(6)写回内存</li><li>返回结果给客户端</li></ol>
<p>在此期间,即使有 100 个客户端同时发送 <code>INCR</code> 命令,Redis 也会将它们排队处理,确保每个 <code>INCR</code> 操作都能正确累加。</p>
<p class="maodian"><a name="_lab2_0_1"></a></p><h3>1.2 原子性的边界:单个命令 vs 多个命令</h3>
<p>需要特别注意的是:Redis 仅保证 "单个命令" 的原子性,多个命令的组合并不天然具备原子性。理解这一点对设计可靠的 Redis 应用至关重要。</p>
<p class="maodian"><a name="_label3_0_1_0"></a></p><h4>典型问题示例</h4>
<div class="jb51code"><pre class="brush:sql;"># 以下两个命令组合不具备原子性
GET key1# 步骤1:读取key1
SET key2 value2# 步骤2:写入key2
</pre></div>
<p><strong>潜在风险场景</strong>:</p>
<ol><li>客户端A执行 <code>GET key1</code> 获取值为 100</li><li>此时客户端B修改了 key1 的值为 200</li><li>客户端A继续执行 <code>SET key2 value2</code></li><li>结果:客户端A基于已过期的 key1 值做出了错误决策</li></ol>
<p class="maodian"><a name="_label3_0_1_1"></a></p><h4>解决方案对比</h4>
<table><thead><tr><th>方案</th><th>实现方式</th><th>适用场景</th><th>性能影响</th></tr></thead><tbody><tr><td>事务(MULTI/EXEC)</td><td>将多个命令打包执行</td><td>简单的命令组合</td><td>中等,需要排队</td></tr><tr><td>Lua脚本</td><td>原子执行复杂逻辑</td><td>需要条件判断的业务</td><td>较高,需要解析脚本</td></tr><tr><td>WATCH</td><td>乐观锁机制</td><td>需要检测变化的场景</td><td>较高,可能重试</td></tr></tbody></table>
<p><strong>Lua 脚本示例</strong>:</p>
<div class="jb51code"><pre class="brush:plain;">-- 原子性地检查并设置值
if redis.call("GET", KEYS) == ARGV then
return redis.call("SET", KEYS, ARGV)
else
return 0
end
</pre></div>
<p><strong>最佳实践建议</strong>:</p>
<ol><li>对于简单的计数器场景,优先使用原生原子命令(INCR/DECR 等)</li><li>需要组合不超过5个命令时,使用 MULTI/EXEC 事务</li><li>复杂业务逻辑(包含条件判断)必须使用 Lua 脚本</li><li>对性能敏感的场景,提前测试不同方案的 QPS 表现</li></ol>
<p class="maodian"><a name="_label1"></a></p><h2>二、Redis 核心原子操作分类与实践</h2>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>2.1 基础数据结构的原子操作</h3>
<p class="maodian"><a name="_label3_1_2_2"></a></p><h4>数据结构详解与扩展应用场景</h4>
<ol><li><p><strong>String类型</strong></p>
<ul><li><code>SETNX</code> 命令扩展应用:
<ul><li>实现分布式锁的基础原语</li><li>用户首次登录初始化配置</li><li>防止缓存击穿(当缓存失效时,只允许一个请求去查询数据库)</li></ul></li><li><code>GETSET</code> 典型使用场景:<ul><li>系统维护状态切换(获取当前状态并更新为新状态)</li><li>实现简单的消息队列(配合<code>LPUSH</code>使用)</li></ul></li></ul></li><li><p><strong>Hash类型</strong></p>
<ul><li><code>HSET</code> 高级用法:
<ul><li>用户会话管理(存储多个会话属性)</li><li>商品详情缓存(避免序列化/反序列化整个对象)</li></ul></li><li><code>HINCRBY</code> 实际案例:<ul><li>电商平台商品库存扣减(保证库存准确性)</li><li>论坛帖子点赞计数</li></ul></li></ul></li><li><p><strong>List类型</strong></p>
<ul><li>高级队列模式:<ul><li>阻塞式队列(<code>BLPOP</code>/<code>BRPOP</code>)</li><li>循环队列(<code>LINDEX</code>+<code>LPUSH</code>)</li></ul></li><li>典型应用:<ul><li>最新消息展示(固定长度列表)</li><li>任务调度系统</li></ul></li></ul></li><li><p><strong>Set类型</strong></p>
<ul><li>扩展功能:<ul><li>共同好友计算(<code>SINTER</code>)</li><li>数据去重处理</li></ul></li><li>实际案例:<ul><li>用户标签系统</li><li>抽奖活动参与者管理</li></ul></li></ul></li><li><p><strong>ZSet类型</strong></p>
<ul><li>高级应用:<ul><li>延迟队列(使用时间戳作为score)</li><li>热点数据统计</li></ul></li><li>典型场景:<ul><li>游戏排行榜</li><li>优先级任务调度</li></ul></li></ul></li></ol>
<p class="maodian"><a name="_label3_1_2_3"></a></p><h4>用户登录状态存储的进阶实现</h4>
<div class="jb51code"><pre class="brush:java;">// 高级登录状态管理
public boolean setLoginStatus(String userId, String deviceId) {
String key = "user:" + userId + ":session";
String value = deviceId + ":" + System.currentTimeMillis();
// 使用SET命令的完整参数
String result = jedis.set(key, value,
"NX",// 仅当key不存在时设置
"EX",// 设置过期时间单位秒
3600,// 1小时过期
"GET"// 返回旧值(如果存在)
);
if (result != null) {
// 处理旧设备踢出逻辑
handleOldDeviceLogout(result);
}
return "OK".equals(result);
}
</pre></div>
<p class="maodian"><a name="_lab2_1_3"></a></p><h3>2.2 计数器与自增操作</h3>
<p class="maodian"><a name="_label3_1_3_4"></a></p><h4>INCR系列命令的底层原理</h4>
<p>Redis实现原子自增的方式:</p>
<ol><li>单线程模型保证命令串行执行</li><li>内存操作避免磁盘I/O延迟</li><li>特殊编码优化(当值较小时使用更紧凑的存储格式)</li></ol>
<p class="maodian"><a name="_label3_1_3_5"></a></p><h4>计数器的高级应用模式</h4>
<ol><li><p><strong>滑动窗口限流</strong></p>
<div class="jb51code"><pre class="brush:java;">-- Lua脚本实现滑动窗口限流
local current_time = redis.call('TIME')
local window_size = 60
local max_requests = 100
local key = KEYS
-- 清除过期记录
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)
-- 获取当前请求数
local count = redis.call('ZCARD', key)
if count >= tonumber(ARGV) then
return 0
end
-- 添加当前请求记录
redis.call('ZADD', key, current_time, current_time..math.random())
redis.call('EXPIRE', key, window_size)
return 1
</pre></div></li><li><p><strong>分布式ID生成器</strong></p>
<div class="jb51code"><pre class="brush:java;">// Twitter的Snowflake算法变种实现
public Long generateId(String bizType) {
String key = "id_generator:" + bizType;
long timestamp = System.currentTimeMillis();
// 获取序列号并自增
long sequence = jedis.incr(key);
jedis.expire(key, 3600);
return ((timestamp - 1288834974657L) << 22)
| (datacenterId << 17)
| (workerId << 12)
| (sequence % 4096);
}
</pre></div></li><li><p><strong>精确计数与基数统计</strong></p>
<ul><li>小数据量:直接使用INCR</li><li>大数据量:结合HyperLogLog进行基数估算</li></ul></li></ol>
<p class="maodian"><a name="_lab2_1_4"></a></p><h3>2.3 分布式锁的完整实现方案</h3>
<p class="maodian"><a name="_label3_1_4_6"></a></p><h4>分布式锁的演进过程</h4>
<ol><li><p><strong>基础版本</strong></p>
<div class="jb51code"><pre class="brush:plain;">SET lock:resource unique_value NX EX 30
</pre></div></li><li><p><strong>改进版本(解决锁续期问题)</strong></p>
<div class="jb51code"><pre class="brush:java;">// 加锁
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
// 启动守护线程定期续期
new Thread(() -> {
while (locked) {
jedis.expire(lockKey, expireTime/1000);
Thread.sleep(expireTime/3);
}
}).start();
</pre></div></li><li><p><strong>Redlock算法实现</strong></p>
<div class="jb51code"><pre class="brush:java;">// 多节点加锁
List<Jedis> jedisList = getRedisNodes();
int successCount = 0;
long startTime = System.currentTimeMillis();
for (Jedis jedis : jedisList) {
if (jedis.set(lockKey, value, "NX", "PX", expireTime) != null) {
successCount++;
}
}
// 检查是否在大多数节点上加锁成功
boolean locked = successCount >= (jedisList.size()/2 + 1);
</pre></div></li></ol>
<p class="maodian"><a name="_label3_1_4_7"></a></p><h4>生产环境最佳实践</h4>
<ol><li><p><strong>锁粒度控制</strong></p>
<ul><li>细粒度锁:按业务ID拆分(如order:123)</li><li>粗粒度锁:全局资源保护</li></ul></li><li><p><strong>异常处理</strong></p>
<div class="jb51code"><pre class="brush:java;">try {
if (acquireLock()) {
// 业务逻辑
}
} finally {
// 确保释放锁
releaseLock();
}
</pre></div></li><li><p><strong>性能优化</strong></p>
<ul><li>避免长时间持有锁</li><li>使用tryLock模式(带超时)</li><li>锁分段技术提升并发</li></ul></li><li><p><strong>锁监控</strong></p>
<div class="jb51code"><pre class="brush:sql;"># 监控锁状态
redis-cli --latency -h 127.0.0.1 -p 6379
redis-cli slowlog get
</pre></div></li></ol>
<p class="maodian"><a name="_label3_1_4_8"></a></p><h4>集群环境特殊考量</h4>
<ol><li><p><strong>主从切换问题</strong></p>
<ul><li>使用Redlock算法</li><li>监控主从同步延迟</li></ul></li><li><p><strong>多数据中心部署</strong></p>
<ul><li>跨机房延迟评估</li><li>本地缓存与分布式锁结合</li></ul></li><li><p><strong>锁服务降级方案</strong></p>
<ul><li>本地锁降级</li><li>乐观锁替代</li><li>熔断机制</li></ul></li></ol>
<p class="maodian"><a name="_label2"></a></p><h2>三、多命令原子性实现:事务与 Lua 脚本</h2>
<p>当需要多个命令组合实现原子性时,Redis 提供了两种方案:MULTI/EXEC事务和Lua 脚本。下面对比两者的差异与适用场景。</p>
<p class="maodian"><a name="_lab2_2_5"></a></p><h3>3.1 MULTI/EXEC 事务:弱一致性的批量执行</h3>
<p>Redis 事务并非传统数据库的 ACID 事务,其核心特性是 "批量执行 + 要么全部执行,要么全部不执行"(但不支持回滚)。</p>
<p class="maodian"><a name="_label3_2_5_9"></a></p><h4>事务执行流程详解</h4>
<ol><li><strong>MULTI</strong>:标记事务开始,后续命令进入队列</li><li><strong>命令入队</strong>:所有操作命令不会被立即执行,而是返回"QUEUED"状态</li><li><strong>EXEC</strong>:执行所有队列中的命令</li><li><strong>DISCARD</strong>:可选操作,用于取消事务</li></ol>
<div class="jb51code"><pre class="brush:sql;">127.0.0.1:6379> MULTI# 开启事务
OK
127.0.0.1:6379> INCR counter:user# 命令1:用户数+1
QUEUED# 命令入队,未执行
127.0.0.1:6379> SET user:1002:status "active"# 命令2:设置用户状态
QUEUED
127.0.0.1:6379> EXEC# 执行事务,所有命令原子性执行
1) (integer) 101# INCR命令结果
2) OK# SET命令结果
</pre></div>
<p class="maodian"><a name="_label3_2_5_10"></a></p><h4>事务的局限性详解</h4>
<ol><li><p><strong>不支持回滚</strong>:</p>
<ul><li>语法错误:事务中某个命令语法错误(如错误的命令名),整个事务都不会执行</li><li>运行时错误:如对字符串执行INCR操作,错误命令会失败,但其他命令仍会执行</li></ul></li><li><p><strong>弱隔离性</strong>:</p>
<ul><li>事务执行期间会阻塞其他客户端命令</li><li>但事务内的命令是"非原子性入队"的(即入队时不执行,执行时才获取数据)</li><li>可能出现"WATCH"失效问题</li></ul></li><li><p><strong>无法处理并发冲突</strong>:</p>
<ul><li>没有类似数据库的乐观锁机制</li><li>两个事务同时修改同一key时,后执行的会覆盖先执行的结果</li></ul></li></ol>
<p class="maodian"><a name="_label3_2_5_11"></a></p><p class="maodian"><a name="_label3_2_6_15"></a></p><h4>适用场景</h4>
<ul><li>需要批量执行多个命令,且不要求严格的事务隔离性</li><li>简单的计数器更新、状态标记等场景</li><li>配合WATCH实现简单的乐观锁控制</li></ul>
<p class="maodian"><a name="_lab2_2_6"></a></p><h3>3.2 Lua 脚本:强一致性的原子执行</h3>
<p>Redis 支持通过 Lua 脚本执行自定义逻辑,且整个 Lua 脚本的执行过程是原子性的—— 脚本执行期间,Redis 不会中断或执行其他命令。这使得 Lua 脚本成为实现复杂原子逻辑的最佳选择。</p>
<p class="maodian"><a name="_label3_2_6_12"></a></p><h4>Lua 脚本的核心优势</h4>
<ol><li><p><strong>完整的原子性</strong>:</p>
<ul><li>脚本作为一个整体执行,不会被其他命令打断</li><li>所有操作要么全部成功,要么全部失败</li></ul></li><li><p><strong>丰富的逻辑控制</strong>:</p>
<ul><li>支持条件判断(if...else)</li><li>支持循环(for/while)</li><li>支持变量和复杂计算</li></ul></li><li><p><strong>网络效率高</strong>:</p>
<ul><li>多个命令打包成脚本,只需一次网络往返</li><li>特别适合高延迟环境</li></ul></li></ol>
<p class="maodian"><a name="_label3_2_6_13"></a></p><h4>实践案例:库存扣减(避免超卖)</h4>
<p>某商品库存初始值为 100,需实现 "用户下单时原子性扣减库存,库存不足时返回失败":</p>
<div class="jb51code"><pre class="brush:plain;">-- Lua脚本:KEYS为库存key,ARGV为扣减数量
local stock = redis.call('get', KEYS)
if not stock or tonumber(stock) < tonumber(ARGV) then
return 0# 库存不足,扣减失败
end
return redis.call('decrby', KEYS, ARGV)# 原子性扣减库存
</pre></div>
<p>Java调用示例:</p>
<div class="jb51code"><pre class="brush:java;">String luaScript = "local stock = redis.call('get', KEYS)\n" +
"if not stock or tonumber(stock) < tonumber(ARGV) then\n" +
" return 0\n" +
"end\n" +
"return redis.call('decrby', KEYS, ARGV)";
List<String> keys = Collections.singletonList("stock:goods:1001");
List<String> args = Collections.singletonList("1");
// 执行脚本
Long result = (Long) jedis.eval(luaScript, keys, args);
if (result == 0) {
System.out.println("库存不足");
} else {
System.out.println("库存扣减成功,剩余库存:" + result);
}
</pre></div>
<p class="maodian"><a name="_label3_2_6_14"></a></p><h4>脚本缓存优化</h4>
<p>Redis会缓存执行过的脚本(通过SHA1校验和),后续可通过<code>evalsha</code>调用:</p>
<div class="jb51code"><pre class="brush:sql;"># 首次执行
127.0.0.1:6379> script load "return redis.call('get', KEYS)"
"a5a06e6a8a4b4a5a5a5a5a5a5a5a5a5a5a5a5a5"
# 后续执行
127.0.0.1:6379> evalsha a5a06e6a8a4b4a5a5a5a5a5a5a5a5a5a5a5a5 1 mykey
"value"
</pre></div>
<h4>适用场景</h4>
<ul><li>需要严格原子性的复杂操作(如库存扣减、秒杀)</li><li>需要条件判断的多步骤操作</li><li>高频操作需要减少网络开销的场景</li><li>分布式锁的实现(包含锁的获取、续期和释放)</li></ul>
<p class="maodian"><a name="_label3_2_6_16"></a></p><h4>注意事项</h4>
<ol><li>脚本执行时间不宜过长(默认5秒超时)</li><li>避免在脚本中执行耗时操作</li><li>脚本应保持简单,避免复杂计算</li></ol>
<p class="maodian"><a name="_label3"></a></p><h2>四、Redis 原子操作的常见问题与避坑指南</h2>
<p>即使掌握了原子操作的用法,在实际开发中仍可能因细节处理不当导致问题。下面总结 4 个高频坑点及解决方案,并提供具体优化建议。</p>
<p class="maodian"><a name="_lab2_3_7"></a></p><h3>4.1 坑点 1:混淆 "单命令原子性" 与 "多命令原子性"</h3>
<p><strong>问题现象</strong>: 在电商秒杀场景中,开发者错误地认为多个独立命令的组合具有原子性。例如以下库存扣减逻辑:</p>
<div class="jb51code"><pre class="brush:java;"># 错误示例:判断库存>0后扣减(非原子操作)
if redis.call('get', 'stock:1001') > 0 then
redis.call('decr', 'stock:1001')# 可能出现并发时库存为负
end
</pre></div>
<p><strong>问题原因</strong>:</p>
<ul><li><code>get</code>和<code>decr</code>是两个独立命令,中间可能插入其他请求</li><li>在高并发场景下,多个请求可能同时判断库存为正,导致"超卖"现象</li></ul>
<p><strong>解决方案</strong>:</p>
<ol><li>使用 Lua 脚本将"判断+扣减"封装为原子操作(完整示例见3.2节)</li><li>或者直接使用<code>DECR</code>命令的返回值判断(返回减后的值,若为负则不允许扣减)</li></ol>
<p class="maodian"><a name="_lab2_3_8"></a></p><h3>4.2 坑点 2:分布式锁未设置过期时间</h3>
<p><strong>典型场景</strong>: 在分布式任务调度系统中,使用Redis实现分布式锁时出现以下问题:</p>
<div class="jb51code"><pre class="brush:plain;"># 错误加锁方式(未设置过期时间)
SET lock:order_123 true NX
</pre></div>
<p><strong>风险分析</strong>:</p>
<ul><li>若客户端崩溃或网络异常,锁将永远无法释放</li><li>其他客户端将无法获取锁,导致系统死锁</li><li>需要人工介入删除key才能恢复</li></ul>
<p><strong>最佳实践</strong>:</p>
<ol><li>必须使用带过期时间的加锁命令:<div class="jb51code"><pre class="brush:plain;">SET lock:order_123 true NX EX 10
</pre></div></li><li>过期时间设置原则:<ul><li>大于业务执行的最大耗时(如业务最多执行5秒,设10秒)</li><li>建议设置自动续期机制(如Redisson的watchdog)</li></ul></li><li>配合唯一标识实现安全解锁:<div class="jb51code"><pre class="brush:plain;">if redis.call("get",KEYS) == ARGV then
return redis.call("del",KEYS)
else
return 0
end
</pre></div></li></ol>
<p class="maodian"><a name="_lab2_3_9"></a></p><h3>4.3 坑点 3:Lua 脚本执行效率低下</h3>
<p><strong>性能问题案例</strong>: 某社交平台在用户Feed流处理脚本中,包含以下低效操作:</p>
<div class="jb51code"><pre class="brush:java;">-- 低效脚本示例:遍历所有粉丝进行计数
local followers = redis.call('SMEMBERS', 'user:'..userId..':followers')
local count = 0
for i, follower in ipairs(followers) do
count = count + redis.call('SCARD', 'user:'..follower..':posts')
end
return count
</pre></div>
<p><strong>影响分析</strong>:</p>
<ul><li>Redis单线程模型下,脚本执行会阻塞其他命令</li><li>当粉丝量达百万级时,脚本执行可能超过1秒</li><li>导致Redis整体吞吐量下降,QPS骤降</li></ul>
<p><strong>优化建议</strong>:</p>
<ol><li>脚本优化原则:<ul><li>避免大数据集遍历(改用SCAN分批处理)</li><li>复杂计算移到客户端(如排序、聚合)</li><li>单个脚本执行时间控制在10ms内</li></ul></li><li>改进方案:<ul><li>使用Redis的<code>SCARD</code>命令直接获取集合基数</li><li>或改用客户端分批查询后聚合</li></ul></li></ol>
<p class="maodian"><a name="_lab2_3_10"></a></p><h3>4.4 坑点 4:使用 INCR 实现分布式 ID 时的溢出问题</h3>
<p><strong>问题背景</strong>: 某物联网平台使用Redis生成设备ID:</p>
<div class="jb51code"><pre class="brush:plain;">INCR device:id_counter
</pre></div>
<p><strong>潜在风险</strong>:</p>
<ul><li>Redis计数器最大值为2^63-1(约9e18)</li><li>假设每天生成1亿ID,约需2.5亿年才会溢出</li><li>但某些高频场景(如日志ID)可能快速耗尽</li></ul>
<p><strong>解决方案</strong>:</p>
<ol><li>组合ID生成方案:<div class="jb51code"><pre class="brush:plain;"># 时间戳(41bit) + 机器ID(10bit) + 序列号(12bit)
INCR id:20230101# 每日重置计数器
</pre></div></li><li>定期重置机制:<div class="jb51code"><pre class="brush:plain;">EXPIRE id_counter 86400# 每日自动过期
</pre></div></li><li>分片方案:<div class="jb51code"><pre class="brush:plain;">INCR id_counter:{shard1}# 按业务分片使用不同key
</pre></div></li></ol>
<p><strong>监控建议</strong>:</p>
<ul><li>对关键计数器设置监控告警</li><li>当计数值超过阈值时自动告警</li><li>定期检查计数器增长趋势</li></ul>
頁:
[1]