悲观锁

悲观锁是利用数据库本身的锁机制来实现,会锁记录。

以一种预防的姿态在修改数据之前先把数据锁住,然后再对数据进行操作,在它释放锁之前任何人都不能对其数据进行操作,知道前面一个释放锁,后面才能对数据进行加锁,然后才可以对苏话剧进行操作。

特点:可以保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁,然后再进行操作,最后再解锁,然而加锁释放锁的过程会造成消耗,所以性能不高。

实现方式:

1
select * from t_table where id = 1 for update

要使用悲观锁,我们必须关闭MySQL数据库的自动提交属性。因为MySQL默认使用autocommit模式,也就是说,当我们执行一个更新操作后,MySQL会立刻将结果进行提交。(sql语句:set autocommit=0)

以下单过程为例:

1
2
3
4
5
6
7
8
// 开始事务
begin;
// 查询商品库存信息
select quantity from items where id = 1 for update;
// 修改商品库存为2
update items set quantity = 2 where id = 1;
// 提交事务
commit;

在修改id=1的记录之前,先用for update进行加锁操作,然后修改

如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。

乐观锁

使用乐观锁就不需要借助数据库的锁机制了,主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

扣减库存:

1
2
3
4
// 查询出商品信息,
select quantity from items where id = 1;
// 修改商品库存为2
update items set quantity = 2 where id = 1 and quantity = 3;

在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当我们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据.

但是存在一个问题,即传说中的ABA问题。

比如说一个线程one从数据库中取出库存数3,这时候另一个线程two也从数据库中取出库存数3,并且two进行了一些操作变成了2,然后two又将库存数变成3,这时候线程one进行CAS操作发现数据库中仍然是3,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

有一个比较好的办法可以解决ABA问题,那就是通过一个单独的可以顺序递增的version字段。改为以下方式即可:

1
2
3
4
// 查询出商品信息,version=1
select * from items where id = 1;
// 修改商品库存
update items set quantity = 2, version = 3 where id = 1 and version = 2;

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

以上SQL其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。

对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度的。

有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

1
2
3
4
// 修改商品库存
update item
set quantity = quantity - 1
where id = 1 and quantity - 1 > 0

以上SQL语句中,如果用户下单数为1,则通过quantity - 1 > 0的方式进行乐观锁控制。

以上update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1。

高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。