本文通过,实际应用中提炼出的两个案例,介绍一下mysql事务和锁,以及相关的一些应用技巧。

一、基本知识准备

1. 表锁

特点:
  • 不要求表引擎类型
  • 不是事务级别:需要手动解锁。
锁定方式:
  1. lock table tablename read :锁定后别的会话,能读不能写。
eg: > lock table user read;
  1. lock table tablename write :锁定后别的会话,不能读也不能写。
eg: > lock table user write;
解锁方式

unlock tables 不用指明表,会解锁所有的表。

由于表锁不是事务级别的,即使开启事务,也不会在事务结束或者提交时,自动解锁,永远需要手动解锁。一旦在解锁前,程序崩溃那么表就会处于锁定状态,一直到有人手动解锁。需要单独处理这种情况。

2.行锁

mysql 的innodb支持行锁,他又如下特点:

  • 锁是事务级别的:锁只再事务内起作用,不开启事务,行锁不起任何作用。
  • 只有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)。加上间隙锁之后,其他事务就不能在这个间隙修
改或者插入记录。
3.dml锁。

元数据锁,在修改和查询数据的时候,会给表加一个dml锁:

  • 这个锁只对修改表结构的语句起作用。
  • 如果你的修改语句有错误,会立刻返回错误,不会等待锁释放
  • 如果你的语句,正确,语句就会进入等待状态,一直到查询语句结束释放dml锁
4.乐观锁

前面三种都是,悲观锁,假设冲突为前提,处理业务。mysql中还有一种,假设没有冲突才处理业务,否则就放弃业务处理的方式,就是乐观锁。

举例:如 user 表 有积分 point 和删除状态deleted(1正常 2删除)。两个字段,要求 ,用户是删除状态时不能更新积分。这时候,我们不愿意开启事务,锁表(开启事务,效率低,锁表如果,处理不当有死锁的风险)。可以使用乐观锁
乐观锁 一般通过where条件实现:

> update user set point = point + 20 where id = xxx and deleted = 1 
然后用affectrows,来查看是否更新成功,返回成功状态。

基本思想是:把锁定条件写在where 条件里,只有在真正执行更新时,才查看是否有冲突。有冲突后就放弃更新。这种乐观的认为,自己的业务大部分情况不冲突,一旦冲突就,认为一种错误,直接终止业务和行为,就是乐观锁。

二、案例1

不加唯一索引,用程序实现某个字段的唯一性。这种情况一般出现再,有假删除字段。某个字段只要求再非删除状态时,保持唯一性,删除状态的数据可以重复。
我们不防设 user 表,user_name字段不能重复

1. 修改:

我们只需要用,新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;
2. 新增:

同样的问题,同样的方案。

select count(1) from user for update;
select * from user where user_name = newName
没有命中:
insert into user (...) value (...)
commit;
命中:
返回 用户名已经存在
commmit;
3. 删除和查询

无需特别处理

为什么不用表锁,lock table user write;表锁,没有自动释放机制,一旦程序崩溃,会造成锁释放不掉。

案例 2

用户有一个表单独存放,积分,经验等信息要求:

  1. 用户有积分的话不允许删除。
  2. 用户是删除状态,不能修改用户积分。
1.删除用户

提问:这时候能不能,只锁定积分表,不锁定用户表。
答案是不能,积分,是你的判断条件。你要修改的表。是积分的判断条件。
如果,修改积分,读取了你修改之前的用户状态,就会出现,已经删除的用户积分被修改。
如果积分也采用了事务和锁表。则会出现死锁,所以需要同时锁定,积分表和用户表。

这时候,就会出现两种情况:
情况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;

2 修改积分。

select u.deteled from user u left points p on u.id = p.uid where id = ? for update;
用户非删除状态
update points set xxxx
commit;
用户删除状态
返回错误。
commit;