news 2026/10/5 11:11:20

从乐观锁到MVCC:并发控制实战与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从乐观锁到MVCC:并发控制实战与避坑指南

1. 乐观锁不是"锁":先弄清楚它在并发控制里的真实位置

很多新人第一次接触乐观锁,都会陷入一个误区:以为它和"锁"一样,是数据库或代码里某个可以lock()、unlock()的机制。我当年也干过这种事——在代码里搜Lock关键字,试图找到乐观锁的API,结果当然是搜了个寂寞。

乐观锁的真实身份,是一种并发控制策略,本质上是对数据一致性的"检测-补救"机制。它不像悲观锁那样,在操作数据前就SELECT ... FOR UPDATE把行锁住,让其他人只能排队等着。乐观锁的出发点是:假设绝大多数情况下数据不会被并发修改,所以不加物理锁,只在提交更新时检查"你改的这条数据,还是不是你当初读到的那个版本"。

这个"检查版本"的动作,才是一切乐观锁实现的核心。

1.1 一句话讲透乐观锁的定义边界

严谨一点说:乐观锁(Optimistic Locking)是一种基于数据版本校验的并发控制策略。它的工作流程分三步:

  1. 读取数据,同时记下当前版本号(version)或时间戳(timestamp),或者干脆记下数据的原始快照。
  2. 业务逻辑处理,此时完全不加锁,所有线程都在"自由"地读和算。
  3. 执行更新时,UPDATE语句的WHERE条件里带上步骤1读到的版本号,同时把版本号+1。如果UPDATE影响的行数是1,说明期间没人动过这条数据,这次提交成功;如果影响行数是0,说明别人已经抢先改了它,这次提交失败,需要业务层决定是否重试。

注意到没有,乐观锁的整个生命周期里,数据库层面没有任何一把"锁"被持有。它真正干的事是有条件更新(CAS,Compare And Swap),把"并发冲突"这个风险,从"操作前预防"挪到了"提交时检测"。

用生活化的方式理解:乐观锁就像图书馆的座位预约。你坐下时在纸条上写了"10:00占了这座位",离开时如果纸条还是你写的"10:00",说明没人来赶你,你可以正常走;如果纸条被人撕了重写,说明这期间有人动过,你就不能继续按原来的计划写作业了——得重新规划。

1.2 为什么"没人抢"的假设在特定业务里是合理的

你一定会问:既然乐观锁会在更新时失败,那为什么不直接上悲观锁,一次搞定?

答案在于业务的冲突概率和锁的成本。拿一个典型的例子说:用户浏览文章详情页,页面上显示"阅读量+1"。这个操作的特点是——读多写少,且同一篇文章同一秒内几乎不会有两个人同时精确地"读又写同一条记录"。如果此时用悲观锁,每个读者进来都要SELECT FOR UPDATE锁住文章行,后面的读者全部阻塞。明明是一次轻量级的统计自增,硬生生变成了串行队列,数据库的连接池很快就被排队的请求拖垮。

而乐观锁在这种场景下的表现就很舒服:读数据是普通的SELECT,不阻塞任何人;只有真正执行UPDATE articles SET read_count = read_count + 1 WHERE id = ? AND read_count = ?那一刻才需要关注冲突。由于文章阅读量并发冲突概率极低,绝大多数UPDATE一次性就成功了。

所以乐观锁的核心适用面是:并发冲突概率低、且一次冲突的代价(回滚重试)远小于持续持有锁的代价的场景。反过来,如果冲突率一高,乐观锁会陷入大量更新失败、大量重试的窘境,性能和体验反而比悲观锁更差。这个"度"在哪里,后面第4节我会专门展开讲。

2. 数据库底层的乐观并发控制:MVCC的版本链与可见性判断

很多人把乐观锁等同于"在表里加个version字段",这其实只看到了最表层的用法。如果面试官问你"InnoDB的乐观锁底层依赖什么机制",你只答版本号字段就显得太浅了。真正支撑乐观锁思想的底层,是MVCC(Multi-Version Concurrency Control,多版本并发控制)——它让数据库的乐观读成为可能。

2.1 隐藏列与undo log版本链

MySQL InnoDB的每行数据,其实还藏着几个"隐藏列",平时SELECT *看不到,但它们一直在默默工作:

  • DB_TRX_ID:最后一次修改这一行的事务ID。
  • DB_ROLL_PTR:回滚指针,指向这行数据在undo log里的上一个版本。
  • DB_ROW_ID:隐藏主键,如果没有显式主键,InnoDB会用它来组织行结构。

当一行数据被修改时,InnoDB不会直接把旧数据抹掉,而是把修改前的数据写入undo log,然后通过回滚指针把这些新旧版本串成一条版本链。新版本在链头,旧版本在链尾。

想象一个场景:事务A把goods表的某个库存字段从100改成90,这行数据的DB_TRX_ID变成A的事务ID,DB_ROLL_PTR指向一条undo log——记录着"100"这个旧值;如果紧接着事务B又把90改成80,那么版本链头是"80(B改)",往下是"90(A改)",再往下是"100(原始)"。

这条版本链的意义在于:让读操作可以"读到过去"。事务C在A和B都还没提交时去读这行数据,它不会因为数据被改就阻塞等待,而是沿着版本链找到对C可见的那个版本——可能是"100",也可能是"90",取决于C的快照创建时间。这就是MVCC下"读"不加锁的理论基础,也是乐观锁能够"先随便读、提交时再校验"的前提。

2.2 ReadView的可见性规则才是"判断冲突"的依据

有了版本链,还得有规则来决定"当前事务到底应该看到哪个版本",否则版本再多也是乱的。这个规则由**ReadView(读视图)**来执行。

ReadView在事务第一次执行快照读(普通SELECT)时生成,里面主要记录了四类信息:

  • m_ids:当前活跃的、尚未提交的事务ID列表。
  • min_trx_id:活跃事务里最小的那个事务ID。
  • max_trx_id:当前系统里下一个要分配的事务ID(即"未来事务"的起点)。
  • creator_trx_id:生成这个ReadView的事务自己的ID。

当读操作走到某行,会顺着版本链逐个判断版本的事务ID:

事务ID情况可见性
trx_id < min_trx_id该版本在ReadView生成前已提交,可见
trx_id >= max_trx_id该版本由"未来事务"生成,不可见
min_trx_id <= trx_id < max_trx_id若在m_ids活跃列表中则不可见;不在列表中则已提交,可见
trx_id == creator_trx_id自己的修改当然可见

这个判断过程,本质上就是一种"乐观"的读取裁决——并不需要等锁,只需要在内存里做几次比较,就能从版本链中选出正确的那份快照。

理解MVCC之后,你对乐观锁的认知应该升级一层:乐观锁的版本字段,本质上是你手动维护的一个"可见性标记"。数据库用隐藏的DB_TRX_ID和版本链实现了内部的一致性读,而你的业务字段版本号则是把这种一致性延伸到跨表、跨服务乃至微服务场景的手段。当你在业务表上ALTER TABLE加一列version时,其实是在自己搭建一条简化版的"版本链"。

3. 代码里落地乐观锁的三种主流写法

理论说透了,下面直接进入"怎么写"。乐观锁没有统一的SDK,它是一套思想,落地方式很多。我按项目里见到的频率,从高到低讲三种:版本号字段、CAS条件更新、时间戳/标记类方案。

3.1 版本号字段:最常用也最容易写错

这是最典型的乐观锁实现,也是我推荐的新手首选方案。给表加一列版本号,例如:

ALTER TABLE `goods` ADD COLUMN `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';

然后更新时走两步:

-- 步骤1:查询,记住version SELECT id, stock, version FROM goods WHERE id = 100; -- 步骤2:更新,带上version条件,并递增version UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = 100 AND version = 1;

注意步骤2的执行结果需要检查affected_rows。如果返回1,更新成功;返回0,说明version已经不是1了,有别人抢先提交过,这时候你的更新静默失效——但业务数据没坏,只需要重试或提示用户。

我见过很多人在这里写错,常见错误有两类。

第一类是更新时忘了把version = version + 1写进SET子句。只写了WHERE version = ?但没递增版本号,结果第二次更新时WHERE id=100 AND version=1依然成立,乐观锁等于被突破,版本号形同虚设。

第二类是把版本号当成"重试口号"。更新失败后,代码里重新SELECT一条新版本号回来继续改——如果新版本号还是被别的线程抢先改了,就继续循环。这本身没问题,但如果循环没有上限,在高冲突场景下会变成死循环般的重试风暴。重试上限和退避策略一定要有,我后面第5节会给出具体代码。

3.2 CAS条件更新:一条SQL完成检查和修改

CAS写法不需要额外的版本号字段,直接把"业务字段的当前值"当成校验条件。最经典的例子是库存扣减:

UPDATE goods SET stock = stock - 1 WHERE id = 100 AND stock = 8;

这条SQL的意思:只有当库存还是我刚才读到的8时,才减1;如果此刻库存已经是7,说明有人先扣了一单,WHERE stock = 8不成立,影响行数返回0,业务层重新查询再做决定。

相比版本号方案,CAS省掉了一列,但它的逻辑等价于"以业务字段本身作为版本号"。这种写法有两个注意点:

  1. 条件字段必须是真实发生变更的字段,或者至少是变更前后差异明显的字段。如果一个用户把收货地址从"A"改成"A"(没有变化),那么WHERE address = 'A'仍然成立,CAS检测不到并发——这就是为什么通常推荐用版本号而非业务值来检测冲突,因为业务值可能相同。
  2. CAS条件字段本身不要参与运算。比如UPDATE goods SET stock = stock - 1 WHERE id = 100 AND stock - 1 = 7这种写法是反模式,它把条件建立在"计算后的结果"上,可读性极差且容易引发索引失效。

多数国产数据库中间件(如ShardingSphere)提供乐观锁内置支持,其实底层的SQL改写逻辑也是帮你把版本号自动拼进UPDATE ... WHERE ... AND version = ?,主动权仍然在业务SQL设计者手里。

3.3 时间戳方案与Java层的AtomicStampedReference

版本号字段存整数,还有一种变体是存update_time时间戳。原理一样:更新时WHERE update_time = 原值,同时把update_time更新为当前时间。时间戳方案有一个坑:如果业务系统允许同一毫秒内两条事务并发修改,时间戳粒度不够会漏判。所以强烈建议用整数版本号而不是时间戳——版本号无歧义、递增单调、不依赖系统时钟。

如果你用Java写并发代码,想在线程内部模拟乐观锁,可以关注java.util.concurrent.atomic.AtomicStampedReference。普通AtomicReference的CAS有个著名的ABA问题(后文会细讲),而AtomicStampedReference额外维护了一个版本标记stamp,每次修改都更新stamp,CAS时对比的是(引用, 标记)二元组,能有效区分"值从A变成B又变回A"的情况。

AtomicStampedReference<Integer> stock = new AtomicStampedReference<>(8, 0); int[] stampHolder = new int[1]; Integer current = stock.get(stampHolder); // 业务处理... boolean success = stock.compareAndSet(current, current - 1, stampHolder[0], stampHolder[0] + 1);

这其实就是把数据库版本号的思想搬到了内存对象上。虽然我们平时很少在Java内存里去实现乐观锁,但理解这份代码能帮你更通透地理解"版本标记"这个核心概念——它不依赖数据是否"相等",而依赖数据"是否经历过修改过程"。

4. 深入ABA问题与乐观锁的边界条件

前面提到ABA问题,这是所有CAS类机制都绕不开的一道坎。搞清楚它,你对乐观锁的理解深度会明显超过"会用version字段"的那批人。

4.1 ABA问题为什么会破坏业务正确性

ABA问题是指:共享变量从A变为B,再变回A。单看数据的当前值,一切如常,但如果把"取值过程"作为一个完整事件来看,已经发生过一次中间修改。

数据库表里最经典的ABA场景是"余额扣减+充值"的组合操作。假设账户余额是100:

  1. 事务T1读取余额100,准备扣减50。
  2. 事务T2读取余额100,先扣减50——余额变成50。
  3. 事务T3读取余额50,又充值50——余额变回100。
  4. 此时事务T1执行UPDATE account SET balance = balance - 50 WHERE id = ? AND balance = 100,条件成立,扣款成功,余额变50。

问题来了:T1以为自己对着"从未变化过的100"做扣减,但它看到的100已经不是最初那个100了——中间发生过"扣减和充值"。如果业务规则要求"每笔扣款前余额必须持续满足某项状态",ABA就可能导致错误。

为什么版本号能解决ABA?因为版本号指向的是"修改的次数"而不是"值的大小"。余额变回100,但版本号从1递增到3,T1拿着版本号1去做WHERE id=? AND version=1,直接失败——它敏锐地发现自己的数据快照已经过期了。所以结论很清晰:用版本号实现乐观锁,天然免疫ABA问题;用业务字段值做CAS条件,则有可能踩中ABA。

4.2 哪些场景乐观锁不该用:与悲观锁的选型对照

乐观锁不是银弹。我梳理了一个简单的决策模型,你可以直接对照自己的业务:

判断维度倾向乐观锁倾向悲观锁
并发冲突概率低(<5%的写冲突率)高(热点数据频繁并发写)
单次冲突成本可接受轻量重试重试代价极高(如跨系统对账)
操作耗时短事务,秒级以内长事务,涉及多步骤业务编排
一致性要求允许短暂的重试延迟必须严格串行,不允许一次失败
典型场景阅读量、点赞、购物车数量库存秒杀、账户余额扣减、票务锁座

重点解释一下库存秒杀。很多人以为秒杀就是"高并发+写多",想当然用乐观锁。但秒杀的热点SKU一旦开抢,同一时刻可能有数千个请求一起去扣同一条库存记录。乐观锁的后果就是:第一个请求成功,后续几千个请求全部affected_rows = 0,然后陷入疯狂重试——每个重试都带着一次新的SELECT和一次新的UPDATE,数据库连接被打满,响应延迟直线上升。

这种场景悲观锁反而是更稳的选择:SELECT ... FOR UPDATE把行锁住,让扣减动作串行,虽然吞吐量下降,但至少数据库不会被重试风暴打垮。或者更极致一点,直接走Redis预扣减+异步落库,根本不让并发落到数据库行上。

我对选型的一句话体会是:乐观锁适合"你算准了没人跟你抢"的场景,悲观锁适合"你明确知道大家都会来抢"的场景。基于冲突概率来做选择,而不是因为某个技术更"高级"。

5. 电商扣库存完整实战:从伪代码到可运行实现

理论说再多,不如一段能跑起来的代码。我用一个"用户下单扣减商品库存"的案例,把乐观锁的完整落地过程串一遍。这个案例我刻意保留了一些业务细节,而不是只给一个光秃秃的SQL。

5.1 事务边界与先查再改的窗口期问题

假设商品表结构如下:

CREATE TABLE `goods` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `stock` INT NOT NULL, `version` INT NOT NULL DEFAULT 0, PRIMARY KEY (`id`) );

第一版代码(有问题的写法):

@Transactional public boolean createOrder(Long goodsId, Integer buyCount) { // 1.查询 Goods goods = goodsMapper.selectById(goodsId); // 2.判断库存 if (goods.getStock() < buyCount) { return false; } // 3.扣库存 int rows = goodsMapper.deductStock(goodsId, goods.getStock(), buyCount); // 4.创建订单 orderMapper.insert(new Order(goodsId, buyCount)); return rows == 1; }

对应的Mapper SQL:

<update id="deductStock"> UPDATE goods SET stock = stock - #{buyCount}, version = version + 1 WHERE id = #{id} AND version = #{oldVersion} </update>

问题出在哪?步骤1的SELECT和步骤3的UPDATE之间隔着一段Java代码的执行时间,这期间如果另一个事务抢先扣了库存,步骤3会因为version不匹配而返回0——这本身是乐观锁在正常工作。但请注意步骤3返回0后,方法继续执行了步骤4,插入了一笔订单。结果就是订单创建成功了,库存却没扣成功——这是严重的业务错误。

这就是乐观锁落地中常被忽略的坑:校验、扣减、订单插入必须处于同一个事务边界,并且扣减失败要立刻抛出异常回滚。修正后的代码:

@Transactional public boolean createOrder(Long goodsId, Integer buyCount) { Goods goods = goodsMapper.selectById(goodsId); if (goods.getStock() < buyCount) { return false; } int rows = goodsMapper.deductStock(goodsId, goods.getStock(), buyCount); if (rows == 0) { // 关键:抛异常让事务回滚,或者自定义积分类通知上层重试 throw new OptimisticLockException("库存已被其他订单更新,请重试"); } orderMapper.insert(new Order(goodsId, buyCount)); return true; }

再往深一步说,"先查后改"其实还有窗口期风险:极端的并发下,两个事务同时SELECT到同一版本号,都执行UPDATE。乐观锁的行为是一个成功、一个失败,这是预期的;但如果你的UPDATE条件不是版本号而是stock > buyCount,两个事务可能同时通过判断,然后在数据库行锁的串行化下先执行成功的把库存扣成负数——这是"CAS条件没约束够"造成的越界。

我推荐的做法是:在UPDATE语句里同时带上stock的下限判断,把WHERE id = ? AND version = ? AND stock >= buyCount作为完整条件,这样即使版本失效,库存约束也不会被突破。

5.2 失败重试策略的两种可靠设计

乐观锁更新失败后,是否自动重试、怎么重试,直接关系到线上表现。我见过两种踏实的设计:

方案一:有限次数重试(适合读多写少)

for (int retry = 0; retry < 3; retry++) { try { return createOrder(goodsId, buyCount); } catch (OptimisticLockException ex) { // 退避一下,避免极端情况下立即重试再次撞车 Thread.sleep(randomRetryMillis(retry)); } } return false;

注意randomRetryMillis要带一点随机性,不要所有失败请求都在同一毫秒重试,否则会形成"重试洪峰"。第一次重试可以退避20-50ms,之后逐次翻倍,最多重试3次就放弃。

方案二:版本号+库存汇总兜底(适合高价值业务)

如果扣减涉及的钱款或核心资产,不建议无脑自动重试。更好的做法是把失败记录写入重试表或消息队列,由异步任务去慢慢地、串行地处理。这种"重试注解"(比如@Retryable)很容易写,但一定要加上最大重试次数和告警通知,否则深夜静默失败没人发现。

另外补充一个连接池层面的心得:乐观锁在高冲突率场景下的重试会“吃掉”数据库连接池。每次重试都重新开启事务、重新SELECT、重新UPDATE,连接释放比正常流程慢得多。如果一定要走乐观锁+重试的路线,前端限流、后端熔断要一起配上,不能孤军奋战。

6. 我踩过的乐观锁坑位清单与复盘

最后一部分,把我过去踩过的、以及帮别人排查过的乐观锁坑位列一个清单。这些细节书本上很少写,但线上事故往往就出在这些地方。

6.1 更新语句漏掉version字段导致锁形同虚设

这是最高频的低级错误。有的人在SET子句中写了stock = stock - 1,却漏写version = version + 1;有的人在WHERE里用version = oldVersion,但oldVersion是后端接口时由前端传参的——用户在页面上打开商品详情时拿到version,磨蹭了5分钟才下单,此时version早被别人改了,于是所有正常用户都下单失败。这算乐观锁的"误伤"。

我的习惯是:版本号永远由后端在一次数据库操作内读取并校验,绝不让前端持有version超过秒级时间。页面上的版本号只能用来做展示层提示,不能作为并发控制的依据。前端传过来的version只应该当成一个参考值,到了后端要以最新的SELECT结果为准。

6.2 高并发下的重试风暴与数据库连接池耗尽

有一个真实案例:某活动页的"领取优惠券"接口用了乐观锁,券池是一个热点行,并发一上来,99%的请求都走失败重试。每个重试线程都占用一个数据库连接,等待SELECT和UPDATE往返,连接池瞬间被打满,紧接着其他依赖数据库的接口全部雪崩。

复盘下来有两个教训:

  1. 热点行的并发写用乐观锁本身就是错误选型。券池的扣减频率极高,冲突概率远超乐观锁的舒适区,应该改为行级悲观锁或Redis原子扣减。
  2. 重试必须限流。如果重试逻辑没有"最大次数+退避+熔断",乐观锁会把并发压力放大数倍——原本1万次请求,可能变成3万次SQL执行。给重试入口加上信号量限制(如Semaphore限制同时重试的线程数)很有必要。

6.3 前端领域的乐观锁:编辑冲突提示的另一种姿势

乐观锁不只在数据库里用。你的前端项目做多人协同时,也会遇到"两个人同时编辑同一份文档"的冲突问题。我在一个知识库项目里用过一次典型的乐观锁思路:每条文档记录有一个revision字段,编辑页打开时读取revision,保存提交时验证revision是否还是打开时的那个值,如果不是,提示用户"文档已被他人修改,请手动合并"。

// 保存前先提交revision,让后端校验 const resp = await api.save({ id: doc.id, revision: this.localRevision, content: this.editorContent, }); if (resp.status === 409) { // 冲突 this.showConflictDialog(); // 弹出对比合并界面 }

这在不用WebSocket实时同步的情况下,是一个成本低、体验尚可的冲突处理方案。虽然它不像CRDT或OT那样能做到无感知自动合并,但绝大多数内部工具场景已经够用了。逻辑上和数据库乐观锁完全同构:读取时记版本、提交时校验版本、冲突时引导重试或人工处理。

6.4 乐观锁与数据库隔离级别的关系:一个易被忽略的细节

最后提一个进阶层面的点。乐观锁的有效性依赖事务隔离级别吗?这里多说两句:MVCC在READ COMMITTED和REPEATABLE READ下快照读的行为不同。在READ COMMITTED下,每次SELECT都会获取新的快照,这意味着两个事务先后读到同一行时,后读的事务可能看到已提交的新版本,版本号对不上——这会让乐观锁的"先读后写"步骤变得更加不稳定,冲突率上升。在REPEATABLE READ下,事务的快照是第一次读到数据时固定的,整个事务内多次读取版本号保持一致,乐观锁的判断更"稳定"。

所以在MySQL默认的REPEATABLE READ隔离级别下,乐观锁配合MVCC使用是最顺的。如果项目里把隔离级别调成了READ COMMITTED,一定要重新审视乐观锁的重试策略是否需要调整,因为同样的代码在两种隔离级别下的冲突概率并不一样。

这个坑很隐蔽,我一次压测时发现版本冲突率莫名升高,排查了半天SQL和索引,最后才发现是某次全局配置变更把隔离级别动了。以此为戒,乐观锁不是加上就完事,它受周围数据库环境的影响远比你想象的大。

我个人在实际项目中,通常默认把乐观锁版本号放在每张核心业务表里——需要时直接拿来用,成本不过是一列加一次更新时多写一个+1。但每个用乐观锁的团队都必须清楚:它本质上是"赌并发冲突不会发生"的策略,赌输了要有计划,赌赢了也别觉得理所当然。上线前压测一下真实的冲突率和重试率,比在工位上纠结各种理论都管用。

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

计算机网络实验报告汇总:从PPP到NAT的完整实验闭环

简介&#xff1a;计算机网络课程实验报告汇总是一份面向高校计算机与网络工程专业学生的课内实验报告合集&#xff0c;系统整理了单台交换机划分VLAN、跨交换机相同VLAN互访、数据链路层PPP协议、RIP路由协议、OSPF路由协议、NAT内部源地址转换以及子网划分等典型实验。资源仅含…

作者头像 李华
网站建设 2026/10/5 11:08:54

系统调用跟踪工具 strace 与 dtruss:从内核边界排查到性能瓶颈定位

1. 先把“系统调用”讲透&#xff1a;为什么跟踪这一层能解决你八成的问题 看到标题里挂着“(-Aaa-) 系统调用跟踪命令strace和dtruss”这种社区味儿十足的写法&#xff0c;我大概猜得到楼主就是在聊两个非常老的排障工具&#xff1a;Linux 上的 strace&#xff0c;以及 macOS/…

作者头像 李华
网站建设 2026/10/5 11:07:16

DeepSeek本地化部署与医疗文本结构化:从GPU选型到JSON输出的完整管线

简介&#xff1a;面向医疗行业数据安全与AI应用落地场景的实战教程&#xff0c;围绕DeepSeek本地化部署与医疗文本结构化处理展开&#xff0c;适合医疗机构IT人员、数据工程师及对隐私保护方案感兴趣的技术学习者。内容从医疗数据隐私风险与法规出发&#xff0c;系统讲解DeepSe…

作者头像 李华
网站建设 2026/10/5 11:07:05

STM32 I2S接口PDM麦克风采集与解码实战

数字麦克风PDM信号采集与STM32 I2S接口应用&#xff08;二&#xff09; 上篇把PDM麦克风的基础原理、I2S管脚对应关系和硬件接线的坑讲了一遍&#xff0c;后台催更的留言比我想象中多。很多人卡在同一个位置&#xff1a;原理懂了、线也接上了&#xff0c;但CubeMX里那几项配置…

作者头像 李华
网站建设 2026/10/5 11:06:01

OpenHarmony上Flutter多屏适配实践与原理

最近在做一个基于 Flutter 的视力保护提醒 App 时&#xff0c;我把最大的开发精力没花在业务逻辑上&#xff0c;而是花在了屏幕适配里。这个 App 要跑在 OpenHarmony 的手机、平板&#xff0c;甚至一些 RK3588 开发板上&#xff0c;屏幕尺寸从 5 英寸到 12 英寸都有&#xff0c…

作者头像 李华