在并发编程和数据库事务中锁是保证数据一致性的重要手段。乐观锁和悲观锁是两种最基础、最常用的并发控制策略。本文将详细解释它们的定义、用法、优缺点、适用场景并给出简单示例和注意事项帮助你更好地理解和选型。一、悲观锁Pessimistic Lock1. 是什么悲观锁是一种假设最坏情况的锁机制。它认为在数据处理过程中一定会有其他事务来修改数据因此在操作数据之前会先对数据加锁阻塞其他事务的访问直到当前事务完成。这种机制“悲观”地认为冲突一定会发生所以主动阻止冲突。2. 怎么用在关系型数据库中悲观锁通常通过select ... for update语句实现。例如-- 事务1 begin; select * from product where id 1 for update; -- 加行锁 update product set stock stock - 1 where id 1; commit; -- 事务2 需要等待事务1释放锁 begin; select * from product where id 1 for update; -- 阻塞直到事务1提交 ... commit;在编程中如Java也可以使用synchronized或ReentrantLock实现悲观锁但数据库层面的悲观锁更常见于分布式或跨进程场景。3. 优点强一致性彻底避免了数据被并发修改保证数据正确性。简单易用开发者无需额外处理冲突重试逻辑数据库负责阻塞和排队。4. 缺点性能较差加锁会增加开销且阻塞其他事务降低并发度。死锁风险多个事务相互等待对方释放锁可能导致死锁。不适合长事务长时间持有锁会严重拖垮系统吞吐量。5. 适用场景写多读少数据被频繁修改冲突概率极高悲观锁可以避免大量无效重试。对一致性要求极严例如金融系统扣款、转账、库存扣减超卖问题。锁竞争不激烈如果冲突频繁悲观锁反而能减少重试开销但注意性能瓶颈。6. 简单例子场景商品秒杀扣减库存假设库存只有1件两个用户同时下单。使用悲观锁-- 用户A begin; select stock from product where id 1 for update; -- 获得锁 -- 假设stock1 update product set stock 0 where id 1; commit; -- 用户B 此时会等待直到A提交后B的select for update才会返回发现stock0无法下单。7. 注意事项索引for update必须走索引否则可能锁表影响巨大。事务范围加锁后尽快提交或回滚避免长时间占用锁。死锁处理数据库通常会自动检测死锁并回滚一个事务但业务层最好设计合理的加锁顺序。隔离级别悲观锁通常在Read Committed或Repeatable Read下工作注意间隙锁MySQL InnoDB可能扩大锁范围。二、乐观锁Optimistic Lock1. 是什么乐观锁是一种假设最好情况的锁机制。它认为数据在大多数情况下不会发生冲突所以读取数据时不加锁只在更新时检查数据是否被其他事务修改过。如果发现冲突则放弃当前更新并重试或报错。通常使用版本号或时间戳实现。2. 怎么用最常用的方式是给表增加一个version字段更新时检查版本号-- 1. 查询数据及版本号 select id, stock, version from product where id 1; -- version5 -- 2. 更新时检查版本号 update product set stock stock - 1, version version 1 where id 1 and version 5; -- 如果更新影响行数为1表示成功否则说明版本号已变需要重试。在编程中可以配合循环重试逻辑// 伪代码 while (true) { Product p dao.select(id); int oldVersion p.getVersion(); int affected dao.updateStock(id, p.getStock() - 1, oldVersion); if (affected 0) break; // 重试或者抛出异常 }3. 优点高并发性能读操作不加锁写操作只在提交时检查适合读多写少场景。无死锁不使用数据库锁机制避免死锁问题。实现轻量只需增加版本字段无需复杂的锁管理。4. 缺点冲突处理复杂更新失败后需要业务层重试增加了代码复杂度。写冲突频繁时效率低如果大量写操作冲突重试会浪费CPU资源甚至导致饥饿。非强一致性依赖应用层重试极端情况下可能丢失更新如果重试逻辑不当。5. 适用场景读多写少大部分操作是读取偶尔更新冲突概率低。业务允许少量重试例如用户修改个人资料、点赞、日志记录等。分布式系统不希望使用数据库锁如跨多个数据库或微服务。6. 简单例子场景编辑文章多人同时保存文章表有content和version字段。-- 用户A读取文章version1 -- 用户B也读取文章version1 -- 用户A先保存 update article set content 新内容A, version 2 where id 1 and version 1; -- 成功version变为2 -- 用户B保存时 update article set content 新内容B, version 2 where id 1 and version 1; -- 影响行数为0因为version已经是2了。用户B需重新读取再保存。7. 注意事项版本号必须自增每次成功更新version1。重试策略需要设置最大重试次数避免无限循环重试时可加入随机退避减少冲突。范围更新乐观锁通常针对单行更新批量更新需要更复杂的设计如使用where条件包含所有待检查的版本。ABA问题版本号机制天然避免了ABA因为版本号严格递增但如果使用时间戳要注意精度问题如毫秒级可能重复。与事务隔离级别的关系乐观锁通常在Read Committed下工作因为Repeatable Read可能读到旧快照但版本号检查在更新时仍然有效。三、对比总结维度悲观锁乐观锁设计思想悲观冲突一定会发生乐观冲突很少发生加锁时机读数据时加锁更新时检查实现方式select for update版本号 / 时间戳 / CAS并发性能低阻塞其他事务高无阻塞冲突处理等待锁释放业务重试适用场景写多读少要求强一致性读多写少允许重试典型例子金融扣款、秒杀库存用户资料修改、计数器四、如何选择高写冲突如热点商品秒杀→ 悲观锁更合适因为乐观锁会频繁重试反而浪费性能。低写冲突如普通业务修改→ 乐观锁更合适能获得更好的并发吞吐。一致性要求极端严格且事务短→ 悲观锁简单可靠。分布式环境数据库锁不可用→ 乐观锁或分布式锁方案。混合使用例如在秒杀场景中可以先用乐观锁预扣库存版本号如果冲突则快速失败配合队列削峰或者使用悲观锁严格控制超卖。五、总结乐观锁和悲观锁没有绝对的优劣关键在于结合业务场景和并发特征。理解它们的原理和代价才能做出合理的架构设计。记住一个简单原则冲突少用乐观冲突多用悲观。实际开发中很多系统会从乐观锁开始实现简单性能好当发现写冲突导致大量重试时再切换到悲观锁或分布式锁。