本文通过,实际应用中提炼出的两个案例,介绍一下mysql事务和锁,以及相关的一些应用技巧。
eg: > lock table user read;
eg: > lock table user write;
unlock tables 不用指明表,会解锁所有的表。
由于表锁不是事务级别的,即使开启事务,也不会在事务结束或者提交时,自动解锁,永远需要手动解锁。一旦在解锁前,程序崩溃那么表就会处于锁定状态,一直到有人手动解锁。需要单独处理这种情况。
mysql 的innodb支持行锁,他又如下特点:
共享锁:select * from tableName where … + lock in share more
排他锁:select * from tableName where … + for update
注意行锁只再事务内有意义,再事务commit 或rollback后就会释放。
提交事务,或者回滚事务。
kill process 杀掉造成死锁的查询。
MySQL InnoDB 的行锁 支持三种行锁定方式:
- 行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。
- 间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。
间隙锁是针对事务隔离级别为可重复读或以上级别而已的。
- Next-Key Lock :行锁和间隙锁组合起来就叫Next-Key Lock。
默认情况下,InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据
行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁和间隙锁的组合,当
InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记
录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修
改或者插入记录。
元数据锁,在修改和查询数据的时候,会给表加一个dml锁:
前面三种都是,悲观锁,假设冲突为前提,处理业务。mysql中还有一种,假设没有冲突才处理业务,否则就放弃业务处理的方式,就是乐观锁。
举例:如 user 表 有积分 point 和删除状态deleted(1正常 2删除)。两个字段,要求 ,用户是删除状态时不能更新积分。这时候,我们不愿意开启事务,锁表(开启事务,效率低,锁表如果,处理不当有死锁的风险)。可以使用乐观锁
乐观锁 一般通过where条件实现:
> update user set point = point + 20 where id = xxx and deleted = 1
然后用affectrows,来查看是否更新成功,返回成功状态。
基本思想是:把锁定条件写在where 条件里,只有在真正执行更新时,才查看是否有冲突。有冲突后就放弃更新。这种乐观的认为,自己的业务大部分情况不冲突,一旦冲突就,认为一种错误,直接终止业务和行为,就是乐观锁。
不加唯一索引,用程序实现某个字段的唯一性。这种情况一般出现再,有假删除字段。某个字段只要求再非删除状态时,保持唯一性,删除状态的数据可以重复。
我们不防设 user 表,user_name字段不能重复
我们只需要用,新user_name ,去数据库中查是否有相关记录,如果没有就修改,如果有就报重复返回。
但是,并发时,可能会出现 多个记录都查到没有,都改成相同名字导致user_name不唯一的情况,这时候,就需要,开启事务锁住相关记录,然后在修改。具体操作如下:
但是这里会有一个问题,如果们查到了数据,自然会锁住相关数据,(有数据就会有索引,有索引就能枷锁),可是如果我们查到一个空,没有命中数据,就肯定加不上锁。有没有一种办法,肯定能锁住数据,肯定让update都是串行的呢。select count(1) from user for update 这实际上是锁住了全表。
代码如下
select count(1) from user for update;
select * from user where user_name = newName
没有命中:
update user set ...
commit;
命中:
返回 用户名已经存在
commit;
同样的问题,同样的方案。
select count(1) from user for update;
select * from user where user_name = newName
没有命中:
insert into user (...) value (...)
commit;
命中:
返回 用户名已经存在
commmit;
无需特别处理
为什么不用表锁,lock table user write;表锁,没有自动释放机制,一旦程序崩溃,会造成锁释放不掉。
用户有一个表单独存放,积分,经验等信息要求:
提问:这时候能不能,只锁定积分表,不锁定用户表。
答案是不能,积分,是你的判断条件。你要修改的表。是积分的判断条件。
如果,修改积分,读取了你修改之前的用户状态,就会出现,已经删除的用户积分被修改。
如果积分也采用了事务和锁表。则会出现死锁,所以需要同时锁定,积分表和用户表。
这时候,就会出现两种情况:
情况1:业务中有规定,创建用户信息时,一定会同时创建一条,积分信息(同一个事务中创建)
情况2 :不同时创建积分信息。
情况1 :的处理非常简单,只要同时锁定,信息中的两条数据就行。
select p.jifen from user u left points p on u.id = p.uid where id = ? for update;
如果积分为0 删除用户
update user set deleted = 2 where id = ?;
commit;
不为0返回积分,不为0;
commit;
情况2:对于情况2一般不建议这样做,因为,积分表有可能会新增数据,新增的数据,通过锁定带条数据是,实现不了的,这时只能锁定整个表。放在id主键索引上的间隙多,就会阻塞所有的 插入和更新操作,具体操作如下。
select count(1) from points for update; select id from user where id=? for update;
select * from point where uid=?
如果积分为0 删除用户
update user set deleted = 2 where id = ?;
commit;
不为0返回积分,不为0;
commit;
select u.deteled from user u left points p on u.id = p.uid where id = ? for update;
用户非删除状态
update points set xxxx
commit;
用户删除状态
返回错误。
commit;