news 2026/10/12 2:45:57

SpringBoot + Redis 分布式锁实战:原理、实现与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot + Redis 分布式锁实战:原理、实现与避坑指南

很多团队第一次意识到“代码里的锁不管用了”,往往发生在服务从单机部署切到多实例部署之后。单体时代写synchronized很顺手,ReentrantLock也很好用,但一旦服务同时跑在三台机器上,同一个订单的两个请求可能分别落到不同实例,每台机器都觉得自己没遇到并发冲突,最后就会出现重复发券、重复扣库存、重复创建支付单这类问题。分布式锁,就是为了在多个独立节点之间同步访问共享资源而引入的互斥机制。

提SpringBoot + Redis来做分布式锁,几乎是目前一线团队最容易达成的共识。SpringBoot 已经是 Java 后端的事实标准骨架,Redis 在多数项目里也早就承担着缓存、计数器、分布式会话这些职责,在现有基础设施上加一个锁服务,不需要申请新的存储资源,也不需要引入额外维护成本。这篇文章我会把从原理选型到代码实现、再到真实踩坑点的完整过程讲清楚,适合两类人看:一类是项目里已经因为本地锁失效吃过亏,想快速把分布式锁补上的开发者;另一类是刚接触分布式锁,想搞清楚 Redis 锁的边界和注意事项的读者。

1. 整体设计思路与方案选型

1.1 为什么优先选 Redis 而不是其他中间件

分布式锁本质上解决的是“跨节点的互斥”问题,有资格做这件事的系统其实不少。最简单的方案是继续用 MySQL,靠select ... for update做悲观锁,或者用版本号做乐观锁,好处是基本不用学新东西,坏处是数据库在高并发争抢锁时并发能力偏弱,而且行级锁一个没控制好就容易拖垮主库性能。ZooKeeper 基于临时顺序节点配合 watch 机制实现锁,可靠性和公平性都不错,但代价是要维护好 ZooKeeper 集群本身,对很多业务团队来说运维成本不太友好。etcd 的 lease 和事务机制同样能实现分布式锁,强一致性好,只是等于在技术栈里硬塞了一个新组件。

Redis 的优势不在于理论上的最强可靠性,而在于它通常已经部署好、已经有人运维、业务系统接入成本极低。Redis 里的SET命令配合NX和PX参数,天然就能表示“在 key 不存在时才写入,并设置过期时间”,这就是一把分布式锁最原始的雏形。性能方面,单次操作在微秒到毫秒级,只要锁的访问频率可控,完全不会成为系统瓶颈。所以在选型阶段,如果项目里已经有 Redis,并且并发互斥场景不是金融级别,我会第一个推荐用 Redis 实现。

当然也不是所有场景都适合。如果业务涉及资金账务、核心状态流转这类极端强一致的场景,Redis 锁在极端情况下(比如主从切换)确实存在丢失锁的可能,那就要考虑 ZooKeeper 或数据库,并且必须配套对账和补偿机制。方案选型没有绝对的对错,只有风险接受度的问题。

1.2 主流分布式锁方案横评

可以把几种常见方案放在一张表里看看,心里会更清楚它们的定位:

方案实现方式可靠性性能和成本适用场景
MySQL悲观锁for update/ 乐观锁版本号强一致数据库压力大,实现简单低频操作、已有数据库且不想引入新组件
ZooKeeper临时顺序节点 + watch强一致需要额外维护 ZK 集群,性能适中强一致性要求较高的分布式协调场景
etcdlease 租约 + 事务强一致引入新组件,运维成本偏高已有 etcd 环境的基础设施团队
RedisSET NX PX/ Lua / Redisson多数场景够用,极端场景可能丢锁性能高,使用成本低高并发、能接受极小概率丢失锁的互斥场景

从这张表能看出来,Redis 的核心竞争力是“高性价比”。很多业务团队已经积累了 Redis 运维经验,再引入一个重组件只是为了锁,性价比确实不高。但选 Redis 不代表忽略可靠性,而是要在方案设计时清楚它的边界在哪里,并提前做好业务兜底。

1.3 分布式锁必须满足的三个硬性指标

做锁不能只停留在“能跑通”的层面,站在使用者的角度,一个靠谱的分布式锁至少要满足三件事。

第一是互斥性:任意时刻,只有一个客户端能持有锁,其他客户端不能同时进入临界区。这是锁的基本盘,做不到互斥,后面全免谈。

第二是防死锁:持有锁的客户端如果崩溃、服务宕机、线程被杀掉,锁必须能在一段时间后自动释放,否则整个系统会被一个异常节点拖死。这也是 Redis 锁必须有PX过期时间的原因。

第三是防误删:加锁时如果只存一个固定字符串,线程 A 因为业务超时导致锁被自动释放,线程 B 随后拿到了锁,A 在 finally 里执行DEL时就会把 B 的锁误删掉。所以解锁必须校验“这把锁是不是我加的”,通过唯一标识来确认归属。

这三个指标听起来很基础,但实现里的很多细节都是为了守住它们。

2. 核心细节解析:原子性、释放与过期时间

2.1 加锁必须是一个原子动作

网上很多老文章给的 Redis 加锁代码是先用SETNX写入 key,再调用EXPIRE设置过期时间。单独看每条命令都没问题,但如果把这两条命令拆开执行,90% 的分布式锁事故都出在这两步之间。比如执行完SETNX之后,业务线程因为 GC 停顿、网络抖动、进程崩溃等原因挂了,EXPIRE根本没执行,Redis 里就留下了一个永远不过期的 key,这把锁就再也拿不到了,这就是典型的死锁。

正确做法是使用 Redis 2.6.12 之后提供的原子命令:

SET dist:lock:order:9527 6f8b2c1e NX PX 30000

这个命令把“无则写入”和“设置过期时间”合并到了同一次运维中,中间没有任何空档。后面在 SpringBoot 代码里,我们可以直接用StringRedisTemplate.execute()执行 Lua 脚本来完成同样的效果,也不只依赖某一个 Redis 版本的命令语法,以后想加其他校验逻辑也更好扩展。

2.2 解锁必须校验 owner,且比对和删除必须原子

如果加锁只是SETNX,解锁时只是DEL,会引入一个很隐蔽的问题。假设线程 A 加锁后业务执行时间过长,锁提前过期了,线程 B 加锁成功。A 忙完准备释放锁,此时如果直接DEL,删掉的就是 B 的锁。这个瞬间线程 C 也可以用同样的方式再拿到锁并进入临界区,互斥性直接失效。

解决办法是加锁时持有一个全局唯一的随机值,通常用 UUID 生成,解锁前先比对 Redis 里的 value 是不是自己存入的那个,只有完全一致才执行删除。但这里还有一个隐藏问题:“比对”和“删除”中间不能有间隔,否则和加锁一样会出现竞态窗口。所以解锁必须用 Lua 脚本把GET和DEL合并在 Redis 服务端一次原子执行:

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

这段脚本看着简单,却是防误删的关键。Redis 执行 Lua 脚本时不会插入其他命令,所以 get 和 del 之间不可能被其他线程的请求打断。

2.3 过期时间太短和太长都会出事

Redis 分布式锁最终依赖过期时间兜底,而怎么定这个时间是最容易纠结的点。设太短,业务没跑完锁就自动释放,A 还没结束,B 已经进来,锁形同虚设;设太长,一旦持有者真的宕机,锁会长时间占住资源,其它请求全部阻塞或失败。

常规做法是先通过日志或压测统计临界区代码的最坏耗时,再把过期时间定在估算值的 3 到 5 倍,给足余量。比如一个接口正常耗时 200ms 左右,但数据库偶尔慢查询会到 2 秒,那把锁的过期时间设在 5~10 秒比较稳妥,而不是盲目设成 30 秒。如果业务里有长耗时任务,还需要“看门狗”机制做自动续期,这个细节我在第四部分展开。

提示:过期时间本质上只是兜底方案。不要指望一个过期值让锁覆盖所有异常场景,还应该通过熔断、超时控制、优雅停机等手段防止业务无限期占用锁。

2.4 拿不到锁不应该立刻失败

分布式锁的调用模式不是一棍子打死,很多场景下拿不到锁就立刻抛异常并不合适。比如一个库存扣减接口同时来了 100 个请求,只有一个能拿到锁,其余 99 个如果全部返回“失败”,用户体验会非常差。更好的做法是设置一个有界的等待时间,在锁释放后的短时间内,让线程重新尝试获取。常见实现是拿不到锁就让线程休眠一小段时间,比如 50ms,再继续 tryLock,直到超过预设的最大等待时间。

这里要注意自旋频率不能太高。如果 200 个线程同时自旋,每隔 5ms 就打一次 Redis,Redis 上会形成新的热点,得不偿失。我习惯把重试间隔设为 50~100ms,最大等待时间控制在 2~3 秒以内,压测数据也证实这样对 Redis 的压力最小。超过等待时间后仍然拿不到锁,再走降级逻辑,比如返回“操作繁忙,请稍后重试”或记录 MQ 消息做异步补偿。

2.5 可重入到底要不要实现

Java 里的synchronized和ReentrantLock都是可重入的,同一个线程嵌套调用不会把自己锁住。但 Redis 锁默认不具备这个能力,如果业务中有一个方法内部调用了另一个带锁的方法,第二次tryLock会直接失败。

实现可重入的思路是:加锁时 value 结构改成“线程标识 + 重入次数”,同一线程再次加锁时只要确认 owner 是自己,就把计数加 1,解锁时减 1,减到 0 才真正删除 key。虽然代码能实现,但我的经验是分布式锁的可重入要慎用。可重入会在代码里隐藏锁边界,开发者很容易忘记锁的实际范围,最后锁越套越深,排查问题成本直线上升。宁可把锁粒度拆细,让嵌套调用变成顺序调用,或者把锁放到最外层统一管理。

3. 实操过程:SpringBoot 集成与完整代码实现

3.1 项目准备与 Maven 依赖

创建一个标准的 SpringBoot 项目,然后在pom.xml里引入 Redis 相关依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>

SpringBoot 2.x 默认使用 Lettuce 作为 Redis 客户端,配合 commons-pool2 可以开启连接池。如果你用的是 SpringBoot 3.x,注意配置前缀从spring.redis.*变成了spring.data.redis.*,细节不一样但整体原理一致。

3.2 Redis 连接配置

在application.yml里做如下配置:

spring: data: redis: host: 127.0.0.1 port: 6379 password: timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 0

连接池的max-active一般不需要配得特别大,16 到 32 之间足够。锁操作本身很快,真正的瓶颈通常在业务临界区,而不是 Redis 连接数。timeout建议设置 3 秒,避免 Redis 故障时业务线程长时间阻塞在连接等待上。

3.3 核心工具类实现

接下来是最核心的锁工具类,我封装了四个基础能力:尝试加锁、阻塞加锁、释放锁、续期。

package com.example.distlock; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; @Component public class RedisDistributedLock { private static final String LOCK_PREFIX = "dist:lock:"; /** * 加锁 Lua 脚本:原子执行 set key value NX PX * KEYS[1] 为锁key,ARGV[1] 为客户端唯一标识,ARGV[2] 为过期时间毫秒值 */ private static final String LOCK_SCRIPT = "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then " + "return 1 else return 0 end"; /** * 解锁 Lua 脚本:确认 value 匹配后删除 */ private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) else return 0 end"; private final StringRedisTemplate redisTemplate; private final DefaultRedisScript<Long> lockScript; private final DefaultRedisScript<Long> unlockScript; public RedisDistributedLock(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; this.lockScript = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class); this.unlockScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class); } /** * 尝试加锁,立即返回。成功返回唯一标识,失败返回 null。 */ public String tryLock(String key, long expireMs) { String token = UUID.randomUUID().toString(); Long result = redisTemplate.execute( lockScript, Collections.singletonList(LOCK_PREFIX + key), token, String.valueOf(expireMs) ); if (result != null && result == 1L) { return token; } return null; } /** * 阻塞加锁,在最大等待时间内不断尝试 */ public boolean lock(String key, long expireMs, long tryWaitMs) throws InterruptedException { long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < tryWaitMs) { String token = tryLock(key, expireMs); if (token != null) { ThreadLocalHolder.setToken(key, token); return true; } Thread.sleep(50); } return false; } /** * 释放锁 */ public void unlock(String key, String token) { if (token == null) { return; } redisTemplate.execute( unlockScript, Collections.singletonList(LOCK_PREFIX + key), token ); } /** * 续期:仅当 value 匹配时延长过期时间 */ public boolean renew(String key, String token, long expireMs) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('pexpire', KEYS[1], ARGV[2]) else return -1 end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(LOCK_PREFIX + key), token, String.valueOf(expireMs) ); return result != null && result > 0; } }

这段代码里有几个关键点值得说明。tryLock成功后必须把 token 留在外层变量里,后面unlock要用它做归属校验,不建议把 token 存到 Redis 和业务数据无关的 ThreadLocal 里,除非你做了非常严谨的清理逻辑。lock方法里的Thread.sleep(50)是我实测下来兼顾响应速度和 Redis 压力的折中值,不建议低于 20ms。另外用StringRedisTemplate而不是RedisTemplate<String, Object>,是为了避开 JDK 序列化器带来的各种隐性 bug,这个小细节后面会专门讲。

3.4 业务代码调用模板

在业务方法中使用分布式锁时,我推荐把锁调用抽象成一个固定模板,避免每个开发写出来的释放逻辑都不一样。

public boolean markOrderFinished(String orderId) { String lockKey = "order:finish:" + orderId; long expireMs = 5000L; long waitMs = 2000L; String token = null; try { token = lockService.tryLock(lockKey, expireMs); if (token == null) { // 拿不到锁说明已有其他线程在处理该订单 log.warn("订单 {} 处理中,请勿重复操作", orderId); return false; } // 模拟业务处理:更新订单状态、记录操作日志等 doMarkFinished(orderId); return true; } finally { if (token != null) { lockService.unlock(lockKey, token); } } }

注意finally里只是释放自己持有的锁,如果这里在中间断网或者返回前线程被 kill,Redis 的过期时间会兜底自动释放。异常路径下释放逻辑同样要执行,但一旦出现网络异常,unlock本身也可能失败,这时只能靠过期时间保证锁最终可用。

3.5 看门狗自动续期的实现思路

如果临界区代码里有长耗时操作,固定过期时间就不够用了。思路是启动一个守护线程,每隔一段时间检查锁是否还在自己手里,如果在,就把过期时间往后推。

public void lockWithRenewal(String key, String token, long expireMs) { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "redis-lock-renewal"); t.setDaemon(true); return t; }); scheduler.scheduleAtFixedRate(() -> { boolean success = lockService.renew(key, token, expireMs); if (!success) { // 锁已丢失或已被其他线程持有,停止续期 scheduler.shutdown(); } }, expireMs / 3, expireMs / 3, TimeUnit.MILLISECONDS); }

看门狗的续期间隔一般是过期时间的三分之一。比如锁过期时间 10 秒,就每隔 3 秒续一次。这个方案比把过期时间盲目调到 30 秒更优雅,因为锁在正常情况下会被及时释放,不会长时间占用 Redis key,但如果持有者真的挂了,守护线程也会随之消亡,过期时间仍然能兜底。

3.6 快速验证方法与并发测试

工具类写完以后,建议先用一个简单的并发测试验证锁是否真的生效。比如编写一个接口,内部执行Thread.sleep(100)模拟耗时操作,然后用压测工具或 JUnit 并发模拟器发起 50 个并发请求,观察日志中进入临界区的线程列表。

验证时可以通过redis-cli观察锁 key 的写入与释放:

redis-cli --scan --pattern 'dist:lock:*'

锁生效时会看到dist:lock:order:finish:9527这样的 key 短暂出现,业务结束后unlock将其删除。如果出现一个线程的锁还没释放、另一个线程也打印了“进入临界区”的日志,说明锁的互斥性没生效,优先检查 token 校验和 Lua 脚本是否执行正确。

4. 常见问题与排查技巧实录

4.1 业务还没执行完,锁自己先过期了

现象:业务代码本身很慢,或者数据库出现慢 SQL,导致临界区执行时间超过锁的过期时间,锁被 Redis 自动释放,第二个线程提前进来。

原因:过期时间设置不合理,或者业务耗时的估算严重偏离实际。锁的过期时间本质上是“最大兜底时间”,不能指望它正好等于业务执行时间。

解法:统计接口最坏耗时,将过期时间设为最坏耗时的 3~5 倍;如果有稳定的长耗时操作,直接升级为看门狗续期方案。还有一个很容易被忽略的点:加锁后不要在临界区里调用 RPC 远程接口,因为远程调用是不可控的,可能等很久,这个问题我踩过好几次。

4.2 Redis 主从切换时锁丢失

现象:请求 A 在 master 节点上写入锁 key,但这条数据还没来得及同步到 slave 节点时,master 发生宕机,slave 被提升为新的 master,此时锁 key 不存在,其他请求就能拿到锁,导致互斥失效。

原因:Redis 的主从复制默认是异步的,锁数据存在丢失窗口。Redis 官方文档也明确提到了这个风险。

解法:如果系统完全无法接受这种极端情况,就要选择 ZooKeeper 或 etcd 这类强一致性的协调组件。Redis 锁在多数业务场景下是可以接受的,因为这类“重复处理”问题通常可以通过幂等设计、唯一约束、对账任务等机制兜住。所谓 RedLock 算法本身也有争议,不建议盲目引入,它并不能彻底解决所有分区场景下的锁丢失问题,反而增加了复杂度。

4.3 Lua 脚本在 Spring Data Redis 里的序列化坑

现象:执行 Lua 脚本时报ClassCastException或返回结果类型不对,最典型的是DefaultRedisScript传Long.class却返回了LinkedHashMap。

原因:如果使用RedisTemplate<String, Object>并且配置了 JSON 序列化器,Redis 中的 value 会被反序列化成复杂对象。执行 Lua 脚本时传入的 key 和参数也会被序列化,导致比对逻辑出错。

解法:在分布式锁场景下统一使用StringRedisTemplate。它的 key 和 value 默认都是 String,配合 Lua 脚本传参和返回数字都非常稳定。我在实际项目中还遇到过因为序列化器不同导致同一个 key 在不同代码块里无法互相识别的案例,所以这个问题必须从源头规避。

4.4 自旋等待把业务线程池打满

现象:高并发期间大量线程阻塞在lock()方法里,Tomcat 线程池快速耗尽,系统出现大面积请求超时。

原因:等待时间过长,或者重试间隔太短。核心问题不是锁本身,而是锁竞争时没有做好流量控制。

解法:给lock()设置一个合理的最大等待时间,比如 2 秒,超过后快速失败;自旋间隔保持在 50ms~100ms,避免线程风暴;更稳妥的做法是提前在网关或入口层做限流,让真正进入业务的请求数量可控。记住,分布式锁解决的是并发修改问题,不是流量削峰问题。

4.5 锁与事务一起使用时顺序颠倒

现象:在@Transactional方法内部加锁,方法结束立即释放锁,但此时事务还没提交。下一个拿到锁的线程查到的仍是旧数据。

原因:事务的提交发生在方法返回之前,如果锁在事务提交前释放,就存在一个“锁已释放但数据未生效”的窗口。

解法:把锁放在事务外层。正确的调用链路是:外层方法先加锁,然后进入事务方法,事务方法正常提交并返回后,外层 finally 再解锁。如果项目里已有大量在事务内加锁的代码,改造时把锁逻辑上提到 Service 层的外层包装方法里即可。

4.6 常见问题速查表

现象可能原因处理建议
拿不到锁,业务全部失败过期时间过长 / 持有者崩溃设置合理过期时间,增加看门狗续期
锁变量丢了,解锁报空指针token 未保存或作用域不对在 try 之前声明,finally 里判空释放
锁被别的线程误删解锁没有校验随机标识使用 Lua 脚本 get + del 原子比对
执行 Lua 报类型异常RedisTemplate 序列化器不一致统一使用 StringRedisTemplate
锁释放后数据还没更新事务提交晚于锁释放锁在外层,事务在内层,提交后再释放
高并发时请求大量超时自旋等待过长或线程池打满设置有界等待时间,入口限流

最后分享一点个人体会

我实际把这个方案落地到项目中后,最强烈的感受是:锁本身并不复杂,真正需要反复打磨的是业务对锁边界和失败处理的理解。Redis 分布式锁不是银弹,而是一个性价比很高的基础设施。它帮你挡住了 99% 的重复提交和并发覆盖问题,但剩下的 1%,必须靠业务幂等、数据对账、补偿任务来兜底。

如果刚准备在 SpringBoot 项目里做分布式锁,建议用这篇文章里的方案先跑通主流程,把过期时间、token、Lua 脚本这几个细节写对,再根据可靠性需求决定要不要引入更重的中间件。最后再分享一个小习惯:每写一段加锁后的业务代码,都问自己一句“如果从这里宕机,锁会不会自动释放,后面的补偿逻辑是什么”。这个问题想明白了,锁相关的问题基本不会留下大隐患。

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

DNA序列分类实战:主成分分析降维与Fisher判别完整流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 2:44:53

CTF Misc实战:从文件隐写到信息提取的解题复盘

1. Misc 26-30&#xff1a;从解题顺序看CTF杂项的核心思维在CTF比赛中&#xff0c;Misc&#xff08;杂项&#xff09;一直是我觉得最有意思的一个方向。它不像Reverse或Pwn那样对底层知识要求极高&#xff0c;也不像Crypto那样充满数学推导&#xff0c;Misc更像是把真实世界里的…

作者头像 李华
网站建设 2026/10/12 2:43:43

AI办公助手本地部署必调10个设置:从模型参数到知识库

很多人在本地部署 AI 办公工具后&#xff0c;第一反应是“别人说这个能帮写周报、做PPT、抽会议纪要&#xff0c;怎么我跑起来就是人工智障&#xff1f;”其实大部分情况不是模型不行&#xff0c;而是工具装完只做了默认初始化&#xff0c;没有针对办公场景做关键设置。工作流、…

作者头像 李华
网站建设 2026/10/12 2:43:42

免费Token实战:DeepSeek、GLM等大模型API接入与批量任务指南

每逢节点&#xff0c;各家大模型平台都会放出免费 token 或者体验额度。标题里的“免费鸡蛋”说的不是菜市场的鸡蛋&#xff0c;而是平台白送的 API 调用额度。拿过来可以直接调 DeepSeek、GLM 这类国产大模型&#xff0c;跑文本生成、代码补全、批量总结、知识库问答都能用。这…

作者头像 李华
网站建设 2026/10/12 2:43:09

前缀和算法实战:从一维到二维,区间查询O(1)的底层思维

前缀和算法&#xff0c;说白了就是“预先把累加结果存起来&#xff0c;查询的时候直接拿”。很多人第一反应是“这玩意不就是求个区间和吗&#xff0c;有什么可讲的”&#xff0c;但实际刷题刷到后面你会发现&#xff0c;前缀和不只是求和的工具&#xff0c;它还经常隐藏在哈希…

作者头像 李华
网站建设 2026/10/12 2:42:44

Flutter在OpenHarmony上的UI构建实践:从跨端框架到电商App落地

1. 项目概述与核心思路做跨端开发的朋友应该都感觉到了&#xff0c;这两年 OpenHarmony 生态的推进速度比想象中快得多。过去我们聊鸿蒙应用开发&#xff0c;第一反应是“又要学一门新语言”&#xff0c;但 Flutter for OpenHarmony 这条路线出现之后&#xff0c;情况完全变了—…

作者头像 李华