MySQL锁机制

共享锁

共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据。

使用方式:在需要执行的语句后面加上 lock in share mode

排他锁

排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。排它锁是悲观锁的一种实现。
排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁。
select语句默认不会加任何锁类型,加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
若事务 1 对数据对象A加上X锁,事务 1 可以读A也可以修改A,其他事务不能再对A加任何锁,直到事物 1 释放A上的锁。这保证了其他事务在事物 1 释放A上的锁之前不能再读取和修改A。排它锁会阻塞所有的排它锁和共享锁。

使用方式:在需要执行的语句后面加上for update

乐观锁

乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。

通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
下单操作包括3步骤:

1.查询出商品信息

select (status,status,version) from t_goods where id=#{id}

2.根据商品信息生成订单

3.修改商品status为2

update t_goods

set status=2,version=version+1

where id=#{id} and version=#{version};

悲观锁

对于悲观锁的概念解释主要有两种,但本质上悲观锁主要用于数据库访问的并发控制上。
解释一
悲观锁是指对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态,在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

解释二
在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

悲观锁处理流程:

  1. 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了
  4. 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常

MySQL InnoDB支持三种行锁定方式:

1、记录锁(Record Lock)

定义:当SQL执行按照唯一性(Primary key、Unique key)索引进行数据的检索时,查询条件等值匹配且查询的数据是存在的,这时SQL语句加上的锁即为记录锁(Record locks),锁住具体的索引项。
注意:

  • 行锁锁定的是索引记录,而不是行数据,也就是说锁定的是key
  • 如果不是索引字段,则退化成表锁。
2、间隙锁(Gap Lock)

定义:锁定索引记录间隙,确保索引记录的间隙不变。对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(GAP LOCK)。Gap Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生。
注意:

  • 间隙锁是针对事务隔离级别为【可重复读】或以上级别才有的。如果把事务的隔离级别降级为【读已提交(Read Committed, RC)】,间隙锁则会自动失效。
  • 区间锁,【左开右开】,它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
  • gap锁只会阻塞insert操作,因为gap间隙中是不存在任何记录的,除了insert操作,其他的操作结果应该都等价于空操作,mysql就不去阻塞它了
  • 范围查询或者等值查询且记录不存在
3、临键锁(Next-Key Lock)

定义:行锁和间隙锁组合起来就叫临键锁(Next-Key Lock),它的封锁范围,既包含索引记录,又包含索引区间。
注意:

  • 会锁住一段【左开右闭】区间的数据

MySQL行锁定测试

注意:

如果更新条件没有走索引,此时会进行全表扫描(行锁在 InnoDB 中是基于索引实现的),扫表的时候,要阻止其他任何的更新操作,所以退化为表锁.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> show create table test;
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| test | CREATE TABLE `test` (
`aa` varchar(30) NOT NULL,
`bb` varchar(40) NOT NULL,
`cc` varchar(50) NOT NULL,
`id` int(10) NOT NULL,
KEY `union_index` (`aa`,`bb`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

##id字段没有任何索引

事务A 事务B