锁
锁是计算机协调多个进程或线程并发访问某一资源的机制。【并发控制】
1、读-读:不会引起问题
2、写-写:多个未提交事务相继对一条记录做改动时,需要让它们排队执行,锁实现排队。事务只有获得了锁才能做修改,事务完成提交后释放锁,其它事务去获得锁后才能对数据库进行操作。
事务的锁结构:trx:事务 代表这个锁结构是哪个事务生成的
is_waiting:false 代表当前事务是否在等待
3、读-写/写-读:一个事务读取,一个进行改动操作。可能发生脏读、不可重复读、幻读的问题。
解决:方案一:读操作采用MVCC(多版本并发控制),写操作进行加锁
所谓MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)。查询语句只能读到生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或之后才开启的事务所作的更改无法看到。而写操作针对的是最新版本的记录。读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。读写操作不冲突,性能更高。
方案二:读操作和写操作都加锁,读写操作彼此需要排队,影响性能。
锁的类别:
一、对数据的操作类型分类:读锁/共享锁、写锁/排他锁
读锁:也称共享锁、S锁。针对同一份数据,多个事务的读操作可以同时进行而不会相互影响,相互不阻塞
写锁:也称排他锁,X锁。当前写操作没有完成前,它会阻断其它写锁和读锁,确保在给定的时间里,只有一个事务能执行写入。
加了S锁还允许其它事务加S锁,但不能加X锁。加了X锁不允许其它事务加S锁、X锁会阻塞等待。
二、锁粒角度划分:表级锁、行级锁、页级锁
为了尽可能提高数据库的高并发,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗费资源的事(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度”的概念
1、表锁:该锁会锁定整张表。开销最小,粒度比较大。避免死锁问题,但是出现资源争用的概率也最高,导致并发性下降。
(1)表级别的S锁、X锁
(2)意向锁:协调行锁和表锁的关系,意向锁是一种不与行级锁冲突的表级锁,表明某个事务正在某些行持有了锁或该事务准备去持有锁。
意向锁分两种:意向共享锁、意向排他锁。
意向锁解决的问题:事务T1和T2,其中T2试图在该表级别上应用共享锁或排他锁,如果没有意向锁的存在,那么T2就需要去检查各个页或行是否存在锁;如果存在意向锁,那么此时就会受到T1控制的表级别意向锁的阻塞。T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。简单来说就是给更大一级别的空间示意里面已经上过锁。
数据表的场景中:如果我们给某一行加上了排他锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排他锁了。意向锁告诉其它事务已经有人锁定了表中的某些记录。
意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求
注:各意向共享锁、意向排他锁都兼容。(行锁)
(3)自增锁(AUTO-INC锁):为表的某个列添加AUTO_INCREMENT属性,插入时不需要为其赋值,系统会自动为它赋上递增的值。表级锁。
(4)元数据锁(MDL锁):当对一个表做增删改查操作的时候,加MDL锁;当要对表做结构变更操作的时候,加MDL写锁。不需要显式使用,在访问一个表的时候会被自动加上。
2、行锁:也称记录锁,就是锁住某一行(某条记录row),需要注意的是,MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。
优点:锁定力度小,发生锁冲突概率低,可以实现的并发度高
缺点:对于锁的开销比较大,加锁会比较慢,容易出现死锁情况
(1)记录锁:仅仅把一条记录锁上,对周围数据没有影响。S型记录锁和X型记录锁。
(2)间隙锁:MySQL在repeatable read隔离级别下是可以解决幻读问题的,可以使用MVCC方案解决,也可以采用加锁方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。InnoDB提出了一种称之为Gap Locks的锁。如在id值为8的那条记录加一个gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新纪录,若有事务要在记录8前面插入新记录,会被阻塞插入,直至gap锁被释放。gap锁的提出仅仅是为了防止插入幻影记录而提出的。对一条记录加gap锁(不论是共享gap锁还是独占gap锁),并不会限制其它事务对这条记录加记录锁或者继续加gap锁。
可能导致死锁。如事务1等待事务2释放id=2的行锁,事务2在等待事务1释放id=1的行锁。进入死锁状态。处理策略:等待超时,或者主动回滚死锁链中的某一个事务(将持有最少行级排他锁的食物进行回滚),让其它事务得以执行。
(3)临键锁:既想锁住某条记录,又想阻止其它事务在该记录前面的间隙插入记录,next-key锁。next-key锁本质就是一个记录锁和一个gap锁的合体,它既能保护该条记录,又能组织别的事务将新纪录插入保护记录前边的间隔。
(4)插入意向锁:如果插入位置被别的事务加了gap锁,那插入操作需要等待,直到拥有gap锁的那个事务提交。但是innoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想要在某个间隙插入新纪录。该锁用以表达插入意向,当多个事务在同一区间插入位置不同的多条数据时,事务之间不需要互相等待。因为数据行不冲突,比如在4和7之间插入5和6两条记录。
3、页锁
页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。并发度一般。
每个层级的锁数量是有限的,因为锁会占用内存空间,锁空间的大小是有限制的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁代替多个更小粒度的锁。比如行锁升级为表锁,好处是占用锁空间降低了,坏处是并发度下降。
三、对待锁的态度划分:悲观锁、乐观锁
悲观锁:线程拿到数据就上锁,其它线程只能阻塞直到锁被释放。【秒杀避免超卖】
select *** for update是MySQL中的悲观锁。语句执行过程中所有扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则会把整个表锁住。
并发性差
乐观锁:读数据不上锁,修改数据会查看数据是否已经被其它事务修改了,若被修改过则不进行操作。【不通过数据库锁机制实现】
版本号机制,时间戳机制,都是将当前数据的版本号或时间戳与更新之前取得的版本号或时间戳进行比较,如果两个一致则更新成功,否则就是版本冲突。
四、加锁方式:隐式锁、显示锁
隐式锁:INSERT时如果即将插入的间隙已经被其它事务加上了gap锁,那么本次INSERT操作会阻塞,并且当前事务会在该间隙上加一个插入意向锁。
五、全局锁和死锁
全局锁就是对整个数据库实例加锁,整个数据库只能读。使用场景:全库逻辑备份。
死锁:两个或多个事务在同一资源上互相占用,并请求锁定对方占用的资源,并且双方都不释放自己的锁。除非外力帮助,否则不能向前推进。
产生死锁的必要条件:
1、两个或两个以上事务
2、每个事务都已经持有锁并申请新的锁
3、锁资源同时只能被同一个事务持有或不兼容
4、事务之间因为持有锁和申请锁导致彼此循环等待。
处理死锁:
1、等待,直到超时
2、使用死锁检测进行死锁处理
事务等待链表和锁的信息链表。制作以事务为顶点、锁为边的等待图,若存在环则有死锁出现。若有环,则回滚undo量最小的事务,让其它事务继续执行。
缺点:操作时间复杂度高。
如何避免死锁:
合理涉及索引,使业务SQL尽可能通过索引定位更少的行,减少锁竞争
调整业务逻辑SQL执行顺序,避免update/delete长时间持有锁的SQL在事务前面
避免大事务,尽量将大事务拆成多个小事务处理,小事务缩短锁定资源的时间,发生锁冲突的几率也更小。
在并发比较高的系统中,不要显示加锁。
降低隔离级别