数据库中的锁机制

        无论是学习、面试过程中,都经常会碰到数据库锁这个概念,而其中又包括了悲观锁‘乐观锁、表锁、行锁、间隙锁、共享锁、排他锁等等,刚接触的时候特别容易搞混,下面来跟我一起学习一下数据库中的众多锁。

 一、锁的态度:

1. 悲观锁

        就是很悲观地任认为我每次要修改数据时,其他的操作总会来改变我要修改的数据,于是就将其加锁。这样一来,其他事务只能等待我先放开锁后才能操作数据。

请看以下oracle的示例。

CREATE TABLE goods(

id NUMBER(4),

name VARCHAR2(20),

count NUMBER(100)

);

insert into goods(id, name, count ) values(1, 'Tom', 1);

insert into goods(id, name, count ) values(2, 'Lisa',  2);

使用select .. from .. where .. for update加上悲观锁:

select * from table_name where id =1 for update ;

update table_name set count = count - 1 where id= 1;

如果此时我重新开启一个事务,也来取这条id=1的数据并修改他会怎样呢?

select * from table_name where id =1 for update ; 
//下面的这行sql会等待,直到上面的事务回滚或者commit才得到执行。 
update table_name set count = count - 1 where id= 1;

我们也可以在取数据时加上nowait,这样就会先检测是否这条数据已被锁上,是就抛错,否就直接给出数据。

 *注:

  1. 当选中某一个行的时候,如果是通过主键id选中的。那么这个时候是行级锁。 
    其他的行还是可以直接insert 或者update的包。如果是通过其他的方式选中行,或者选中的条件不明确含主键。这个时候会锁表。其他的事务对该表的任意一行记录都无法进行插入或者更新操作。只能读取。
  2. MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。
  3. 使用:在InnoDB中使用悲观锁必须关闭Mysql的自动提交属性,MySQL默认使用autocommit模式。需要set autocommit=0,即不允许自动提交。
  4. 申请前提:没有线程对该结果集中的任何行数据使用排他锁或共享锁,否则申请会阻塞。for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。
  5. 优点与不足:悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数。
  6. MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。

2. 乐观锁

        就是乐观地认为我每次要修改数据时,不会有人来修改我要修改的数据,所以不上锁,但是在提交的时候会检测在我修改期间有没有人修改此数据,如果没有则顺利提交,如果有则需要用业务逻辑去解决数据不一致问题。

1、乐观并发控制,假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据,如果其他事务又更新,正在提交的事务会进行回滚。

2、相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突,则返回用户错误信息,让用户决定如何去做。在对数据库进行处理时,乐观锁不会使用数据库提供的锁机制。

3、实现方式:版本号(记录数据版本)、时间戳。

4、流程:创建一张表时添加一个version字段,表示是版本号,修改数据的时候首先把这条数据的版本号查出来。

5、数据版本:为数据增加的一个版本标识,当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新,当我们提交更新时,判断数据库对应记录的当前版本信息与第一次取出的版本标识比对,若一致,表明这条数据没有被其他用户修改,予以更新,否则是过期数据(数据在操作期间被其他用户修改过,此时需要在代码中抛出异常或回滚)。使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号

6、优点与不足:乐观并发控制相信事务之间的数据竞争的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

7、update是单线程,即如果一个线程对一条数据进行update操作,会获得锁,其他线程如果对同一条数据操作会阻塞,直到这个线程update成功后释放锁。

8、乐观锁不需要数据库底层支持。
 

二、MySQL中的锁:

1、存储引擎:

MyISAM存储引擎:表级锁

InnoDB存储引擎:既支持行级锁,也支持表级锁,默认情况下是采用行级锁。

2、表级锁

开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低

②表共享读锁(Table Read Lock):lock tables xxx read local

③表独占写锁(Table Write Lock):lock tables xxx write

④查询表级锁争用情况

没有索引的情况下,InnoDB只能使用表锁。

表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;

⑦可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺:

show status like 'table%';

3、行级锁

开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率最低,并发度最高

②行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

③InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁! 
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。

④即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决 定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突 时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。 

比如,在tab_with_index表里的name字段有索引,但是name字段是varchar类型的,检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点

mysql> explain select * from tab_with_index where name = 1 \G
mysql> explain select * from tab_with_index where name = '1' \G

④查看行锁的争用情况

show status like 'innodb_row_lock%'; 

4、页面锁

开锁和加锁时间界于表锁和行锁之间,会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

5、间隙锁

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。 
举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:

Select * from  emp where empid > 100 for update;

是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。

InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使 用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另外一方面,是为了满足其恢复和复制的需 要。有关其恢复和复制对锁机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况,在后续的章节中会做进一步介绍。

很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!下面这个例子假设emp表中只有101条记录,其empid的值分别是1,2,……,100,101。 
InnoDB存储引擎的间隙锁阻塞例子 

 三、锁的方式:

1、读锁(共享锁S):

允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。InnoDB通过使用lock in share mode加读锁,但是注意只锁覆盖索引

2、写锁(排他锁X):

允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。InnoDB所有的DML操作默认加写锁。select可以通过for update加写锁,并且会锁住所有索引,不仅仅是索引覆盖的索引。

注:

1、InnoDB下,是给索引加行锁,如果没有通过索引条件检索数据,则会使用表锁。

共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE  ... FOR UPDATE

2、加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

3、意向锁:

表明某个事物正在锁定一行或者将要锁定一行。

在判断每一行是否已经被行锁锁定效率比较低下,因此使用意向锁,当发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。

申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。

IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突

综合一下行级锁和意向表级锁的兼容性

本文参考:

https://blog.csdn.net/qq_44766883/article/details/105879308

https://blog.csdn.net/yxx1915146/article/details/108449159

https://blog.csdn.net/sherry_y_fan/article/details/80548560

Guess you like

Origin blog.csdn.net/weixin_47465999/article/details/120777240