二阶段项目抖粉智算实战知识点:redis分布式锁

文章目录

前言

在我刚学锁的时候,因为会数据库悲观锁,一到高并发场景就踩坑:秒杀超卖、AI算力额度重复扣减、支付重复发权益。

在二阶段项目抖粉智算短视频AI营销平台中,我采用了redis分布式锁,用户额度扣减、限时秒杀下单、3D任务并发提交 全部依赖 Redis 分布式锁解决并发冲突。

本文不讲晦涩理论,我将从「为什么要用、基础实现、踩坑问题、完整实战方案、项目落地代码思路」一步步拆解,零基础也能看懂,写完直接能用在项目里。

一、先搞懂:什么是分布式锁?什么时候必须用?

1. 单机锁为什么失效?

单机项目用 synchronized、本地锁就可以控制并发,但现在项目都是微服务多实例部署

  • 支付、AI生成、秒杀拆分成独立服务;
  • 同一服务部署多台服务器做负载均衡;
    本地锁只能锁住当前服务器的请求,其他机器的请求不受限制,最终出现:
  • 秒杀商品超卖;
  • 用户算力额度多次扣减,造成资金损失;
  • 同一AI任务重复创建,重复消耗算力。

跨服务器、跨实例统一控制并发的锁,就是分布式锁

2. 什么场景需要Redis分布式锁(贴合我们实战项目)

  1. AI任务提交:并发请求同时扣用户算力额度,防止超扣;
  2. 限时秒杀活动:防止库存超卖、重复下单;
  3. 支付权益发放:支付宝重复回调时,避免多次赠送会员/算力;
  4. 定时任务执行:多服务定时任务重复执行,重复更新数据。

3. 为什么选Redis做分布式锁?

  1. Redis内存操作,读写毫秒级,高并发性能远超数据库锁;
  2. 支持自动过期,不怕程序宕机死锁;
  3. 支持Lua脚本实现原子操作,不会出现锁逻辑拆分导致并发漏洞;
  4. 项目本身已引入Redis做缓存、库存存储,无需额外中间件。

二、最简单的初级版Redis锁

核心思路

占坑思想:想操作数据时,先往Redis写入一个唯一key,代表拿到锁;操作完成后删除key释放锁。

命令:SET lock_key unique_value NX EX 过期时间

  • NX:key不存在才能设置成功,等价加锁;
  • EX:给锁设置过期时间,防止程序崩溃锁永久不释放;
伪代码逻辑(通俗易懂)
python 复制代码
# 1. 生成唯一标识,区分不同服务的锁(防止误删别人的锁)
lock_id = 随机uuid
lock_key = "user_quota_lock:10086"  # 用户10086额度锁

# 2. 尝试加锁,过期时间10秒
lock_success = redis.set(lock_key, lock_id, nx=True, ex=10)

if lock_success:
    try:
        # 执行业务:扣减用户算力额度、创建AI任务
        update_user_quota()
    finally:
        # 释放锁:先判断当前锁是自己加的,再删除
        if redis.get(lock_key) == lock_id:
            redis.delete(lock_key)
else:
    # 获取锁失败,返回提示:操作繁忙,请重试
    return "当前操作人数过多,请稍后再试"

初级版本存在3个致命问题(项目不能直接用)

  1. 业务执行超时,锁自动过期,并发问题复现
    假设锁过期时间10秒,但AI生成任务执行15秒,锁提前失效,其他请求拿到锁,出现超扣。
  2. 释放锁非原子操作,极端情况误删锁
    先get判断、再del删除,两条命令中间线程阻塞,会删掉其他线程新加上的锁。
  3. 没有重试机制,用户体验差
    抢锁失败直接返回报错,高并发下大量用户操作失败。

三、生产可用进阶方案:看门狗自动续期 + Lua释放锁

针对上面三个缺陷,我们在抖粉智算平台落地两套解决方案,分开讲解。

1. Lua脚本释放锁,保证原子性

把「判断锁归属+删除锁」合并成一条Lua脚本,Redis单线程执行,不会被其他命令打断,杜绝误删锁。

Lua脚本:

lua 复制代码
-- KEYS[1]锁key,ARGV[1]当前线程唯一标识
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

调用时传入锁key和uuid,一条命令完成释放,无并发漏洞。

2. 看门狗(自动续期机制)解决业务超时锁失效

适用场景:AI视频、3D建模等耗时任务,无法预估业务执行时长。

实现逻辑:

  1. 线程拿到锁后,开启一个异步守护线程(看门狗);
  2. 每隔 过期时间/3(如锁10秒,3秒执行一次)自动延长锁过期时间;
  3. 业务执行完毕,主线程释放锁,看门狗线程同步关闭;
  4. 如果服务宕机,看门狗停止,锁到期自动释放,不会死锁。

3. 自旋重试抢锁,提升用户体验

抢锁失败不直接返回,循环重试3次,每次间隔50ms,重试全部失败再提示繁忙。

适合秒杀、充值等高频操作,大幅降低用户报错概率。

四、高并发场景终极优化:Lua脚本加锁+业务原子操作(秒杀专用)

像秒杀库存扣减、额度扣减这种简单短逻辑,不需要看门狗,直接用Lua脚本把「判断库存、扣减库存、加锁」全部写在脚本里,一次Redis请求完成,性能拉满。

以额度扣减举例,一条Lua脚本完成:

  1. 判断用户是否持有锁;
  2. 校验用户剩余算力额度;
  3. 原子扣减额度;
  4. 生成流水记录标识。
    优势:仅一次网络IO,极大降低并发下Redis请求压力,也是我们平台秒杀模块核心方案。

五、实战避坑:新手最容易踩的5个错误

坑1:不加过期时间,程序宕机永久死锁

如果忘记EX设置过期时间,服务异常崩溃后key永久存在,所有用户永远无法操作对应数据,线上事故。

解决:所有锁必须设置过期时间。

坑2:释放锁直接del,不校验锁归属

线程A的锁过期,线程B拿到锁,此时A执行到删除锁逻辑,直接删掉B的锁,并发击穿。

解决:必须携带唯一uuid,Lua脚本校验后再删除。

坑3:过期时间设置太短,业务没执行完

短任务(文案生成)设5s,长任务(3D模型)搭配看门狗,不能统一写死过期时间。

坑4:锁粒度太粗,全部用同一个lock_key

所有用户共用一把锁,所有请求串行排队,接口响应极慢。

正确做法:按业务维度拆分key,用户额度锁 user_quota:{uid}、秒杀商品锁 flash_sale:{goods_id},不同资源互不阻塞。

坑5:分布式锁替代数据库事务

Redis锁只控制并发,不能替代MySQL事务,更新数据库数据仍要保证事务,防止数据不一致。

六、结合抖粉智算平台真实业务落地案例

场景:用户提交3D生成任务,并发扣算力额度

整体流程:

  1. 用户上传提示词提交3D生成任务;
  2. 后端生成唯一UUID,使用 user_quota_lock:{用户ID} 尝试获取分布式锁;
  3. 抢锁失败自旋重试3次,失败返回操作繁忙;
  4. 加锁成功,开启看门狗线程持续续期锁;
  5. 事务内校验用户剩余算力,扣减对应额度,生成扣减流水;
  6. 通过RabbitMQ异步推送3D生成任务;
  7. AI任务执行完成,主线程通过Lua脚本释放锁,关闭看门狗;
  8. 若中途服务崩溃,锁10秒后自动过期,不会阻塞其他用户。

场景:限时秒杀算力额度

秒杀属于短逻辑,无需看门狗,直接Lua脚本原子操作:

  1. 活动预热库存存入Redis;
  2. 用户发起秒杀请求,执行Lua脚本:判断锁→校验库存→扣库存;
  3. 扣减成功推送订单到MQ,异步同步MySQL库存;
  4. 脚本自带过期时间,无需额外续期,百万并发稳定运行。

七、Redis分布式锁 vs 数据库悲观/乐观锁 实战对比

锁类型 并发性能 死锁风险 适用场景 项目选择
MySQL悲观锁 差,串行阻塞 极低并发内部后台 不用于秒杀、额度扣减
MySQL乐观锁 中等,冲突需要重试 支付回调、订单更新 辅助兜底,不做主锁
Redis分布式锁 极高,内存操作 极低(有过期兜底) 秒杀、AI额度、高并发任务 平台核心并发解决方案

八、总结(记忆要点)

  1. 分布式场景本地锁失效,必须使用Redis分布式锁;
  2. 基础加锁命令 SET key uuid NX EX time,过期时间必不可少;
  3. 释放锁要用Lua脚本,校验唯一标识,防止误删;
  4. 长耗时AI任务搭配看门狗自动续期,避免锁提前失效;
  5. 短并发秒杀场景直接Lua脚本完成全部业务逻辑,性能最优;
  6. 锁按业务维度拆分key,细化粒度,提升系统吞吐量;
  7. 不能忽略重试、过期、宕机兜底三大异常场景,上线前完整测试。