这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战。
悲观锁和乐观锁是用来解决并发问题的两种思想,在不同的平台有着各自的实现。例如在Java中,synchronized
就可以认为是悲观锁的实现(不严谨,有锁升级的过程,升级到重量级锁才算),Atomic***
原子类可以认为是乐观锁的实现。
悲观锁
具有强烈的独占和排他特性,在整个处理过程中将数据处于锁定状态,一般是通过系统的互斥量来实现。当其他线程想要获取锁时会被阻塞,直到持有锁的线程释放锁。
乐观锁
对数据的修改和访问持乐观态度,假设不会发生冲突,只有当数据提交更新时才会对数据冲突与否进行检测,如果没有冲突则顺利提交更新,否则快速失败,返回一个错误给用户,让用户选择接下来该如何去做,一般来说失败后会继续重试,直到提交更新成功为止。
MySQL本身就支持锁机制,例如我们有一个「先查再写」的需求,我们希望整个流程是一个原子操作,中间不能被打断,这时候就可以通过给查询的数据行加「排他锁」来实现。只要当前事务不释放锁,其他事务要想获得排他锁,MySQL就会将其阻塞,直到当前事务释放锁。这种MySQL底层的排他锁就称作「悲观锁」。
MySQL本身不提供乐观锁的功能,需要开发者自己实现。普遍的做法是在表中加一个version
列,用来标记数据行的版本,当我们需要更新数据时,必须比对version
版本,version
一致说明这个期间数据没有被其他事务修改过,否则说明数据已经被其他事务修改,需要自旋重试了。
实战
假设数据库有两张表:商品表和订单表。
用户下单后需要执行两个操作:
- 商品表减去库存。
- 订单表创建一条记录。
初始数据:ID为1的商品有100的库存,订单表数据为空。
客户端启动10个线程并发下单,分别在无锁、悲观锁、乐观锁的场景下有哪些表现。
如下是创建表的sql语句:
1 | sql复制代码-- 商品表 |
1、无锁
不做任何处理。
1 | java复制代码// 下单 |
控制台输出结果:
2、悲观锁
查询商品时加FOR UPDATE
,给数据行加排他锁,这样其他线程再查询时就会被阻塞,直到当前线程的事务提交并释放锁,其他线程才能继续下单。这种方式并发性能不高。
sql语句
1 | java复制代码@Select("SELECT * FROM goods WHERE id = #{id} FOR UPDATE") |
控制台输出结果:
注意:FOR UPDATE必须在事务中才有效,查询和更新必须在同一个事务中!!!
3、乐观锁
实现思路是:每次更新时校验版本号,如果版本号一致说明期间数据没有被其他线程改过,当前线程可以正常提交更新,否则说明数据已经被其他线程改过了,当前线程需要自旋重试,直到业务成功为止。
更新数据的同时版本号必须自增!!!
1 | java复制代码@Update("UPDATE goods SET stock = #{stock},version = version+1 WHERE id = #{id} AND version = #{version}") |
业务代码
1 | java复制代码boolean order(){ |
控制台输出结果:
本文转载自: 掘金