什么是悲观锁
在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
简而言之,悲观锁主要用于保护数据的完整性。当多个事务并发执行时,某个事务对数据应用了锁,则其他事务只能等该事务执行完了,才能进行对该数据进行修改操作。
使用场景
在商品购买场景中,当有多个用户对某个库存有限的商品同时进行下单操作。若采用先查询库存,后减库存的方式进行库存数量的变更,将会导致超卖的产生。
若使用悲观锁,当B用户获取到某个商品的库存数据时,用户A则会阻塞,直到B用户完成减库存的整个事务时,A用户才可以获取到商品的库存数据。则可以避免商品被超卖。
如何使用悲观锁
用法:SELECT … FOR UPDATE;
例如,
select * from tbl_user where id=1 for update;
获取锁的前提:结果集中的数据没有使用排他锁或共享锁时,才能获取锁,否则将会阻塞。
需要注意的是, FOR UPDATE
生效需要同时满足两个条件时才生效:
- 数据库的引擎为 innoDB
- 操作位于事务块中(BEGIN/COMMIT)
体验悲观锁
Step 1 初始化表结构和数据
CREATE TABLE `tbl_user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`status` int(11) DEFAULT NULL,
`name` varchar(255) COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `tbl_user` (`id`, `status`, `name`)
VALUES
(1,1,X'7469616E'),
(2,1,X'63697479');
Step 2
窗口1
// 关闭mysql数据库的自动提交属性
set autocommit=0;
// 开启事务
BEGIN;
SELECT * FROM tbl_user where id=1 for update;
窗口2
此时,我们在窗口2执行下面这条命令,尝试获取悲观锁:
SELECT * FROM tbl_user where id=1 for update;
执行完后,窗口2并没有像窗口1一样,立刻返回结果,而是发生了阻塞。
若超时间未获取锁,将会得到一个锁超时错误提示。如下图所示:
行锁与表锁
当执行 select ... for update
时,将会把数据锁住,因此,我们需要注意一下锁的级别。MySQL InnoDB 默认为行级锁。当查询语句指定了主键时,MySQL会执行「行级锁」,否则MySQL会执行「表锁」。
常见情况如下:
- 若明确指明主键,且结果集有数据,行锁;
- 若明确指明主键,结果集无数据,则无锁;
- 若无主键,且非主键字段无索引,则表锁;
- 若使用主键但主键不明确,则使用表锁;
select * from tbl_user where id<>1 for update;
若需要了解更多情况,可以阅读 此篇文章了解更多。
小结: innoDB的行锁是通过给索引上的索引项加锁实现的,因此,只有通过索引检索数据,才会采用行锁,否则使用的是表锁。
总结
悲观锁采用的是「先获取锁再访问」的策略,来保障数据的安全。但是加锁策略,依赖数据库实现,会增加数据库的负担,且会增加死锁的发生几率。此外,对于不会发生变化的只读数据,加锁只会增加额外不必要的负担。在实际的实践中,对于并发很高的场景并不会使用悲观锁,因为当一个事务锁住了数据,那么其他事务都会发生阻塞,会导致大量的事务发生积压拖垮整个系统。