前言:我们一个普通的下单接口通常都包含如下三步操作,如果下单不成功的话将会返回给用户一个提示下单失败。
- 查询库存(select stock from xx where id = xx)
- 扣减更新库存(update xx set stock = stock - 1 where id = xx)
- 生成订单
如果是只有一个用户来请求下单接口,那么上述的操作毫无疑问是一定正常的,可是真实环境中可是处处存在并发的,如果此时库存数量为1,在高并发的情况下面用户一、二都查询到了库存,然后都来扣减库存那么会导致库存数量变为-1,这种情况是我们要杜绝的。
解决方案
- 悲观锁:我们可以在查询库存这一步加悲观锁(for update),然后常规的扣减库存操作。简而言之查询加锁,常规更新。悲观锁的体现:每次查询都认为有人来竞争资源,都会对资源进行加锁操作。在查库存阶段就已经对数据上锁。
- 乐观锁 :查询库存不加锁,更新携带版本号更新,简而言之查询不加锁,更新加版本号。加锁的时机在更新库存的时候。
悲观锁执行流程图(拿下单接口举例)
- 查询库存(select stock from xx where id = xx for update)
- 扣减更新库存(update xx set stock = stock - 1 where id = xx)
- 生成订单
下单接口伪代码
悲观锁下单接口实现流程
乐观锁执行流程(拿下单接口举例)
- 查询库存(select stock,version from xx where id = xx)
- 扣减更新库存(update xx set stock = stock - 1 ,version = version + 1 where id = xx and version = ?)
- 生成订单
乐观锁接口伪代码
悲观锁与乐观锁的使用场景以及优缺点分析
分析乐观锁与悲观锁的区别之前,我们要知道我们在执行
update xx set stock = stock-1 where id = 1
语句的时候,mysql会自动给id = 1的这条数据给上锁。而mysql进行上锁是会带来开销的。mysql上锁详解过程参考 mysq锁原理
悲观锁使用场景:适合并发冲突比较多的场景。
悲观锁缺点:
- 无论有没有并发操作,一定会有互斥锁(for update)带来的开销。
- 使用不当还容易造成死锁的问题
乐观锁使用场景:适合并发冲突比较少的场景
乐观锁的优点:乐观锁衍生出来的目的就在于减少mysql中互斥锁(for update)的开销,在冲突比较少的情况下面对比悲观锁,乐观锁可以有效的减少mysql使用互斥锁给我们带来的开销,与其说乐观锁是一把锁,不如说乐观锁其实是一种我们能否修改数据的依据
什么叫做乐观锁适合并发冲突比较少的场景?
乐观锁是查询不加锁的,在有多个用户请求下单接口的时候,由于查询不加锁,那么这些用户都会来到更新库存这,而我们在执行一条update语句的时候会给所更新的记录加上一把行锁防止其他事务更新同一条数据。所以多个用户来到更新库存这里必定只有一个用户能更新成功(这里要理解并发与并行的区别)。而由于更新成功的用户进行了version++操作,等其他的用户接着更新的时候where条件根本就不会满足了,更新个锤子直接返回给用户下单失败的消息了,这就导致了一个问题,如果有100个人同时请求下单接口的话只有一个人会请求成功,其他的99个人直接就失败了。体验感不好啊
为什么说悲观锁适合并发冲突比较高的场景?
悲观锁在更新库存的时候,在查询库存的时候就已经对相应的记录给上过锁了,其次才是更新库存。假设有100个人同时请求下单接口,100个人会并发的去查库存,由于查库存是进行了加锁操作,只有一个人能查到库存,并且执行接下来的更新库存操作。剩下的99个人呢?由于竞争不到锁会卡在那里,直至等到锁释放才会正常的查询到库存然后执行更新库存的操作。虽然说这些人在等待锁释放的过程中会消耗一些时间,但是最起码不会直接给用户返回下单失败的信息吧。你敢说你在参与秒杀活动的时候,系统是秒回的嘛。