Mysql 中的 select * from tableName for update

悲观锁:

悲观锁是对数据被修改持有悲观的态度,认为数据在被修改的时候一定会存在并发的问题,因此,在整个数据处理的过程中将数据锁定,悲观锁的实现往往是依靠数据提供的锁机制,其实,也只有数据库层,提供的锁机制才能真正保证数据访问的排他性,否则,即使在应用层中实现了枷锁机制,也无法外部系统不会修改数据。

场景:

仓库中,商品表goods中有字段enable_count,表示商品的剩余数量,当我们下单购买商品的时候,数量count会随之减少,同时会在order订单表中插入相应的数据,当然,下单的时候,商品的enable_count必须大于零

如果不采用加锁的方式,那么操作的方法如下:

# 1,查询出商品信息,假设:查询商品ID=1的,而且,该商品的可用数量只剩 1
select `enable_count`,`id` from goods where id = 1 and enable_count > 0;

# 2,根据商品生成order数据
insert into `order` (id,goods_id,`count`) values(1,1,1);

# 3,修改商品的数量
update goods set enable_count = enable_count-1 where id = 1;

上面这种场景,在高并发的情况下,可能就会出现问题,前面所提到的,当商品的enable_count大于零的时候,才允许下单。但是,当一个人(甲)还没有完成第三步的时候,有其他人(乙)同时进入了第一步,第二步,而且在甲没有完成第三步的时候,乙先完成了第三步,让goods表中的enable_count减一,因为只剩下1,所以此时就为0了,此时甲再执行第三步,然后再减一,就变成了-1,可用数量为-1这种情况是不可能的,所以这种方式是不安全的。

使用悲观锁来实现:

同样是上面的案例,使用悲观锁的原理就是,当我们查询出goods表中的数据之后,就把当前这条数据进行锁定,直到我们修改完毕之后再解锁,那么在这个过程中,因为goods表中的该条数据,所以不会被其他人对其进行修改了。

方式如下:tip:我们使用悲观锁的时候,必须关闭mysql中的自动提交

# 1,设置mysql不能自动提交
set autocommit = 0;

# 2,开始事务
begin;                  --这里也可以使用  begin work;  或者    start transaction;

# 3,查询出商品信息,假设:查询商品ID=1的,而且,该商品的可用数量只剩 1
select `enable_count`,`id` from goods where id = 1 and enable_count > 0 for update;

# 4,根据商品生成order数据
insert into `order` (id,goods_id,`count`) values(1,1,1);

# 5,修改商品的数量
update goods set enable_count = enable_count-1 where id = 1;

# 6,提交事务,我们在第一步将自动提交关闭了,这里要进行手动的提交
commit;                 --这里也可以使用  commit work;

可以看到,在第三步中,使用了 select ..... for update 的方式,这样就通过数据库实现了悲观锁,在goods表中,id = 1 and enable_count > 0 的数据就被锁定,其他人(其他事务),必须等待本次事务提交了之后才能执行,保证了当前处理的数据,不会被其他事务修改;

同时:注意一下select ..... for update 的行级锁(Row Lock)和表级锁(Table Lock),mysql中,InnoDB默认的就是行级锁,但是,只有明确地指明主键,mysql才会触发行级锁去锁住被选中的数据,否则,就会触发表级锁,将整表锁住。当然,这里如果是索引,同样也会触发同样的效果,具体的效果如下:

以下通过两个Console来模拟两个事务,分别为A,B。

tip: 可通过 show variables like 'autocommit'; 来查看是否是设置了自动提交,ON自动提交打开,OFF自动提交关闭

1,明确指定主键,并且有对应条件的数据,触发行级锁。

A:
set autocommit = 0;
select * from goods where id = 1 for update;      --当执行完这句的时候,先不止像下面的 commit
commit;
B:
select * from goods where id = 1 for update;      --执行该语句的时候,被阻塞,等待A释放行级锁

update goods set name = '衣服2' where id = 1;     --执行该语句的时候,被阻塞,等待A释放行级锁

select * from goods where id = 1;                 --不获取排它锁,只是普通查询,可以成功

select * from goods where id = 2 for update;      --由于行级锁 锁住的不是该条数据,可以成功

select * from goods where id = 2;                 --不言而喻,这也可以成功

结论:

  • 主键明确存在,对应条件的数据存在,对于一个事务对该条数据加了行级锁之后,另外的事务将不能获得该条数据的行级锁,就不能对这些数据进行操作(获取排它锁的读修改),但是普通的查询可以成功。另外的事务对于其他没有被锁住的数据时可以正常的访问的。

1638878074326.png

  • 没有主键,没有任何的索引,这种情况只要是使用了select ..... for update ,就不再是行级锁,而是表级锁,当一个事务第一次执行了该语句的时候,就将整表锁了起来,其他的事务将不能对表中的任何一条数据进行加锁的操作,同理,普通的查询是可以的,比如简单的:select * from tabelname where cloum = ''

总结一下:

如果一个事务(A)没有使用索引去命中数据,
那A是加的表级锁,会将所以的数据锁住,其他事务(B)对于该表的任何加锁操作
都会被阻塞,等待之前事务(A)的提交之后释放锁。

如果一个事务(A)使用了索引(唯一,普通)、或者主键 去命中数据,那A
就是对于该条数据是加的行级锁,其他事务(B)对于该条数据的任何加锁操作都会被阻塞,
等待A提交之后释放锁,但是对于其他没有命中的数据,B是可以进行操作的

同一行数据

  • 不使用索引和使用索引去命中数据时,事务A,B对改行数据加共享锁

  • 事务A对数据加共享锁,事务B进行更新操作,进入等待,等待A释放锁才可以进行更新操作

  • 事务A对数据加共享锁,事务B先进行共享锁的获取,再进行更新数据操作,进入等待

  • 事务A对数据加共享锁,事务B获取排他锁,进入等待 可以证明为啥事务A加共享锁的时候,事务B不能进行更新数据操作,因为想要更新数据,就要获取数据的排它锁,但是数据被加了共享锁,就不能获取它的排他锁了,所以就不能更新啦。

  • 事务A获取数据的排他锁,事务B对数据的共享锁进行获取,进入等待