本文主要是对【MySQL中Update语句是悲观锁?还是乐观锁?】中提到乐观锁与悲观锁的补充。
在 MySQL中,悲观锁是需要依靠数据库提供的锁机制实现的,在 InnoDB 引擎中,使用悲观锁,就需要先关闭 MySQL 数据库的自动提交属性,然后通过 select ... for update 来进行加锁。
在数据库中,悲观锁的流程如下:
- 在对记录进行修改前,先尝试为该记录加上排他锁。
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
使用悲观锁实现扣减库存
-- 1.开始事务
begin;
-- 2.查询出商品信息
select quantity from goods where id=1 for update;
-- 3.修改商品quantity为2
update goods set quantity=2 where id=1;
-- 4.提交事务
commit;
在对 id=1 的记录修改前,会先通过 for update 的方式进行加锁,然后再进行修改。这就是典型的悲观锁策略。
如果修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得 id=1 的锁,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。
注意:
使用 select…for update 虽然会把数据给锁住,但需要注意一些锁的级别,MySQL InnoDB 默认行级锁,行级锁都是基于索引的。
如果一条SQL语句没有用到索引,优化器在选择执行计划时,如果发现全表扫描可能比索引扫描性能更好,可能会选择直接锁定整个表进行操作,以提高查询性能。
MySQL 中的乐观锁在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
CAS 是乐观锁,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
使用乐观锁实现扣减库存:
-- 查询出商品信息,quantity=3
select quantity from goods where id=1
-- 修改商品quantity为2
update goods set quantity=2 where id=1 and quantity=3;
以上,在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当我们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。
注意:
如果你认为乐观锁的整个过程中完全没有任何锁的参与的话那就大错特错了。
虽然在使用乐观锁的时候,没有显式的加锁,也没有用到对他的相关锁机制。但是乐观锁是使用 updata 语句过程中实现的,update 的过程是有锁的。
数据库在更新时,会根据where条件中是否包含索引考虑加锁范围,如果有索引,那么就使用索引添加行级锁(可能还有gap 或者 next key),如果没有索引 ,那么就会添加表级锁。
所以,乐观锁的过程中,并不是完全无锁的。
既然乐观锁既然也有锁,那么他相比悲观锁意义在哪里呢?
乐观锁最大的好处就是通过 CAS 的方式做并发校验,这个过程不需要提前加锁,只需要在更新的那一刻加一个短暂的锁而已,而悲观锁的话,需要你先 select ...... for update,锁的时长要长得多。