好的,各位观众老爷们,今天咱们来聊聊并发控制界两大门派的绝世武功:乐观锁和悲观锁!😎 话说江湖纷争,数据江湖更是刀光剑影,一不小心就数据错乱,人仰马翻。要维护数据的一致性,就得靠锁这玩意儿了。
第一回:话说锁的江湖,悲观锁横行霸道
很久很久以前,在并发控制的江湖里,悲观锁是当之无愧的霸主。这名字一听就透着一股“我不信任任何人”的劲儿。悲观锁就像一个疑心病极重的老头,总是觉得有人要偷他的宝贝,所以在任何时候,只要他一访问某个数据,就立刻把数据锁起来,生怕别人动它一根毫毛。
这就好比你去银行取钱,悲观锁就像银行保安,你一进门,他就拉起警戒线,说:“这钱柜现在是我的了,谁也不许靠近!” 等你取完钱走了,他才把警戒线撤掉,让别人进来。
悲观锁在数据库层面的典型实现就是事务锁(Transaction Lock)。比如,你在执行SELECT ... FOR UPDATE
语句时,数据库就会对选中的数据行加锁,直到事务结束才会释放锁。
优点:
- 简单粗暴,效果好: 保证数据绝对安全,适用于对数据一致性要求极高的场景,比如银行转账。
- 实现容易: 数据库本身就提供了悲观锁机制,用起来很方便。
缺点:
- 效率低下: 并发度低,因为每次只有一个线程能访问被锁定的数据。
- 容易死锁: 如果多个线程互相等待对方释放锁,就会形成死锁,大家都僵在那里,谁也动不了。想象一下,两辆马车在狭窄的巷子里迎面相遇,谁也不让谁,结果就堵死了。
悲观锁的江湖地位:
虽然悲观锁有缺点,但在并发量不高,对数据一致性要求极高的场景下,它仍然是首选。就好比古代的城门,虽然进出很慢,但能保证城内绝对安全。
第二回:乐观锁横空出世,剑走偏锋
随着互联网的发展,并发量越来越高,悲观锁的效率问题就暴露出来了。这时候,乐观锁应运而生! 乐观锁就像一个心大的老哥,他觉得:“哎呀,没那么多人会来抢我的东西,我相信大家都是好人!” 所以,他在访问数据的时候,不会立刻加锁,而是假设没有人会修改数据。
但是,为了防止真的有人修改了数据,乐观锁会在更新数据的时候,检查一下数据是否被修改过。如果数据没有被修改,就更新数据;如果数据已经被修改,就放弃更新,并提示用户重新操作。
这就好比你去图书馆借书,乐观锁就像自助借书机,你直接拿书去扫描,如果这本书还在架上(没有被别人借走),你就能成功借走;如果这本书已经被别人借走了,系统就会提示你:“这本书已经被借走了,请选择其他书籍。”
应用层实现乐观锁的常见方法:
-
版本号机制:
- 在数据表中增加一个版本号字段(
version
)。 - 每次读取数据时,同时读取版本号。
- 更新数据时,比较当前版本号与读取时的版本号是否一致。
- 如果一致,则更新数据,并将版本号加1;如果不一致,则更新失败,提示用户重新操作。
-- 假设表名为 products,包含 id, name, price, version 字段 -- 读取数据 SELECT id, name, price, version FROM products WHERE id = 1; -- 更新数据 UPDATE products SET price = 100, version = version + 1 WHERE id = 1 AND version = 当前版本号;
- 在数据表中增加一个版本号字段(
-
时间戳机制:
- 在数据表中增加一个时间戳字段(
timestamp
)。 - 每次读取数据时,同时读取时间戳。
- 更新数据时,比较当前时间戳与读取时的时间戳是否一致。
- 如果一致,则更新数据,并将时间戳更新为当前时间;如果不一致,则更新失败,提示用户重新操作。
-- 假设表名为 products,包含 id, name, price, timestamp 字段 -- 读取数据 SELECT id, name, price, timestamp FROM products WHERE id = 1; -- 更新数据 UPDATE products SET price = 100, timestamp = NOW() WHERE id = 1 AND timestamp = '读取到的时间戳';
- 在数据表中增加一个时间戳字段(
-
CAS(Compare and Swap)算法:
- CAS 是一种原子操作,它比较内存中的值是否等于预期值,如果相等,则将内存中的值更新为新的值。
- 在 Java 中,可以使用
AtomicInteger
、AtomicLong
等原子类来实现 CAS 操作。
import java.util.concurrent.atomic.AtomicInteger; public class OptimisticLockExample { private AtomicInteger stock = new AtomicInteger(100); public void decreaseStock() { int oldValue; int newValue; do { oldValue = stock.get(); newValue = oldValue - 1; } while (!stock.compareAndSet(oldValue, newValue)); // CAS 操作 System.out.println("Stock decreased successfully!"); } public static void main(String[] args) throws InterruptedException { OptimisticLockExample example = new OptimisticLockExample(); for (int i = 0; i < 10; i++) { new Thread(example::decreaseStock).start(); } Thread.sleep(1000); // 等待所有线程执行完毕 System.out.println("Final stock: " + example.stock.get()); } }
优点:
- 并发度高: 允许多个线程同时访问数据,只有在更新的时候才进行冲突检测,大大提高了并发效率。
- 避免死锁: 不需要加锁,避免了死锁的发生。
缺点:
- 冲突率高时性能下降: 如果数据被频繁修改,导致冲突率很高,乐观锁会不断重试,反而降低了性能。
- 需要额外的业务逻辑: 需要在业务逻辑中处理更新失败的情况,比如提示用户重新操作。
乐观锁的江湖地位:
乐观锁适用于读多写少的场景,比如商品库存查询。就好比现代的图书馆,虽然人很多,但自助借书机效率很高,能满足大部分人的需求。
第三回:应用层实现乐观锁的注意事项
在应用层实现乐观锁,需要注意以下几点:
-
版本号/时间戳的选择:
- 版本号:适用于数据更新频率不高的场景,每次更新版本号加1。
- 时间戳:适用于需要记录数据最后更新时间的场景,每次更新时间戳更新为当前时间。
- 选择哪种方式取决于具体的业务需求。
-
并发冲突处理:
- 当更新失败时,需要处理并发冲突,常见的处理方式有:
- 重试: 提示用户重新操作,让用户再次尝试更新。
- 合并: 将用户的修改与最新的数据进行合并,然后更新数据。
- 放弃: 放弃用户的修改,提示用户数据已被修改。
- 选择哪种处理方式取决于具体的业务需求。
- 当更新失败时,需要处理并发冲突,常见的处理方式有:
-
ABA 问题:
- ABA 问题是指,一个值从 A 变成 B,又从 B 变回 A,乐观锁会认为数据没有被修改过,但实际上数据已经被修改过了。
- 可以使用版本号来解决 ABA 问题,每次修改数据都增加版本号,即使数据的值变回原来的值,版本号也会发生变化。
举个形象的例子:你有一个杯子,初始状态是满的(A)。你喝了一口(变成B),然后又倒满了(变回A)。如果只看杯子里的水,你会觉得什么都没发生。但实际上,你喝了一口这个动作已经被忽略了。版本号就像给杯子贴了一个标签,每次操作都更新标签,即使杯子里的水又满了,标签也变了,就能区分出这个杯子被动过了。
-
数据一致性:
- 乐观锁只能保证最终一致性,不能保证强一致性。
- 如果对数据一致性要求极高,建议使用悲观锁。
第四回:悲观锁 vs 乐观锁,谁更胜一筹?
要说谁更胜一筹,那得看具体场景。就好比华山论剑,各路高手都有自己的绝招,没有绝对的胜者。
特性 | 悲观锁 | 乐观锁 |
---|---|---|
加锁方式 | 预先加锁 | 延迟加锁,更新时检查冲突 |
并发度 | 低 | 高 |
冲突处理 | 阻塞等待 | 重试、合并、放弃 |
死锁风险 | 高 | 无 |
适用场景 | 写操作频繁,冲突概率高,对数据一致性要求极高 | 读操作频繁,冲突概率低,允许最终一致性 |
优点 | 保证数据绝对安全,实现简单 | 并发度高,避免死锁 |
缺点 | 效率低下,容易死锁 | 冲突率高时性能下降,需要额外的业务逻辑处理 |
总而言之:
- 悲观锁: 适合于“宁可错杀一千,不可放过一个”的场景,保证数据绝对安全,但效率较低。
- 乐观锁: 适合于“相信人性本善”的场景,提高并发效率,但需要处理并发冲突。
第五回:应用层乐观锁的实战案例
案例一:秒杀系统
秒杀系统是一个典型的读多写少的场景,可以使用乐观锁来提高并发效率。
-
数据库表设计:
CREATE TABLE `seckill_products` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID', `name` varchar(255) NOT NULL COMMENT '商品名称', `stock` int(11) NOT NULL COMMENT '库存', `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';
-
秒杀逻辑:
public boolean seckill(long productId) { // 1. 查询商品信息,包括库存和版本号 SeckillProduct product = productMapper.selectById(productId); int stock = product.getStock(); int version = product.getVersion(); // 2. 判断库存是否足够 if (stock <= 0) { return false; } // 3. 更新库存,使用乐观锁 int rows = productMapper.updateStock(productId, version); if (rows > 0) { // 秒杀成功 return true; } else { // 秒杀失败,说明库存已被其他线程更新 return false; } } // Mapper 接口 int updateStock(@Param("productId") long productId, @Param("version") int version); // Mapper XML <update id="updateStock"> UPDATE seckill_products SET stock = stock - 1, version = version + 1 WHERE id = #{productId} AND version = #{version} AND stock > 0 </update>
案例二:电商订单系统
电商订单系统也经常用到乐观锁。比如,用户下单后,需要更新商品的库存。
-
数据库表设计:
CREATE TABLE `products` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID', `name` varchar(255) NOT NULL COMMENT '商品名称', `stock` int(11) NOT NULL COMMENT '库存', `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-
下单逻辑:
public boolean createOrder(long productId, int quantity) { // 1. 查询商品信息,包括库存和版本号 Product product = productMapper.selectById(productId); int stock = product.getStock(); int version = product.getVersion(); // 2. 判断库存是否足够 if (stock < quantity) { return false; } // 3. 更新库存,使用乐观锁 int rows = productMapper.updateStock(productId, quantity, version); if (rows > 0) { // 下单成功 return true; } else { // 下单失败,说明库存已被其他线程更新 return false; } } // Mapper 接口 int updateStock(@Param("productId") long productId, @Param("quantity") int quantity, @Param("version") int version); // Mapper XML <update id="updateStock"> UPDATE products SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{productId} AND version = #{version} AND stock >= #{quantity} </update>
第六回:总结与展望
好了,各位观众老爷们,今天咱们就聊到这里。乐观锁和悲观锁都是并发控制的重要手段,没有绝对的好坏,只有适合与不适合。在实际应用中,需要根据具体的业务场景选择合适的锁机制。
未来,随着技术的发展,可能会出现更多更高效的并发控制方法。让我们拭目以待! 🚀
希望今天的分享对大家有所帮助!如果觉得有用,请点赞、评论、转发,让更多人受益! 🙏
最后,祝大家编码愉快,bug 永远不再! 😄