您的位置:首页 > 数据库

数据库的锁机制

2018-03-07 18:20 106 查看
数据库的锁机制

前言在上一篇文章《数据库的事务&事务的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为2update t_goods set status=2;//4.提交事务&释放锁commit;上面的查询语句中,我们使用了select…for update的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行,从而保证在操作过程中id为1的那条数据不会被其它事务修改。另外,我们在前面说过,对于InnoDB存储引擎,只有where条件中存在索引项时,才会加行级锁,否则会锁住整张表,这点在日常开发时要注意,否则很容易会锁住整张表。最后,数据库的事务和锁机制很深,涉及到的点很多,希望我们可以在以后的学习中不断深入。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: