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; --不言而喻,这也可以成功
结论:
- 主键明确存在,对应条件的数据存在,对于一个事务对该条数据加了行级锁之后,另外的事务将不能获得该条数据的行级锁,就不能对这些数据进行操作(获取排它锁的读,修改),但是普通的查询可以成功。另外的事务对于其他没有被锁住的数据时可以正常的访问的。
- 没有主键,没有任何的索引,这种情况只要是使用了
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对数据的共享锁进行获取,进入等待