数据库常见面试题——锁

常见问题:

  • MyISAM与 InnoDB关于锁方面的区别是什么
  • 数据库事务的四大特性
  • 事务隔离级别以及各级别下的并发访问问题
  • InnoDB可重复读隔离级别下如何避免幻读
  • RC、RR级别下的InnoDB的非阻塞读如何实现

MyISAM与 InnoDB关于锁方面的区别是什么

  • MyIsAM默认用的是表级锁,不支持行级锁
  • InnoDB默认用的是行级锁,也支持表级锁

MyIsAM默认支持的表级锁

表级锁的含义: 直接锁定整张表,在你锁定期间,其它进程无法对该表进行写操作。如果你是写锁,则其它进程则读也不允许。特点:开销小,加锁快;不会出现死锁;锁定粒度最大(等级最高),发生锁冲突的概率最高,并发度最低。
  有 2 种模式:表共享读锁和表独占写锁。
  支持并发插入:支持查询和插入操作并发进行(在表尾并发插入)。
  锁调度机制:写锁优先。一个进程请求某个 MyISAM 表的读锁,同时另一个进程也请求同一表的写锁,MySQL 如何处理呢?答案是写进程先获得锁。

我们在数据库中模拟并发访问的情况,我们在运行一个查询语句的时候,MySQL会自动给表加上读锁,此时新的update、insert、delete操作将会被锁住。同理,当我们对表数据进行增删改的时候,MySQL会自动给表加上一个表级别的写锁。同时我们可以使用语句手动给数据库加锁。

lock tables slowquerytest read;--给表上读锁,模拟比较慢的读操作
unlock tables;--给表解锁

然后我们做一个查询操作看看(下图),可以发现当读锁未被释放的时候,是无法进行写操作的

数据库常见面试题——锁_第1张图片

读锁又称为共享锁,因为在进行范围查询的时候,我们依然可以对表进行查询,不受读锁的控制。

同理,当上了写锁,必须要等写锁释放了,才能进行读操作。此外写锁不共享!即在写锁进行时,在进行写操作,也需要等待写锁释放。写锁又称排它锁

当然我们也可以给读语句上排它锁,即给读语句后面加上for update语句。

explain select name from slowquerytest for update

InnoDB默认用的行级锁

行级锁的含义: 仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。

特点:开销大,加锁慢;会出现死锁;锁定粒度最小(等级最低),发生锁冲突的概率最低,并发度也最高。

此外:InnoDB在SQL语句没有用到索引的时候用的是表级锁

共享锁和排它锁的兼容性

只有在两条查询语句都使用共享锁的时候,两条SQL语句才能兼容(同时查询)

MyIsAM适合的场景

  • 频繁执行全表count语句
  • 对数据进行增删改的频率不高,査询非常频繁
  • 没有事务

InnoDB适合的场景

  • 数据增删改查都相当频繁
  • 可靠性要求比较高,要求支持事务

数据库锁的分类

  • 按锁的粒度划分:可分为表级锁、行级锁、页级锁
  • 按锁级别划分:可分为共享锁、排它锁
  • 按加锁方式划分:可分为自动锁、显式锁
  • 按操作划分:可分为DML锁、DDL锁
  • 按使用方式划分:可分为乐观锁、悲观锁
     

乐观锁和悲观锁

参考:Java常见面试题汇总-----------数据库(数据库锁、数据库存储过程)

悲观锁的含义:锁如其名,他对世界是悲观的,他认为别人访问正在改变的数据的概率是很高的,所以从数据开始更改时就将数据锁住,直到更改完成才释放
  一个典型的倚赖数据库的悲观锁调用:

select * from account where name="灾难始终慢我一步" for update

for update 是将读语句变成排它锁的重要方法。
  这条 sql 语句锁定了 account 表中所有符合检索条件(name=”灾难始终慢我一步”)的记录。 本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。该语句用来锁定特定的行(如果有 where 子句,就是满足 where条件的那些行)。当这些行被锁定后,其他会话可以选择这些行,但不能更改或删除这些行,直到该语句的事务被 commit 语句或 rollback 语句结束为止。需要注意的是,select …for update 要放到 mysql的事务中,即 begin 和commit 中,否则不起作用。
  缺点:悲观锁可能会造成加锁的时间很长,并发性不好,特别是长事务,影响系统的整体性能。
  悲观锁的实现方式:
  悲观锁,也是基于数据库的锁机制实现。 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

  乐观锁的含义:他对世界比较乐观,认为别人访问正在改变的数据的概率是很低的,所以直到修改完成,准备提交所做的修改到数据库的时候才会将数据锁住, 当你读取以及改变该对象时并不加锁,完成更改后释放。乐观锁不能解决脏读的问题。
  乐观锁加锁的时间要比悲观锁短,大大提升了大并发量下的系统整体性能表现。
  乐观锁的实现方式:
  1、大多是基于数据版本(Version )记录机制实现, 需要为每一行数据增加一个版本标识(也就是每一行数据多一个字段 version),每次更新数据都要更新对应的版本号+1。
  工作原理:读出数据时,将此版本号一同读出,之后更新时,对此版本号加一。 此时,将提交数据的版本信息与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据,不得不重新读取该对象并作出更改。
  假设数据库中帐户信息表中有一个version 字段,当前值为 1;而当前帐户余额字段(balance)为 $100。
  1)、操作员 A 此时将其读出(version=1),并从其帐户余额中扣除 $50($100-$50)。
  2)、在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1),并从其帐户余额中扣除 $20($100-$20)。
  3) 操作员 A 完成了修改工作,将数据版本号加一(version=2),连同帐户扣除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2。
  4) 操作员 B 完成了操作,也将版本号加一(version=2)试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为2,数据库记录当前版本也为 2 ,不满足“提交版本必须大于记录当前版本才能执行更新“的乐观锁策略,因此,操作员 B 的提交被驳回。
  这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
  从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A 和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。

  2、使用时间戳来实现
  同样是在需要乐观锁控制的 table 中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp),和上面的 version 类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则 OK,否则就是版本冲突。
  悲观锁和乐观锁的适用场景:
  如果并发量不大,可以使用悲观锁解决并发问题;但如果系统的并发量非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法。

 

你可能感兴趣的:(java)