通常我们为了防止并发读写导致的数据不一致或者数据覆盖问题会采取锁的方式 解决。今天介绍一把数据库级别的锁,使用for update语句产生的数据库锁,以及可能导致的表锁问题。(测试版本处于mysql 5.x版本)
先来看一个这样的问题。就拿超卖举个例子.现在用户A与用户B。大家都要同时对一个订单做扣减,假如A查完库存以后,cpu调度到了用户B执行的线程上,这个时候再查出来也是100,然后各自做了各自的扣减之后,数据库还是99.就发生了超卖的问题。针对这个问题其实有很多解决方案,可以上分布式锁也可以上数据库锁,也可以把订单扣减改成异步队列操作,但是队列要保证消息的有序性。下面演示数据库行锁来解决这个问题,以及操作不当可能又会带来什么问题。
拿account表来掩饰,表结构以及数据如下
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(40) DEFAULT NULL,
`money` float DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
INSERT INTO `bbs`.`account` (`id`, `name`, `money`) VALUES ('1', 'A', '400');
INSERT INTO `bbs`.`account` (`id`, `name`, `money`) VALUES ('2', 'B', '1300');
INSERT INTO `bbs`.`account` (`id`, `name`, `money`) VALUES ('3', 'C', '1000');
现在我要操作id为1的数据,首先第一步是要查询出来然后做后面一系列的业务操作,在我操作id=1的数据时,不希望其他线程可以拿到还未更改的数据进行操作,然后导致数据覆盖问题。这个时候我采用这样的方式进行修改
#这里注意一定要在事务当中,否则for update不会产生行锁。
#用户A先开始操作订单1
BEGIN;
select * from account where id = '1' for UPDATE
#==========
#用户B开始操作订单1
BEGIN;
select * from account where id = '1' for UPDATE
#这个时候会出现什么样的情况呢?id为1的这一行数据会被锁起来
#在用户A的事务没有执行完以前,用户B是拿不到这一行数据的,会被堵塞。
#假如这个时候用户B操作的是订单2,那么是不会被堵塞的
BEGIN;
select * from account where id = '2' for UPDATE
#因为行锁锁的是id为1的行
上述例子已经成功的产生的了行锁。假如我们换个查询条件又会产生什么样的问题呢?例如这样
BEGIN;
select * from account where id = '1' for UPDATE
#==========
#用户A开始操作name=1
BEGIN;
select * from account where name = '1' for UPDATE
#用户B开始操作name=1
BEGIN;
select * from account where name = '1' for UPDATE
#这个时候name为1的行被锁起来,这是正常的。
#那么我们再来试试操作name=2的行
BEGIN;
select * from account where name = '2' for UPDATE
#这个时候你会发现name=2的行也被锁住了,行锁已经升级成为了表锁
#那么是什么导致了这样的差异?为什么id就是行锁,name就是表锁?
先别急着知道答案,我再给你演示一个操作,假如这个时候我们把name加上索引,再来试一下上述操作,你会发现只会出现行锁,而不会产生表锁。
总结:如果使用不带索引的列作为查询条件来使用for update产生行锁,那么会出现表锁的问题,所以大家使用for update一定要注意查询条件是否建立了索引