news 2026/4/23 17:33:17

并发冲突怎么处理:乐观锁/悲观锁/最终一致性(附PRD写法)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
并发冲突怎么处理:乐观锁/悲观锁/最终一致性(附PRD写法)

前言

并发冲突是多用户系统的常见问题:两人同时编辑同一条数据,后提交的覆盖了先提交的。这篇给你3种并发控制策略的完整对比和PRD写法。

一、3种并发控制策略对比

策略原理适用场景优点缺点
乐观锁提交时检查版本号读多写少性能高,无锁等待冲突时需重试
悲观锁操作前先加锁写多读少强一致性性能低,可能死锁
最终一致性允许短暂不一致分布式系统高可用业务复杂度高

二、乐观锁(推荐)

原理

每条记录有一个版本号(version),更新时检查版本号是否一致。如果版本号不匹配,说明数据已被他人修改,返回冲突。

适用场景:读多写少,冲突频率低(如编辑商品信息、编辑用户资料等)

优点:性能高,无锁等待,适合高并发场景

缺点:冲突时需要重试,用户体验稍差

实现步骤

  1. 数据库设计:在表中添加version字段(整数类型,默认值为1)
  2. 查询数据:查询时返回version字段
  3. 提交更新:更新时带上version字段作为条件
  4. 检查结果:如果更新影响行数=0,说明版本号不匹配,返回冲突
  5. 用户提示:提示用户刷新页面,重新编辑

PRD写法

场景:编辑商品信息 并发控制策略:乐观锁(版本号机制) 【数据库设计】 表结构:product表添加version字段(整数类型,默认值为1) 示例:id, name, price, version 【实现方式】 1. 查询商品时,返回版本号(version) SELECT id, name, price, version FROM product WHERE id = ? 2. 提交更新时,带上版本号 UPDATE product SET name=?, price=?, version=version+1 WHERE id=? AND version=? 3. 检查更新结果 - 如果更新影响行数>0,说明更新成功 - 如果更新影响行数=0,说明版本号不匹配,返回冲突 【用户提示】 冲突提示:数据已被他人更新,请刷新后重试 恢复路径:刷新页面,重新编辑(不要自动重试) 【技术实现】 // 伪代码 function updateProduct(productId, newName, newPrice, version) { const affectedRows = db.execute( "UPDATE product SET name=?, price=?, version=version+1 WHERE id=? AND version=?", [newName, newPrice, productId, version] ); if (affectedRows === 0) { return { success: false, message: "数据已被他人更新,请刷新后重试" }; } return { success: true, message: "更新成功" }; }

真实案例

场景:两人同时编辑商品价格

时间线: 10:00:00 - 用户A查询商品,price=100, version=1 10:00:01 - 用户B查询商品,price=100, version=1 10:00:05 - 用户A提交:price=120, version=1 → 成功,version变为2 10:00:06 - 用户B提交:price=110, version=1 → 失败(version已变为2) 10:00:07 - 系统提示用户B:数据已被他人更新,请刷新后重试 10:00:08 - 用户B刷新页面,看到price=120, version=2 10:00:10 - 用户B重新编辑:price=110, version=2 → 成功,version变为3 结果:用户A的修改(price=120)被保留,用户B的修改(price=110)在刷新后成功提交

最佳实践

  • 版本号字段:使用整数类型,每次更新自动+1
  • 版本号初始化:新记录version默认为1
  • 版本号检查:更新时必须检查version,不能跳过
  • 冲突处理:不要自动重试,提示用户刷新页面重新编辑
  • 前端提示:冲突时明确提示"数据已被他人更新",不要只说"更新失败"

常见错误

  • 错误1:更新时不检查version,导致覆盖他人修改
    ❌ 错误:UPDATE product SET name=? WHERE id=? ✅ 正确:UPDATE product SET name=?, version=version+1 WHERE id=? AND version=?
  • 错误2:冲突时自动重试,导致用户修改丢失
    ❌ 错误:冲突时自动重试,可能导致用户修改丢失 ✅ 正确:冲突时提示用户刷新页面,重新编辑
  • 错误3:版本号字段类型错误,导致精度问题
    ❌ 错误:version字段使用浮点数类型 ✅ 正确:version字段使用整数类型(INT或BIGINT)

三、悲观锁

原理

操作前先加锁(SELECT FOR UPDATE),其他人只能等待锁释放后才能操作。保证同一时间只有一个请求能修改数据。

适用场景:写多读少,冲突频率高(如秒杀扣减库存、抢票等)

优点:强一致性,不会丢失更新

缺点:性能低,可能死锁,不适合高并发场景

实现步骤

  1. 开启事务:在事务中执行操作
  2. 加锁查询:使用SELECT FOR UPDATE加锁
  3. 执行业务逻辑:在锁保护下执行业务逻辑
  4. 提交事务:提交事务时自动释放锁
  5. 异常处理:如果发生异常,回滚事务,释放锁

PRD写法

场景:秒杀扣减库存 并发控制策略:悲观锁(SELECT FOR UPDATE) 【实现方式】 1. 开启事务 BEGIN TRANSACTION; 2. 查询库存时加锁 SELECT * FROM stock WHERE id=? FOR UPDATE; (其他请求会等待,直到锁释放) 3. 检查库存 IF qty > 0 THEN 扣减库存:UPDATE stock SET qty=qty-1 WHERE id=? ELSE 返回"库存不足" END IF 4. 提交事务,释放锁 COMMIT; (如果发生异常,回滚事务:ROLLBACK;) 【用户提示】 库存不足:库存不足,请选择其他商品 恢复路径:无(库存已售罄) 【技术实现】 // 伪代码 function deductStock(productId) { try { db.beginTransaction(); // 加锁查询 const stock = db.query("SELECT * FROM stock WHERE id=? FOR UPDATE", productId); if (stock.qty <= 0) { db.rollback(); return { success: false, message: "库存不足" }; } // 扣减库存 db.execute("UPDATE stock SET qty=qty-1 WHERE id=?", productId); db.commit(); return { success: true, message: "扣减成功" }; } catch (error) { db.rollback(); return { success: false, message: "扣减失败,请重试" }; } }

真实案例

场景:秒杀活动,1000个用户同时抢购最后10件商品

时间线(使用悲观锁): 10:00:00 - 用户A查询库存,加锁,qty=10 10:00:00 - 用户B查询库存,等待锁... 10:00:00 - 用户C查询库存,等待锁... 10:00:01 - 用户A扣减库存,qty=9,提交事务,释放锁 10:00:01 - 用户B获得锁,查询库存,qty=9 10:00:01 - 用户C继续等待锁... 10:00:02 - 用户B扣减库存,qty=8,提交事务,释放锁 10:00:02 - 用户C获得锁,查询库存,qty=8 ... 10:00:10 - 用户J获得锁,查询库存,qty=0 10:00:10 - 用户J返回"库存不足" 10:00:10 - 用户K获得锁,查询库存,qty=0 10:00:10 - 用户K返回"库存不足" 结果:只有10个用户成功购买,其他用户收到"库存不足"提示 (如果使用乐观锁,可能会有超卖问题)

最佳实践

  • 锁粒度:尽量缩小锁的范围,只锁必要的行
  • 锁超时:设置锁超时时间(如30s),避免死锁
  • 事务时间:尽量缩短事务时间,减少锁持有时间
  • 死锁检测:数据库自动检测死锁,自动回滚其中一个事务
  • 索引优化:WHERE条件使用索引,减少锁范围

常见错误

  • 错误1:忘记提交事务,导致锁一直持有
    ❌ 错误:加锁后忘记提交事务,导致锁一直持有 ✅ 正确:加锁后立即执行业务逻辑,然后提交事务
  • 错误2:锁范围过大,导致性能问题
    ❌ 错误:SELECT * FROM stock FOR UPDATE(锁整个表) ✅ 正确:SELECT * FROM stock WHERE id=? FOR UPDATE(只锁一行)
  • 错误3:没有设置锁超时,导致死锁
    ❌ 错误:没有设置锁超时,死锁时一直等待 ✅ 正确:设置锁超时时间(如30s),超时自动释放

死锁处理

死锁场景:两个事务互相等待对方释放锁

时间线(死锁场景): 10:00:00 - 事务A:锁定商品1,等待商品2 10:00:01 - 事务B:锁定商品2,等待商品1 10:00:02 - 死锁发生,数据库自动检测并回滚其中一个事务 【预防措施】 1. 按相同顺序加锁(如按id排序) 2. 尽量缩小锁范围 3. 设置锁超时时间 4. 使用索引,减少锁范围

四、最终一致性

原理

允许短暂不一致,通过消息队列/事件驱动最终达到一致。适合分布式系统,保证高可用性。

适用场景:分布式系统,允许短暂不一致(如订单支付后更新库存、用户注册后发送欢迎邮件等)

优点:高可用,性能好,适合分布式系统

缺点:业务复杂度高,需要处理消息丢失、重复消费等问题

实现步骤

  1. 业务操作:执行业务操作(如订单支付)
  2. 发送消息:发送消息到消息队列(如RabbitMQ、Kafka)
  3. 消费消息:消费者服务消费消息,执行业务逻辑(如扣减库存)
  4. 重试机制:如果消费失败,重试3次
  5. 人工介入:如果重试3次仍失败,发送告警,人工介入

PRD写法

场景:订单支付后更新库存 并发控制策略:最终一致性(消息队列) 【实现方式】 1. 订单支付成功后,发送消息到队列 - 消息内容:{orderId, productId, quantity} - 消息队列:RabbitMQ / Kafka - 消息持久化:是(防止消息丢失) 2. 库存服务消费消息,扣减库存 - 消费者:库存服务 - 消费逻辑:扣减库存 - 幂等性:使用订单号作为幂等键,防止重复消费 3. 如果扣减失败,重试3次 - 重试间隔:1秒、3秒、5秒(指数退避) - 重试次数:3次 - 重试失败:发送告警,人工介入 4. 仍失败则人工介入 - 告警方式:邮件、短信、钉钉 - 处理方式:人工检查,手动处理 【用户提示】 支付成功:支付成功,库存更新中(预计1-2分钟) 恢复路径:等待系统处理,如有问题联系客服 【技术实现】 // 伪代码(订单服务) function payOrder(orderId) { // 1. 支付订单 const payment = processPayment(orderId); if (!payment.success) { return { success: false, message: "支付失败" }; } // 2. 发送消息到队列 const message = { orderId: orderId, productId: payment.productId, quantity: payment.quantity }; messageQueue.send("stock.deduct", message); return { success: true, message: "支付成功,库存更新中" }; } // 伪代码(库存服务) function consumeStockMessage(message) { try { // 1. 检查幂等键(防止重复消费) const existingDeduction = redis.get(`deduct:${message.orderId}`); if (existingDeduction) { return { success: true, isDuplicate: true }; } // 2. 扣减库存 const result = deductStock(message.productId, message.quantity); if (!result.success) { throw new Error("库存扣减失败"); } // 3. 保存幂等键 redis.set(`deduct:${message.orderId}`, { productId: message.productId }, 3600); return { success: true }; } catch (error) { // 重试3次 if (retryCount < 3) { retryCount++; setTimeout(() => consumeStockMessage(message), retryCount * 1000); } else { // 发送告警 sendAlert("库存扣减失败", message); } } }

真实案例

场景:订单支付后更新库存、发送短信、更新积分

时间线(最终一致性): 10:00:00 - 用户支付订单,订单状态变为"已支付" 10:00:01 - 订单服务发送3条消息到队列: - 消息1:扣减库存 - 消息2:发送短信 - 消息3:更新积分 10:00:02 - 库存服务消费消息1,扣减库存(成功) 10:00:03 - 短信服务消费消息2,发送短信(成功) 10:00:04 - 积分服务消费消息3,更新积分(失败,重试) 10:00:05 - 积分服务重试消息3,更新积分(成功) 结果:订单支付成功,库存已扣减,短信已发送,积分已更新 (虽然积分更新延迟了3秒,但最终达到一致)

最佳实践

  • 消息持久化:消息必须持久化,防止消息丢失
  • 幂等性:消费者必须支持幂等,防止重复消费
  • 重试机制:消费失败时重试,使用指数退避策略
  • 死信队列:重试3次仍失败,发送到死信队列,人工处理
  • 监控告警:监控消息积压、消费延迟,及时告警

常见错误

  • 错误1:消息没有持久化,服务重启后消息丢失
    ❌ 错误:消息不持久化,服务重启后消息丢失 ✅ 正确:消息必须持久化,防止消息丢失
  • 错误2:消费者不支持幂等,重复消费导致数据错误
    ❌ 错误:消费者不支持幂等,重复消费导致库存扣减2次 ✅ 正确:消费者支持幂等,使用订单号作为幂等键
  • 错误3:没有重试机制,消费失败后消息丢失
    ❌ 错误:消费失败后直接丢弃消息 ✅ 正确:消费失败后重试3次,仍失败则发送到死信队列

不适合的场景

最终一致性不适合以下场景:

  • 金额操作:支付、退款等金额操作必须强一致性
  • 库存扣减:秒杀、抢票等库存扣减必须强一致性
  • 账户余额:账户余额变更必须强一致性

适合的场景:

  • 订单支付后更新库存:允许短暂不一致,最终达到一致
  • 用户注册后发送邮件:允许短暂延迟,最终发送成功
  • 数据同步:主从数据库同步,允许短暂延迟

五、选择策略的决策树

根据业务场景选择合适的并发控制策略,以下是决策流程:

Q1:是否允许短暂不一致? ├─ 是 → 最终一致性(消息队列) │ └─ 适用场景:订单支付后更新库存、用户注册后发送邮件等 └─ 否 → Q2(必须强一致性) Q2:冲突频率高吗? ├─ 高(写多读少,如秒杀、抢票)→ 悲观锁(SELECT FOR UPDATE) │ └─ 优点:强一致性,不会丢失更新 │ └─ 缺点:性能低,可能死锁 └─ 低(读多写少,如编辑商品、编辑用户)→ 乐观锁(版本号) └─ 优点:性能高,无锁等待 └─ 缺点:冲突时需要重试

策略对比表

策略适用场景性能一致性复杂度
乐观锁读多写少(编辑商品、编辑用户)强一致性
悲观锁写多读少(秒杀、抢票)强一致性
最终一致性分布式系统(订单支付后更新库存)最终一致性

实际项目案例

案例1:电商系统

  • 编辑商品信息:乐观锁(读多写少,冲突频率低)
  • 秒杀扣减库存:悲观锁(写多读少,冲突频率高)
  • 订单支付后更新库存:最终一致性(允许短暂不一致)

案例2:内容管理系统

  • 编辑文章:乐观锁(读多写少,冲突频率低)
  • 发布文章:悲观锁(写多读少,冲突频率高)
  • 文章发布后发送通知:最终一致性(允许短暂延迟)

案例3:金融系统

  • 账户余额变更:悲观锁(必须强一致性,不能丢失更新)
  • 交易记录查询:不需要锁(只读操作)
  • 交易后发送短信:最终一致性(允许短暂延迟)

六、常见错误与陷阱

错误1:乐观锁更新时不检查version

问题:更新时不检查version,导致覆盖他人修改。

❌ 错误示例: UPDATE product SET name=? WHERE id=? 问题:不检查version,可能覆盖他人修改 ✅ 正确示例: UPDATE product SET name=?, version=version+1 WHERE id=? AND version=? 检查version,如果version不匹配,更新失败

错误2:悲观锁忘记提交事务

问题:加锁后忘记提交事务,导致锁一直持有,其他请求一直等待。

❌ 错误示例: BEGIN TRANSACTION; SELECT * FROM stock WHERE id=? FOR UPDATE; UPDATE stock SET qty=qty-1 WHERE id=?; -- 忘记提交事务,锁一直持有 ✅ 正确示例: BEGIN TRANSACTION; SELECT * FROM stock WHERE id=? FOR UPDATE; UPDATE stock SET qty=qty-1 WHERE id=?; COMMIT; -- 提交事务,释放锁

错误3:最终一致性用于强一致性场景

问题:金额操作、库存扣减等强一致性场景使用最终一致性,导致数据错误。

❌ 错误示例: 支付扣款使用最终一致性(消息队列) 问题:支付成功后,库存可能还没扣减,导致超卖 ✅ 正确示例: 支付扣款使用悲观锁(强一致性) 库存扣减必须在支付时同步完成,不能异步处理

错误4:乐观锁冲突时自动重试

问题:乐观锁冲突时自动重试,可能导致用户修改丢失。

❌ 错误示例: 乐观锁冲突时自动重试,使用旧数据重新提交 问题:用户的新修改可能丢失 ✅ 正确示例: 乐观锁冲突时提示用户刷新页面,重新编辑 不要自动重试,让用户看到最新数据后重新编辑

错误5:悲观锁范围过大

问题:悲观锁范围过大,锁整个表或大量行,导致性能问题。

❌ 错误示例: SELECT * FROM stock FOR UPDATE 问题:锁整个表,其他请求全部等待,性能极差 ✅ 正确示例: SELECT * FROM stock WHERE id=? FOR UPDATE 只锁一行,其他请求可以正常访问其他行

错误6:最终一致性没有幂等性

问题:消息队列消费者不支持幂等,重复消费导致数据错误。

❌ 错误示例: 消费者不支持幂等,重复消费导致库存扣减2次 问题:消息重复消费时,库存扣减多次 ✅ 正确示例: 消费者支持幂等,使用订单号作为幂等键 重复消费时,检查幂等键,已处理则直接返回

七、FAQ

Q1:乐观锁冲突了怎么办?

答:提示用户刷新页面重新编辑。不要自动重试,因为用户的修改可能已经过时,自动重试可能导致用户修改丢失。

处理流程:

  1. 检测到version不匹配,返回冲突错误
  2. 前端提示用户"数据已被他人更新,请刷新后重试"
  3. 用户刷新页面,获取最新数据和version
  4. 用户重新编辑,使用最新的version提交

Q2:悲观锁会死锁吗?

答:会。两个事务互相等待对方释放锁时会发生死锁。

预防措施:

  • 按相同顺序加锁(如按id排序)
  • 尽量缩小锁范围
  • 设置锁超时时间(如30s),超时自动释放
  • 使用索引,减少锁范围

处理方式:数据库自动检测死锁,自动回滚其中一个事务。

Q3:最终一致性适合所有场景吗?

答:不适合。以下场景必须使用强一致性:

  • 金额操作:支付、退款等金额操作必须强一致性
  • 库存扣减:秒杀、抢票等库存扣减必须强一致性
  • 账户余额:账户余额变更必须强一致性

适合的场景:

  • 订单支付后更新库存:允许短暂不一致,最终达到一致
  • 用户注册后发送邮件:允许短暂延迟,最终发送成功
  • 数据同步:主从数据库同步,允许短暂延迟

Q4:乐观锁和悲观锁哪个性能更好?

答:乐观锁性能更好,因为不需要加锁,无锁等待。但冲突时需要重试,用户体验稍差。

性能对比:

  • 乐观锁:读多写少场景,性能高,无锁等待
  • 悲观锁:写多读少场景,性能低,有锁等待

选择建议:根据冲突频率选择,冲突频率低用乐观锁,冲突频率高用悲观锁。

Q5:如何监控并发控制效果?

答:监控以下指标:

  • 乐观锁冲突率:version不匹配的次数 / 总更新次数
  • 悲观锁等待时间:锁等待的平均时间
  • 死锁次数:死锁发生的次数
  • 最终一致性延迟:消息消费的平均延迟

告警阈值:

  • 乐观锁冲突率 > 10%:考虑使用悲观锁
  • 悲观锁等待时间 > 1秒:考虑优化锁范围
  • 死锁次数 > 0:立即排查死锁原因
  • 最终一致性延迟 > 5分钟:检查消息队列积压

Q6:分布式系统如何保证强一致性?

答:分布式系统保证强一致性有以下方案:

  • 两阶段提交(2PC):保证所有节点同时提交或回滚,但性能低
  • 三阶段提交(3PC):改进2PC,减少阻塞时间,但复杂度高
  • 分布式锁:使用Redis或Zookeeper实现分布式锁,保证同一时间只有一个请求执行
  • 最终一致性:允许短暂不一致,通过消息队列最终达到一致

选择建议:根据业务需求选择,强一致性场景用分布式锁,允许短暂不一致用最终一致性。

Q7:如何测试并发控制?

答:使用压力测试工具(如JMeter、LoadRunner)模拟并发请求:

  • 乐观锁测试:模拟多个用户同时编辑同一条数据,检查是否覆盖他人修改
  • 悲观锁测试:模拟多个用户同时扣减库存,检查是否超卖
  • 最终一致性测试:模拟消息重复消费,检查是否幂等

测试指标:

  • 并发用户数:100、500、1000
  • 请求成功率:> 99%
  • 数据一致性:100%(不能丢失更新)
  • 响应时间:< 1秒

工具入口

生成并发控制思维导图

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

HTML动态图表生成:Miniconda-Python3.10集成Plotly可视化库

HTML动态图表生成&#xff1a;Miniconda-Python3.10集成Plotly可视化库 在数据密集型项目中&#xff0c;最令人头疼的往往不是算法本身&#xff0c;而是环境配置和成果展示——你是否也经历过“代码跑通了&#xff0c;但同事打不开图表”“换台机器就报错”“静态图表达不清趋势…

作者头像 李华
网站建设 2026/4/23 13:17:32

锂电池均衡之主动均衡Simulink仿真探索

锂电池均衡 主动均衡 simulink仿真 耦合电感类 耦合电感四节电池 耦合电感加开关电容的六节电池在锂电池应用系统中&#xff0c;电池均衡技术对于提升电池组性能、延长使用寿命至关重要。主动均衡作为一种更为高效的均衡方式&#xff0c;近年来备受关注。今天咱就聊聊基于耦合电…

作者头像 李华
网站建设 2026/4/23 16:05:43

Linux下PyTorch安装教程GPU支持:基于Miniconda-Python3.10镜像快速部署

Linux下PyTorch安装教程GPU支持&#xff1a;基于Miniconda-Python3.10镜像快速部署 在人工智能项目开发中&#xff0c;最让人头疼的往往不是模型设计本身&#xff0c;而是环境搭建——明明代码没问题&#xff0c;却因为PyTorch版本和CUDA不匹配、Python依赖冲突导致“在我机器…

作者头像 李华
网站建设 2026/4/23 16:06:16

CodeSys——TCP客户端通讯

本示例工程以汇川AC712控制器为例&#xff0c;打开“InoProShop”&#xff0c;新建工程。PLC_PRG代码&#xff1a;PROGRAM POU VARTCP_Client_0: TCP_Client;m1: STRING : 200.200.200.90;m0: BOOL;port: UINT : 9004;hClint: __XWORD;TCP_Send_0: TCP_Send;m2: BOOL;uiSize: U…

作者头像 李华
网站建设 2026/4/19 8:18:39

Pyenv install python3.10失败?切换Miniconda-Python3.10绕过编译难题

Pyenv install python3.10失败&#xff1f;切换Miniconda-Python3.10绕过编译难题 你有没有在深夜调试环境时&#xff0c;被 pyenv install 3.10 卡住几个小时&#xff1f;明明只是想跑个简单的机器学习脚本&#xff0c;却因为 OpenSSL 缺失、zlib 编译失败或 GCC 版本不兼容而…

作者头像 李华
网站建设 2026/4/23 1:54:27

FlipperKit报错

报错&#xff1a; CompileC /Users/admin/Library/Developer/Xcode/DerivedData/Windoent-fslitwgmpysmhabgkknjkzcpexfd/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/FlipperKit.build/Objects-normal/x86_64/FlipperPlatformWebSocket.o /Users/admin/De…

作者头像 李华