news 2026/10/22 4:27:34

Redis分布式锁从原理到实践:SETNX、Redisson与RedLock全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis分布式锁从原理到实践:SETNX、Redisson与RedLock全解析

搞后端几年,分布式锁算是我见过的“看起来简单、踩坑最多”的技术之一。以前我也不以为意,觉得无非就是 Redis 的 SETNX,后来在真高并发场景里线上事故出过一两次,才开始老老实实地把整套原理捡起来研究。这篇就把我这些年用 Redis 实现分布式锁的经验完整梳理一遍,从最简单的 SET NX 写法,到工程上常用的 Redisson,再到争议很大的 RedLock 算法,一次性讲清楚,顺便把值得注意的坑都列出来。

1. 分布式锁解决的是哪一类问题

在解释分布式锁之前,先明确它解决的是什么问题。很多人一上来就背命令、套代码,却说不清为什么需要这个东西,结果方案选型全靠抄,出问题也不知道往哪个方向排查。

1.1 单机锁为什么解决不了分布式场景

传统单体应用,多个线程同时操作共享数据时,用语言内置的锁就能保证互斥。Java 里就是 synchronized、ReentrantLock,这些锁的底层依赖 JVM 的内存模型和操作系统同步指令,只能锁住当前 JVM 进程内的线程。一旦应用拆成多个实例,或者同一个服务在多台机器上部署,每个 JVM 各自维护一套锁标记,就完全无法感知其他实例上的线程。A 机器的线程和 B 机器的线程同时执行同一段代码,单机锁根本起不了作用。

我最早接触这个场景是在一个订单系统里。早期单体应用,库存扣减用 synchronized 锁住方法,一切正常。后来服务拆分,支付、库存、订单拆成独立服务,库存接口被多个实例同时调用,问题马上暴露:同一个订单被两个实例同时处理,库存扣成了负数。当时第一反应是找数据库锁或者直接的原子更新,但这会儿才意识到,跨进程的互斥需要一个所有实例都能访问的“协调者”。

1.2 分布式锁必须具备的核心素质

分布式锁就是要让多个进程之间保持互斥访问,一个简单理解就是:所有实例都去同一个地方登记,谁登记成功谁就执行,其他人只能等待或者放弃。

一个合格的分布式锁至少应该满足这些条件:

  • 互斥性:同一时刻只能有一个客户端持有锁,这是最核心的要求。
  • 安全释放:锁的持有者崩溃或网络异常时,锁也能自动过期,不会变成死锁。
  • 可靠释放:正常业务执行完后释放锁,不会误删其他客户端的锁。
  • 可用性:锁服务本身要稳定,如果 Redis 挂了,锁还能不能继续工作,需要一个兜底方案。
  • 高性能:加锁和解锁的操作要快,不能成为业务链路的瓶颈。
  • 可重入性:同一个线程可以多次获取同一把锁,对应 Java 里的重入锁概念。

这些条件并不是一开始都要满足,而是根据业务场景逐步叠加。比如一个小项目只需要互斥和超时释放,但如果介入支付对账、库存扣减等高一致性业务,光满足前面两项根本不够。

Redis 能在分布式锁里成为主角,主要原因就三点:高性能的内存读写、SET/PEXPIRE 这类原子命令、以及大多数公司已经有成熟的 Redis 基础设施。后面讲实现时,你会看到这些特性如何在代码里落地。

2. 最基础的 Redis 锁实现:从 SETNX 到 Lua 脚本

这一部分从零开始写一把最简单的锁,再逐步修复其中的问题。这条路线其实也是分布式锁发展的一个小缩影。

2.1 老式的 SETNX 加锁为什么不靠谱

很多文章会把 setnx 与 expire 两条命令组合说明:先 setnx 加锁,再 expire 设置过期时间。但这个方案有很大问题。假如执行完 setnx 后,进程在设置 expire 之前崩溃或网络异常,锁就永远存在。这个缺陷导致后续其他线程永远无法获取锁,也就是所谓的“死锁”。

所以 Redis 后来推出了原子命令:SET key value NX PX 30000。它一次性把“如果不存在才设置”和“过期时间 30 秒”合在一起执行,从根子上解决了两条命令之间的窗口期问题。这也是我后来一直用的方式。

具体命令就一行:

SET lock:order:1001 7e5f2c9d NX PX 30000

几个参数看看就明白:NX 表示只有 key 不存在时才设置;PX 30000 表示 30 秒过期;lock:order:1001 是锁的 key,粒度按订单维度;7e5f2c9d 是 value,它必须是一个能被当前线程识别的唯一标识,用来后续安全释放锁。有人会问,value 随便写个 1 行不行?这个问题的答案直接在 2.2 里说。

2.2 解锁时的“删除陷阱”

直接用 DEL 删除锁 key,表面上没问题,实际埋着大坑。考虑一个经典线程切换场景:线程 A 获取锁,value 是 abc,业务执行时间超过了锁过期时间。锁过期自动释放后,线程 B 获取同一把锁,value 是 def。此时线程 A 业务终于执行完,调用 DEL 删除 lock:order:1001,这个 DEL 删掉的是线程 B 的锁。线程 C 随即拿到锁,B 和 C 同时进入临界区,分布式锁失效。

这种事故在生产环境里非常容易发生,尤其是 JVM GC 卡顿、外部接口耗时不稳定的时候。A 的删除操作会无条件清掉 B 的锁,随后 C 能成功加锁,系统就出现两个客户端同时持锁的严重后果。

解决办法有两个关键词:唯一标识 + Lua 脚本。加锁时 value 用 UUID 或“实例IP + 线程ID + 随机数”拼起来的标识;解锁时先 GET 判断 value 是否等于自己的标识,相等才 DEL。但 GET 和 DEL 之间也有时间窗口,所以这两步必须通过 Lua 脚本原子执行。

2.3 Lua 脚本保证“判断+删除”的原子性

解锁脚本如下:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

这段脚本的理解成本很低:先获取锁 key 的值,如果和传入的标识 ARGV[1] 一致,说明这把锁确实是当前线程持有的,直接 DEL;如果不一致,说明锁是别人的,删除失败。因为整个判断和删除都由 Redis 引擎执行,中间没有任何 Redis 命令插入,所以不会出现“别人锁被删”的竞态问题。

在 Java 里配合 Jedis 或者 Spring 的 RedisTemplate,一个能用于生产的简易分布式锁就出来了:

String lockKey = "lock:order:1001"; String requestId = UUID.randomUUID().toString(); Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(success)) { try { // 执行业务逻辑 } finally { redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(lockKey), requestId); } }

这里有几个细节值得注意:

  • lockKey 的选择不能太粗也不能太细,太粗会把不同业务串行化,太细会造成 Redis key 数量暴涨。
  • requestId 必须保证每条线程都不一样,可以用 UUID,也可以组合 IP、线程 ID。
  • finally 里释放锁,是因为不管业务是否异常,锁都必须释放,否则会产生死锁。
  • 30 秒过期时间是一个最保守的估计。如果业务平均耗时 200 毫秒,给 30 秒毫无问题;如果业务偶尔要执行 5 分钟,30 秒就不够用。

2.4 这套“手工锁”的短板

手工实现到这一步,已经能解决最基本的互斥和安全释放了,但它还有几个明显的短板。

第一,没有自动续期。锁一旦过期,不管业务逻辑是否执行完成,Redis 都会强制释放。下次另一个线程就能拿到锁。如果业务耗时波动很大,你很难给过期时间设置一个合适的阈值。

第二,没有可重入。同一个线程如果第二次执行加锁,会被当成别的客户端拒绝。在高并发环境下,比如一个方法内调用了另一个需要锁的方法,很容易出现问题。

第三,锁的公平性和等待机制缺失。手动加锁失败后直接放弃,没有队列等待、自旋重试的机制。某个热点资源竞争激烈时,大量请求直接失败,体验很差。

第四,代码分散在各个模块里,无法统一管理。每个团队写出来的锁行为不一致,后期维护成本高。

这些短板正是 Redisson 这类封装库要解决的,下一节展开讲。

3. Redisson 如何把分布式锁做到工程级

Redisson 是 Java 生态里最常用的 Redis 客户端之一,它提供的分布式锁不仅解决了手工实现的短板,还附带看门狗续期、可重入、信号量等多种高层抽象。本节讲它的核心原理和用法。

3.1 几行代码完成锁的加解锁

最基础的使用方式:

RLock lock = redissonClient.getLock("lock:order:1001"); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }

lock.lock() 默认使用看门狗机制,锁的默认过期时间是 30 秒,业务逻辑没有执行完,看门狗会自动续期;执行完了,unlock() 会立即释放锁。使用者的确不用去管那些繁琐的 Lua 脚本和标识判断,交给 Redisson 内部处理就好。

还可以显式指定锁超时时间:

lock.lock(10, TimeUnit.SECONDS);

这个时候看门狗机制不会启动,锁在执行完业务后或等待 10 秒后自动释放。两种模式各有使用场景,后面会讲它们如何权衡。

3.2 看门狗:解决“锁提前过期”的核心机制

看门狗(Watch Dog)是 Redisson 最吸引人的设计之一。它的工作原理是:当锁未传入超时时间时,Redisson 内部会启动一个后台定时任务,默认每 10 秒执行一次,检查当前持有的锁是否还存在。如果锁还存在,就把锁的过期时间重置为默认值 30 秒。

这样,即使业务方法执行了 2 分钟,只要持续有续期的机会,锁就不会提前消失。常规业务逻辑如果按秒级甚至毫秒级完成,看门狗任务会持续运行到 unlock() 被调用后才退出。

需要补充的是,看门狗并非万能。如果业务线程发生长时间 GC 停顿、完全阻塞,或者 Redis 连接出现持续异常,不存在持续续期的条件,锁还是会在某个时间点过期。不过从工程概率角度看,它已经比人工设置一个固定过期时间可靠得多,也避免了手工锁里“业务长于锁时长”的最常见事故。

3.3 可重入锁的内部实现

Redisson 的可重入锁用的是 Hash 数据结构来存储锁状态。key 是锁名,field 是加锁客户端的标识(一般是 UUID 拼接线程 ID),value 是重入计数。

加锁时的逻辑大致如下:

  • 如果锁不存在,直接新增 Hash 数据,field 为当前线程标识,value 为 1,并设置过期时间。
  • 如果锁存在,取 field 与当前线程标识比较,相同则 field 对应的 value 加 1。
  • 如果锁存在但 field 不等于当前线程标识,说明锁被其他线程持有,获取锁失败。

解锁逻辑则相反,value 减 1,减到 0 才删除整个 Hash key。这就等于是给分布式锁加了一层类似 Java ReentrantLock 的重入计数,同一个线程可以随意重复进入被锁保护的方法,不至于自己把自己锁死。

3.4 看门狗、TryLock 与释放时机

在生产代码里,我更常用的是带等待时间的 tryLock,因为单纯的 lock.lock() 如果获取不到锁会一直阻塞,业务接口很难接受这种模式。

if (lock.tryLock(2, 10, TimeUnit.SECONDS)) { try { // 执行业务 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } else { // 获取锁失败,返回提示或降级处理 }

tryLock 的完整参数含义是:最多等待 2 秒获取锁,锁持有时间为 10 秒。获取失败时返回 false,我就不会一直阻塞下去,而是让请求快速失败,调用方可以走降级逻辑或者提示稍后重试。

关于 unlock 方法,还有一个大家特别容易忽略的点:代码里明明调了 unlock,但 Redis 里锁还存在,原因是这次请求可能已经锁过期了,或者加锁的线程不是当前线程。所以在并发高的项目里,释放前最好判断一下 isHeldByCurrentThread 再释放,否则可能出现异常。

4. 高可用与多节点:RedLock 算法的价值与争议

4.1 单节点锁的稳定性隐患

前两节讲的实现都建立在单个 Redis 节点能持续提供服务的基础上。可是生产环境里 Redis 节点会出故障,最常见的是主从切换下的锁丢失。

举个具体例子:线程 A 通过 SET NX 在主 Master 上加锁成功。主节点在同步数据到 slave 节点前突然宕机。哨兵或集群提升某个 slave 为新的 Master。新 Master 上没有 A 的锁记录,线程 B 再次获取同一把锁成功。于是 A 和 B 同时持锁,分布式锁失效。

更麻烦的是,Redis 的持久化也有延迟,RDB 或 AOF 都可能丢失最近部分数据。因此单节点 Redis 锁从“一致性”角度看并不稳。它更适合被理解成一种高性能的“软锁”,而不是严格的“硬锁”。

4.2 RedLock 算法的加锁流程

针对单点问题,Redis 作者提出了 RedLock 算法,思想类似分布式共识:用多个互相独立的 Redis 节点,一般建议 5 个,加锁时同时向所有节点发起 SET NX PX 操作,只有在大多数节点(N/2 + 1)上都加锁成功,并且总耗时小于锁有效时间,才认为锁真正获取成功。

流程可以概括为:

  • 获取当前时间 t0。
  • 依次向 5 个节点执行 SET key value NX PX ttl,每个节点设置的过期时间相同。
  • 计算加锁成功的节点数量,同时计算整个过程耗时。
  • 如果成功节点数 >= 3,并且总耗时 < ttl,锁获取成功。
  • 如果获取失败,则向全部节点发送解锁指令,清理可能残留的锁。

这个算法要求各个 Redis 节点完全独立,不共享任何数据,也不使用主从部署。设计目的是希望即使少数节点宕机或网络隔离,其余多数节点依然能够仲裁锁的归属。

4.3 RedLock 的争议与适用范围

RedLock 在分布式系统领域一直有争议。质疑的一方认为,它并没有彻底解决分布式锁的一致性问题,因为客户端在获得锁之后,可能会发生 GC 停摆、CPU 飙高、网络延迟等,导致锁在客户端感知之前已经过期。另一边客户端也能获取锁,两个客户端同时持锁的窗口依然存在。甚至时钟漂移也会影响过期时间计算,使原作者的“加锁总耗时”判断条件失真。

所以我的看法是:RedLock 不是银弹,但它确实提高了在节点故障场景下的锁可用性。如果业务允许极低概率的并发错误并能通过业务层幂等兜底,那用单节点 + Redisson 就够了;如果业务对一致性要求极高,宁可多投入成本选择可靠协调服务,也不要在 Redis 层面硬扛。如果你完全不接受“极小概率锁失效”,那么无论用哪种方案,都要在业务层加一道最终一致性校验。

4.4 生产环境里的务实选型

在实际项目里,我通常按下面几个维度来选择:

  • 大多数互联网场景:Redis 单节点 + 哨兵足够,配合 Redisson 的看门狗,锁可靠性在工程上已经很可观。
  • 核心资金链路、对账系统:不要只靠 Redis,业务层要有唯一索引、乐观锁、幂等表等数据库兜底。
  • 真正强调强一致且可用性要求极高的系统:认真评估 Zookeeper、etcd 这类 CP 系统实现分布式锁,而不是继续硬用 Redis。
  • 如果团队对 Redis 运维很熟且能接受 5 节点成本,Redis 多节点 + RedLock 也可以作为折中方案。

无论选哪种,都应该在代码里为“获取锁失败”设计一个明确分支:是阻塞重试、快速失败,还是异步重试。我见过不少人只在加锁成功后写业务,失败后什么都不做,结果接口丢失请求,这比不加锁还严重。

5. 实战中踩过的坑与排查实录

到最后,分享一些我真实遇到过的坑和排查思路。这个部分不是理论,是从线上事故和压测环境里一点点填出来的经验,希望对正在做方案或者已经上线的你有参考意义。

5.1 锁过期导致两个线程同时扣库存

有一个库存服务,当时锁的超时时间设成了 3 秒,但扣减库存逻辑里调用了一个外部库存接口,接口最长耗时 5 秒。刚开始一切正常,后来外部依赖抖动,大量请求超过 3 秒,锁提前释放,后续请求同时拿到锁,两个线程共同扣了同一份库存,库存数据变成负数。

排查路径:

  1. 查看扣减库存前后的日志,发现同一订单被处理了两次。
  2. 对比加锁时间,两次处理时间差距超过锁过期时间。
  3. 确认锁操作没有续期,导致锁自动释放。

修正方案:

  • 把锁实现切到 Redisson,开启看门狗自动续期。
  • 给外部接口配置单独超时,避免业务链路无限等待。
  • 在库存扣减表加唯一索引,作为兜底防止重复扣减。

5.2 解锁删掉了别人的锁

在 2.2 里讲过的场景,在我一个支付模块真实发生过。当时加锁 value 用的是固定字符串 PAY_LOCK_VAL,结果一次 JVM Full GC 导致持锁线程停顿了 5 秒,锁 3 秒过期,另一个线程获得锁。旧线程恢复后直接在 finally 里 DEL,把新线程的锁删掉了,导致第三个线程也能进入临界区,出现两笔支付重复扣款。

排查路径相对曲折,因为现象不是每次都出现。最终通过给锁加日志看出,锁删除前后的 value 对不上。

修正方案:

  • value 改成“UUID + IP + 线程 ID”组成唯一标识。
  • 删除锁必须走 Lua 脚本先比较后删除。
  • 在关键业务入口加分布式 Trace ID,把加锁和解锁关联起来记录。

5.3 锁粒度不合理的性能问题

有段业务为了防止用户并发领券,直接用 user:{userId}:coupon 作为锁 key,所有和用户领券相关的请求都串行,高峰期接口 RT 飙高。后来我把锁粒度细化成 user:{userId}:couponType:{typeId},用优惠券类型维度加锁,同一用户领取不同类型优惠券就不会互相阻塞,接口吞吐量上去了,互斥性也没受影响。

锁粒度直接影响系统性能,这个优化点在压测或者大促前一定要专门检查。锁 key 的设计其实可以看作“资源维度”的设计:如果业务只在操作同一种资源时需要互斥,就锁该资源;能窄则窄,不能窄才放宽。锁粒度太粗会放大无关请求的串行等待,锁粒度太细则会增加 Redis key 数量和运维成本,这是一个需要在业务场景里反复权衡的设计点。

5.4 锁无法释放导致的全链路堆积

有一次业务反馈某个核心接口突然大面积超时,排查后发现是 Redis 连接池被占满。定位到一个现象:一段加锁代码在 finally 之前抛出了未捕获异常,unlock 没有被执行,Redisson 看门狗虽然能续期,但连接资源被长连接占用,最终连接池耗尽。后来修复方式是统一封装 lock 操作,使用 tryLock 并确保在 finally 中释放,同时对 Redis 操作设置短超时。

给一个比较通用的锁封装建议:

public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, Supplier<T> supplier) { RLock lock = redissonClient.getLock(lockKey); boolean locked = false; try { locked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); if (!locked) { return null; // 或者抛自定义异常 } return supplier.get(); } finally { if (locked) { lock.unlock(); } } }

所有业务都通过这个统一入口加锁,防止有人忘记 unlock。另外,锁的 key 最好不要在代码中散落,可以集中在一个常量类里,方便排查和治理。

5.5 Redisson 看门狗不生效的一种情况

业务方法使用异步线程池执行,主线程获取锁后立刻返回,子线程在后台处理业务。这时 Redisson 看门狗仍然在主线程的定时任务中续期,如果主线程已经返回,定时任务可能被取消,锁过期。这种情况经常出现在“把异步任务放入线程池”的代码里。解决方案是:要么在子线程里同样持有锁并显式传入超时时间,要么关闭看门狗,用固定租期并保证任务在租期内完成。

这个坑比较隐蔽,排查时最大的特征是:日志里加锁成功,业务也在执行,但实际操作数据时,发现锁已经不在 Redis 中了。如果遇到这类问题,建议先检查代码里有没有异步化、线程池切换,再考虑是不是锁续期任务被取消。

5.6 时间不一致与锁过期计算

当一个系统的应用服务器时间和 Redis 服务器时间相差较大时,基于时间戳的锁过期判断会出错。比如应用认为锁还有 10 秒才过期,但 Redis 服务器时间已经比应用快 20 秒,锁实际早已过期。

解决方式:避免依赖本地时间做锁有效期判断;尽量使用 Redis 的 PX 过期时间,并让所有节点通过 NTP 校时。特殊场景如果需要对比时间,优先以 Redis 侧时间为准。

重要提示:在真实生产环境里,任何分布式锁都无法替代业务层的幂等性设计。加锁只是降低并发冲突概率,不等于绝对不冲突。无论锁实现得多严谨,最好再给关键操作加上唯一索引、乐观锁版本号或者幂等表作为最后一道防线。

这些坑总结下来,其实都指向一个核心原则:分布式锁的代码看似只是“加锁、解锁”,实际它要求你同时理解和设计并发安全、资源隔离、异常兜底和系统可靠性。只靠背诵命令是学不会的,一定要在真实环境下压测和观察。我个人体会是,分布式锁的难度不在写出来,而在于长期维护时能扛住各种业务耗时波动、Redis 抖动和代码结构变化。如果你正打算在自己的项目里引入 Redis 锁,或者已经被线上锁问题困扰,建议先按第 2 节的手工实现搞清楚基础原理,再切换到 Redisson 这类成熟方案,最后再结合业务量决定要不要上多节点 RedLock。这样每走一步都会更清楚自己在做什么,也不容易在踩坑时抓瞎。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:27:06

ROS2小车实战:从仿真到实车部署的完整指南与避坑记录

搞ROS2小车开发这几年&#xff0c;见过不少人的学习路径&#xff0c;要么是只跑通了一个官方demo就以为入门了&#xff0c;要么是仿真里跑得飞起&#xff0c;一上真实车就各种扑街。仿真和实车之间的落差&#xff0c;是新手最容易卡住的地方。这个项目标题放在这儿&#xff0c;…

作者头像 李华
网站建设 2026/10/22 4:27:04

YOLOv11实现血液细胞计数识别:从数据到部署的完整毕设实战指南

血液细胞计数识别这个毕设方向&#xff0c;最近确实被问得很多。尤其当题目明确落在“YOLOv11”上时&#xff0c;很多人的第一反应是“这不就是把YOLO跑通、数几个细胞吗”&#xff0c;真做起来才会发现坑不少——类别极度不均衡、小目标密集、标注质量参差&#xff0c;甚至相同…

作者头像 李华
网站建设 2026/10/22 4:26:32

SpringBoot2+Vue3新生报到系统设计与实现拆解

每年八月底九月初&#xff0c;全国各大高校都会迎来一年一度的新生报到季。作为一个写过多个管理系统、也带过新生项目的老开发&#xff0c;我深知这个时候教务系统和学工部门有多忙乱&#xff1a;现场排队填表、纸质材料重复录入、数据统计滞后&#xff0c;这些痛点几乎每所高…

作者头像 李华
网站建设 2026/10/22 4:26:03

Spring Boot+微信小程序:校园二手交易平台设计与状态机实践

1. 校园二手场景的真实痛点&#xff1a;为什么群聊和闲鱼都顶不上大学里的二手交易&#xff0c;其实每天都在发生&#xff0c;但大多数还停留在"毕业群一声吼、楼道海报贴一周、宿舍门口蹲一个学弟"的状态。我做这个基于微信小程序的校园二手交易平台时&#xff0c;最…

作者头像 李华
网站建设 2026/10/22 4:24:54

VLP-16激光雷达点云预处理与建图稳定性优化方案

简介&#xff1a;本资源是一套基于Velodyne VLP-16激光雷达的三维地图构建完整开源实现&#xff0c;面向智能驾驶、机器人SLAM开发与点云处理方向的中高级学习者及工程实践者&#xff0c;聚焦解决真实传感器数据驱动下的实时建图与定位问题。压缩包共75个文件&#xff0c;涵盖1…

作者头像 李华
网站建设 2026/10/22 4:24:54

电商用户购买意图预测建模全流程(LightGBM+特征工程)

简介&#xff1a;本资源是京东JData大数据算法大赛中Rank13队伍提交的用户建模方案代码包&#xff0c;面向数据挖掘、推荐系统方向的学习者与竞赛参赛者&#xff0c;聚焦于电商场景下的用户行为建模与预测任务。压缩包共12个文件&#xff0c;含3个核心Python脚本&#xff08;ge…

作者头像 李华