从对待
锁
的态度来看锁的话,可以将锁分成乐观锁
和悲观锁
,从名字中也可以看出这两种锁是两种看待数据并发的思维方式
。需要注意的是,乐观锁
和悲观锁
并不是锁,而是锁的设计思想
。
一、悲观锁(Pessimistic Locking)
悲观锁
是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度
,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性
。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞
直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
1、实战1-秒杀案例
商品秒杀过程中,库存数量的减少,避免出现
超卖
的情况。如:商品表中有一个字段为 stock 表示当前该商品的库存量。假设商品为华为 mate40,id为1001,stock=100个。如果不使用锁的情况下,操作方法如下
- 第1步:查出商品库存
SELECT stock
FROM t_goods
WHERE id = 1001;
- 第2步:如果库存大于 0,则根据商品信息生产订单
INSERT INTO t_order(good_id) VALUES (1001);
- 第3步:修改商品的库存,num 表示购买商品数量
UPDATE t_goods
SET stock = stock - num
WHERE id = 1001;
- 在
高并发环境
下可能出现以下问题,其中线程B此时已经下单并且减完库存,这个时候线程A依然去执行step3,就造成了超卖
高并发环境.png
2、解决超卖
使用
悲观锁
可以解决这个问题,商品信息从查询出来到修改,中间有一个生成订单的过程,使用悲观锁
的原理就是,当我们在查询 t_goods 信息后就把当前的数据锁定,直到我们修改完毕后再解锁
。那么整个过程中,因为数据被锁定了,就不会出现有第三者来对其进行修改了。而这样做的前提是需要将要执行的 SQL 语句放在同一个事务中,否则达不到锁定数据行的目的
- 第1步:查出商品库存,并且加
排他锁
SELECT stock
FROM t_goods
WHERE id = 1001 FOR UPDATE ;
- 第2步:如果库存大于 0,则根据商品信息生产订单
INSERT INTO t_order(good_id) VALUES (1001);
- 第3步:修改商品的库存,num 表示购买商品数量
UPDATE t_goods
SET stock = stock - num
WHERE id = 1001;
小结
-
SELECT . . . FOR UPDATE
是 MySQL 中悲观锁
。此时在t_goods表
中,id为 1001 的那条数据就被我们锁定了,其他的要执行SELECT stock FROM t_goods WHERE id = 1001 FOR UPDATE
语句的事务必须等本次事务提交
之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。 -
注意
:当执行SELECT stock FROM t_goods WHERE id = 1001 FOR UPDATE
语句之后,如果其他事务中执行SELECT stock FROM t_goods WHERE id = 1001
语句,并不会受第一个事务的影响,仍然可以正常查询出数据。 -
注意
:SELECT . . . FOR UPDATE
语句执行过程中所有扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住 -
悲观锁不适用的场景较多,它存在一些不足,因为悲观锁大多数情况下依靠数据库的锁机制来实现,以保证程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是
长事务
而言,这样的开销往往无法承受
,这时就需要乐观锁
二、 乐观锁(Optimistic Locking)
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是
不采用数据库自身的锁机制,而是通过程序来实现
。在程序上,我们可以采用版本号机制
或者CAS机制
实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量
。在Java中java.util.concurrent.atomic
包下的原子变量类就是使用了乐观锁的一种实现方式:CAS机制
实现的。
1、 乐观锁的版本号机制
在表中设计一个
版本字段 version
,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version
。此时如果已经有事务对这条数据进行了更改,修改就不会成功。
2、乐观锁的时间戳机制
时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。
你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。
3、实战1-秒杀案例
- 第1步:查出商品库存
SELECT stock
FROM t_goods
WHERE id = 1001;
- 第2步:如果库存大于 0,则根据商品信息生产订单
INSERT INTO t_order(good_id) VALUES (1001);
- 第3步:修改商品的库存,num 表示购买商品数量
UPDATE t_goods
SET stock = stock - num
WHERE id = 1001 and version = #{version};
小结
如果数据表是
读写分离
的,当 master 表中写入的数据没有及时同步到 slave 表中时,会造成更新一直失败的问题。此时需要强制读取 master 表
中的数据(将 SELECT 语句放到事务中即可,这时候查询的就是 master 主库了)
-
如果对同一条数据进行
频繁的修改
的话,那么就会出现这么一种场景,每次修改都只有一个事务能更新成功,在业务感知上面就有大量的失败操作。需要如下改善 -
第1步:查出商品库存
SELECT stock
FROM t_goods
WHERE id = 1001;
- 第2步:如果库存大于 0,则根据商品信息生产订单
INSERT INTO t_order(good_id) VALUES (1001);
- 第3步:修改商品的库存,num 表示购买商品数量
UPDATE t_goods
SET stock = stock - num
WHERE id = 1001 and stock - num > 0;
小结:乐观锁和悲观锁的适用场景
-
乐观锁
适合读操作
多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁
问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作 -
悲观锁
适合写操作
多的场景,因为写的操作具有排它性
。采用悲观锁
的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读-写
和写-写
的冲突
image.png
网友评论