悲观锁

# 悲观锁的范围

悲观锁:select ... for update

我们模拟一个例子,首先插入新的表数据:

image-20221003210232665

//首先执行如下SQL语句,操作的行数显示为2行,在事务提交之前,这两条数据是被锁住了
update db_stock set count = count - 1 where product_code='1001' and count >= 1;

//然后我们对id=3这条数据进行操作,看看能不能进行操作
update db_stock set count = count - 1 where id = 3;

//执行回车,被阻塞了,然后我们提交或者回滚第一条事务
rollback;
//可以看到事务二立刻被放行了
1
2
3
4
5
6
7
8
9

由此可见,这种SQL语句是一种表级锁,在一般的实际业务中,是非常不建议使用这种表级锁的。

# 在MySQL悲观锁中使用行级锁

悲观锁使用行级锁需要以下几个条件:

  • 锁的查询或者更新条件必须是索引字段

在表中给product_code添加索引'idx_pc',然后执行以下流程:

//事务一执行更新操作,但不提交事务
update db_stock set count = count - 1 where product_code='1001' and count >= 1;

//事务二执行更新操作
update db_stock set count = count - 1 where id = 3;

//事务二可以正常执行,没有被阻塞
1
2
3
4
5
6
7
  • 查询或更新条件必须是具体值
//事务一执行更新操作,但条件改为like模糊判断,以001结尾的数据
update db_stock set count = count - 1 where product_code like'%001' and count >= 1;

//或者事务一条件改为 !=判断
update db_stock set count = count - 1 where product_code !='1002' and count >= 1;

//开启事务二,也进行更新操作
update db_stock set count = count - 1 where id = 3;

//事务二被阻塞了,因为索引失效了
1
2
3
4
5
6
7
8
9
10

# 悲观锁使用select for update减库存

使用方法:select ... for update

注意:在此之前,product_code字段已经是加了索引的。

//事务一执行
select * from db_stock where product_code='1001' for update
//事务二执行减库存
update db_stock set count = count - 1 where id = 1;
1
2
3
4

加了'for update'语句后,我们可以发现,事务二的写操作的会被阻塞的,因为事务一用了悲观锁的行级锁。这个时候,只有事务一被提交或者回滚了,锁才会被释放。

代码实现:

在Mapper接口创建方法:

@Select("select * from db_stock where product_code= #{productCode} for update")
    List<Stock> queryStock(@Param("productCode") String productCode);
1
2

在Service层编写业务代码:

@Autowired
    private StockMapper stockMapper;

    @Transactional
    public void deduct() {
        //1.查询库存信息,并且锁定库存信息
        List<Stock> stocks = stockMapper.queryStock("1001");
        //这里取第一个仓库库存来计算
        Stock stock = stocks.get(0);
        //2.判断库存是否充足
        if (stock != null && stock.getCount() > 0) {
            //3.扣减库存
            stock.setCount(stock.getCount() - 1);
            stockMapper.updateById(stock);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在集群模式下,通过压力测试,并发问题没有出现。

悲观锁的性能不是非常高,因为有多条SQL语句执行,锁的粒度变大了

# 悲观锁select for update存在的问题

优点:

  • 解决了一个SQL语句在同一个商品有多条库存记录下不够灵活的问题
  • 解决了一个SQL语句无法记录库存前后变化的状态的问题

缺点:

  • 性能虽然比JVM本地锁高一点,但是比起一条SQL语句的性能,差的非常远
  • 死锁问题,对多条数据加锁时,加锁顺序要一致
  • 库存操作要统一,select for update是减库存时的操作,但是在实际业务中,还有出库入库等等其他操作,而且也是频繁的操作,也会被影响到,所以要全部都使用统一语句