[toc]
MySQL笔记11-锁
锁的介绍
当多个事务并发执行数据库操作时会遇到以下4种问题:脏写,脏读,不可重复读,幻读。
而为了解决这4种问题,mysql设计了4种隔离级别:读未提交,读已提交,可重复读,串行化。
这4种隔离级别通过锁来实现。锁的机制就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等房客使用完毕才可以再次使用房间。
隔离级别与锁的关系
mysql的4种隔离级别:读未提交,读已提交,可重复读,串行化。就是通过锁来实现。
在读未提交(Read Uncommitted)级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
在读已提交(Read Committed)级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
在可重复读(Repeatable Read)级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
串行化(SERIALIZABLE)是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。
锁的分类
- 根据读写分类:共享锁(读锁,S锁),排他锁(写锁,X锁)
- 根据颗粒度分类:行级锁、表级锁和页级锁。
- 根据锁的状态分类:意向共享锁,意向排他锁。
锁的读写分类
共享锁(读锁,S锁)
一个数据可以同时添加多个共享锁。获得共享锁的事务只能读取数据,不能修改数据。
当一个事务为数据加上共享锁后,其他事务只能对该数据加共享锁而不能对数据加写锁,直到所有的共享锁释放之后其他事务才能对其加写锁。
共享锁的目的主要是为了支持并发的读取数据,并且读取数据的时候不支持对数据进行任何修改。
排他锁(写锁,X锁)
一个数据只能添加一个排他锁(写锁),排他锁和其他的排他锁,共享锁都相斥。
当一个事务为数据加上写锁后,其他事务将不能再为数据加任何锁,直到该锁释放之后其他事务才能对数据进行加锁。
例如事务T对数据A加上排他锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到事务T释放A上的锁。这保证了其他事务在事务T释放A上的排他锁之前不能再读取和修改A。排它锁会阻塞所有的排它锁和共享锁
排他锁的目的是在数据修改的时候,不允许其他人同时修改,也不允许其他人读取,避免出现脏数据和脏读的问题。
锁的颗粒度分类
在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁、表级锁和页级锁。
MyISAM和InnoDB存储引擎使用的锁:
- MyISAM采用表级锁,不支持行级锁。
- InnoDB支持表级锁和行级锁,默认设置为为行级锁。
行级锁
行级锁:指锁住的对象是表中的某一行或多行记录,其他事务不能访问被锁住的记录,但可以访问未被锁住的记录。
其特点是:开销大,加锁慢;会出现死锁;颗粒度最小,发生锁冲突的概率最低,并发度也最高。
页级锁
页级锁是介于行级锁和表级锁中间的一种锁。页级锁一次锁定一页的数据(相邻的一组记录)。
其特点是:开销和加锁时间界于表级锁和行级锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
表级锁
表级锁:指锁住的对象是整个表。当其他事务访问该表的时候,必须等当前事务释放了表级锁,才能进行对表的访问。
其特点是:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
锁的状态分类
InnoDB存储引擎支持多粒度锁定,即允许数据同时存在行级锁和表级锁。为了支持在不同粒度上进行加锁操作。InnoDB存储引擎支持一种额外的锁方式,称之为意向锁。意向锁是将锁定的对象分为多个层次,意向锁意味着事务可以在更细粒度上进行加锁。
意向锁分为:意向共享锁,意向排他锁。
意向共享锁
意向共享锁:事务想要获取到一张表中某几行的共享锁。即若事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
意向排他锁
意向排他锁:事务想要获取到一张表中某几行的排他锁。即若事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
各种锁的兼容性
锁的算法
暂无
死锁
死锁:是指两个或两个以上的进程在执行过程中,因互相争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
下图演示了死锁的一种经典的情况,即A等待B、B等待A,这种死锁问题被称为AB-BA死锁。
死锁的解决方法
方法1:等待事务超时。当其中一个事务等待时间超过阈值时,该事务会进行回滚,另一个等待的事务就能继续进行。
方法2:InnoDB存储引擎使用wait-for graph(等待图)的方式来主动进行死锁检测。
wait-for graph(等待图)方式需要数据库保存两种信息:锁的信息链表,事务等待链表
通过这两种链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁。这是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁。
常见的预防死锁的处理
- 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
两种锁的设计思想:悲观锁和乐观锁
无论是悲观锁还是乐观锁,它们本质上不是数据库中具体的锁,而是两种锁的设计思想。
乐观锁
乐观锁(Optimistic Concurrency Control,乐观并发控制)是一种并发控制的方法。
乐观锁是假设在并发环境中,外界对数据的操作是不会造成冲突,所以一般不会主动去加锁(所以乐观锁不是一把锁),而是在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回冲突信息,让用户决定如何去做下一步,比如说重试,直至成功为止;
乐观锁,不是利用数据库本身的锁去实现的,是利用某种实现逻辑去实现做到乐观锁的设计思想。
乐观锁实现方式: 一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
悲观锁
悲观锁(Pessimistic Concurrency Control,悲观并发控制)是一种并发控制的方法。
悲观锁指的是采用一种持悲观的态度,默认数据被外界访问时,必然会产生冲突,所以在数据处理的整个过程中都采用加锁的状态,保证同一时间,只有一个线程可以访问到数据,实现数据的排他性;
通常数据库的悲观锁是利用数据库本身提供的锁机制去实现的。例如共享锁和排它锁就是悲观锁设计思想的两种实现方式,它俩都属于悲观锁的范畴。