数据库的锁机制

数据库的锁机制
 
 
前言
在上一篇文章 《数据库的事务&事务的ACID特性&四种隔离级别》中说过,根据所使用的存储引擎的不同,对应表具有不同的存储机制、索引技巧以及 锁机制等。所以,我们要知道, 不同存储引擎的锁机制是不一样的,讨论数据库的锁机制之前,我们首先要明确讨论的是哪种存储引擎的锁机制。
如果对数据库事务的存储引擎还不清楚的读者,可以先看下上篇文章 《数据库的事务&事务的ACID特性&四种隔离级别》,里面有介绍到MySQL的存储引擎,可以先了解一下。
 
一、锁
定义:锁是计算机协调多个进程或线程并发访问同一资源的机制。所以,锁是一种机制,是在多个事务并发访问同一资源时起协调作用的。
数据库中的数据也是一种资源,当多个事务同时去修改同一条数据时,就有可能导致数据不一致的问题。因此我们需要一种机制来将事务对数据的访问顺序化,以保证数据库数据的一致性,这种机制就是锁机制。
简单来说,就是事务A想要操作某数据前,必须先获得该数据对应的锁,获得锁之后事务A就可以去操作这条数据,此时如果有另一个事务B也想操作该条数据,那么它就得等待,等待事务A执行完释放掉锁,B获取到锁才能去操作该条数据。
上一章节中学习了事务的ACID特性(原子性、一致性、隔离性、持久性),其中事务的隔离性就是通过锁机制来实现的。
二、行锁&表锁&页锁
MySQL各存储引擎的锁机制是不一样的。按照锁的粒度来说,MySQL中各存储引擎使用了三种类型的锁机制:行级锁、表级锁、页级锁。
1、行级锁:开销大,加锁慢; 可能出现死锁;锁定粒度最小,发生锁冲突的概率最低, 并发度也最高
2、表级锁:开销小,加锁快; 不会出现死锁(如MyISAM引擎是不会出现死锁的);锁定粒度大,发生锁冲突的概率最高, 并发度最低,可能导致超时
3、页级锁:介于行锁和表锁之间,页级锁不在本文的讨论范围;
 
  • MySQL常用的存储引擎对应的锁机制
MyISAM采用表级锁;
InnoDB支持行级锁和表级锁,默认采用行级锁;
BDB支持页级锁和表级锁,默认采用页级锁;(实际上在5.1版本后,MySQL就不再支持BDB存储引擎了,因为BDB被Oracle收购了,此处了解一下支持页级锁的存储引擎有BDB即可)
 
  • MySQL使用最频繁的存储引擎——InnoDB的锁机制
上面说过,InnoDB引擎既支持行锁,也支持表锁。那么什么情况会锁住某些行(行锁),什么情况下会锁住整张表(表锁)呢?
首先我们要清楚一点, InnoDB的行锁是通过给记录的索引项加锁来实现的,所以, 只有将索引项作为查询条件去检索数据时,才会使用行锁,否则,InnoDB将使用表锁。显然,行锁都是基于索引的,如果一条SQL语句用不到索引查询条件是不会使用行锁的,会使用表锁。在实际开发中,一定要注意InnoDB的这一特性,如果where条件中没有索引列,就会给整个表加上锁,所有访问这个表的数据库连接都要等待,可能会导致超时。
另外,因为InnoDB行锁是针对索引项加锁而不是记录,所以如果多个事务访问不同的记录,但是使用相同的索引项作为查询条件,还是会出现锁冲突的。
三、共享锁&排他锁
1、共享锁/读锁/S锁
共享锁又称为读锁或S锁,是在进行读取操作时创建的锁,为了统一表述,后文中统一称为S锁。
对于加了S锁的数据,允许其他事务并发进行读取操作,但是不允许包括自身在内的任何事务对数据进行修改操作。
如,事务A对数据D加了S锁后,其他任何事务只能对数据D加S锁,不能加X锁。即允许其他事物进行读操作,但是不允许事务A在内的任何事务对数据进行修改。 即"我读的时候,你也能读,但我们大家都不能写"
2、排他锁/写锁/X锁
排他锁又称为写锁或X锁,是在进行写操作(增、删、改)时创建的锁,为了统一表述,后文中统一称为X锁。
事务A对数据D加上X锁之后,其他事务对数据D既不能加S锁,也不能加X锁;获得排他锁的事务既能读数据,也能写数据(增、删、改)。 即"我在"读写"的时候,你既不能读,也不能写"

对于写操作(insert、update、delete),InnoDB会自动给涉及的数据加X锁;而对于select操作,InnoDB不会对数据加任何锁,如果需要,事务可以给select操作显式的加共享锁或排他锁。
共享锁:select ... lock in share mode;
在select语句后面加lock in share mode,MySQL会对查询结果中的每行都加S锁。如:select * from t_course where course_type='course_type1' lock in share mode;此时其他事务可以对锁定的数据加S锁,但不能加X锁,否则会被阻塞。
排他锁:select ... for update;
在select语句后面加for update,MySQL会对查询结果中的每行都加X锁。如:
select * from t_course where course_type='course_type2' for update;此时其他事务既不能对锁定的数据加S锁,也不能加X锁。

3、意向共享锁&意向排他锁
InnoDB还有两个锁:
意向共享锁(IS):表示事务准备给数据行加共享锁,也就是说事务在给一个数据行加共享锁前必须先获得该表的IS锁。
意向排他锁(IX):类似的,表示事务准备给数据行加排他锁,也就是说事务在给一个数据行加排他锁前必须先获得该表的IX锁。
意向锁是InnoDB自动加的,不需要用户干预,我们在开发中不需要额外关注。

※ 以上四种锁互斥/兼容关系(√表示兼容;×表示互斥)
 S(共享锁)IS(意向共享锁)X(排他锁)IX(意向排他锁)
S(共享锁)××
IS(意向共享锁)×
X(排他锁)××××
IX(意向排他锁)××
说明:当一个事务请求的锁模式与当前的锁兼容时,InnoDB就将请求的锁授予该事务,反之该事务将阻塞等待。
4、间隙锁
当我们用"范围条件"而不是"等于条件"来查询数据,并请求S锁或X锁时,InnoDB会给符合条件的数据行加锁(实质是给记录的索引项加锁);对于在条件范围内但表中并不存在的记录,叫做"间隙",InnoDB也会给这部分间隙加锁,这种锁机制就是间隙锁。间隙锁的出现是为了防止幻读。
例如,t_user表中有10条记录,id依次为:1、2、3、4、5、6、7、8、9、10,执行如下SQL:
select * from t_user where id>9 for update;
该SQL是一个范围检索,InnoDB不仅会对符合条件的id为10的记录加锁,也会对id大于10(这些记录并不存在)的"间隙"加锁。这样一来,就防止了其他事务插入id>10的新记录,从而防止了幻读。
显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
另外,InnoDB除了在通过"范围条件"加锁时使用间隙锁外,还有一个出现场景: 使用"相等条件"请求给一个不存在的记录加锁,InnoDB也会使用间隙锁! 如果作为相等条件查询的记录在数据库中存在, 那么这个时候产生的是普通行锁;如果这条记录不存在,问题就来了,数据库会扫描索引,发现这个记录不存在,然后数据库会先向左扫描,扫到第一个比给定查询参数小的值,再向右扫描到第一个比给定查询参数大的值,然后以此为界,构建一个区间, 锁住整个区间内的数据, 一个特别容易出现死锁的间隙锁诞生了。
四、乐观锁&悲观锁
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段,是一种思想机制。 不要把乐观锁和悲观锁狭义的理解为数据库中的概念,更不要将其与数据库提供的锁机制(行锁、表锁、共享锁、排他锁)混为一谈。
1、乐观锁
操作数据库时,想法很乐观,认为不会出现多事务并发导致的数据不一致问题,所以每次操作总是不加锁,而是在完成一系列的操作准备提交事务时才去判断,在提交事务之前,检查在该事务读取数据后,有没有其他事务又修改了数据,如果有其他事务修改过该数据,那么当前正在提交的事务会进行回滚。
乐观锁在对数据库进行操作时不会使用任何数据库提供的锁机制,乐观锁的一般实现方式是"记录数据版本"。
具体操作是为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 "version" 字段来实现,数据每更新一次,版本号+1。当读取数据时,将版本号一起读出,当我们提交更新的时候,判断对应数据的版本信息与之前读取到的版本是否一致,如果一致,则予以更新,否则说明在此过程中有其他事务更新过该数据,当前事务回滚。
实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。
像MyBatis、Hibernate等ORM框架都能够实现或者说实现了乐观锁,有兴趣可以专门去学习了解。
优点:乐观锁不会产生死锁。
2、悲观锁
悲观锁的特点是先获取锁,再进行业务操作,在整个操作过程中保持数据处于锁定状态,要确保先获取到锁再进行业务操作才有安全感,比较悲观。悲观锁的实现,往往依靠数据库本身的锁机制,通常使用select ... for update来实现。select ... for update获取的锁会在当前事务结束时自动释放,因此必须在事务中使用。
悲观锁的优点是为数据的安全性提供了保证,但缺点是有可能会发生死锁、降低了事务的并行性。
要使用悲观锁,必须关闭MySQL数据库的自动提交属性(设置autocommit=0),防止操作未执行完成事务就已自动提交释放掉锁。
#0.禁止事务自动提交
set autocommit=0;
//0.开始事务
begin;
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务&释放锁
commit;
上面的查询语句中,我们使用了select…for update的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行,从而保证在操作过程中id为1的那条数据不会被其它事务修改。
另外,我们在前面说过,对于InnoDB存储引擎,只有where条件中存在索引项时,才会加行级锁,否则会锁住整张表,这点在日常开发时要注意,否则很容易会锁住整张表。
最后,数据库的事务和锁机制很深,涉及到的点很多,希望我们可以在以后的学习中不断深入。
 
 
感兴趣的小伙伴可以关注一下博主的公众号,1W+技术人的选择,致力于原创技术干货,包含Redis、RabbitMQ、Kafka、SpringBoot、SpringCloud、ELK等热门技术的学习&资料。
 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值