zoukankan      html  css  js  c++  java
  • mysql(3):锁和事务

    MySQL锁的介绍

    锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。

    表级锁

    例如MyISAM引擎,其锁是表锁设计。并发情况下的读没有问题,但是并发插入时的性能要差一些。

    直接锁定整张表,在你锁定期间,其它进程无法对该表进行写操作。如果你是写锁,则其它进程读也不允许。

    两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。

    对WRITE,MySQL使用的表锁定方法原理如下:
    如果在表上没有锁,在它上面放一个写锁。否则,把锁定请求放在写锁定队列中。

    对READ,MySQL使用的表锁定方法原理如下:
    如果在表上没有写锁定,把一个读锁定放在它上面。否则,把锁请求放在读锁定队列中。

    页级锁

    Microsoft SQL Server 2005版本之前其实都是页锁的,相对于MyISAM引擎来说,并发性能有所提高,但是存在热点数据页的并发问题。2005版本开始支持乐观并发和悲观并发,在乐观并发下开始支持行级锁。

    表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

    行级锁(InnoDB)

    行锁

    Microsoft SQL Server 2005版本行级锁的实现与InnoDB引擎的实现不同。在Microsoft SQL Server下,会有锁升级,即行锁会升级到表锁。

    InnoDB引擎锁的实现和Oracle十分相似,提供一致性的非锁定读行级锁支持。

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

    两种模式:

    1)共享锁:允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。

    select * from table_name where ... lock in share mode
    

    2)排他锁:允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

    select * from table_name where ... for update
    

    InnoDB行锁是通过给索引项加锁来实现的,只有通过索引条件检索数据才使用行级锁,否则将使用表锁!

    元数据锁(MDL, metadata lock)

    用于解决或者保证DDL操作与DML操作之间的一致性。

    DML(Data Manipulation Language)数据操纵语言:

    适用范围:对数据库中的数据进行一些简单操作,如insert, delete, update, select等。

    DDL(Data Definition Language)数据定义语言:

    适用范围:对数据库中的某些对象(例如,database,table)进行管理,如create, alter和drop。

    例如:针对同一张表执行,T1-DML, T2-DDL, T1-DML。若没有MDL锁的保护,则事务2可以直接执行DDL操作,并且导致事务1出错。加入MDL锁后,由于事务1进行DML操作,获得了MDL锁,锁的模式为SHARED_READ,事务2要执行DDL,则需获得EXCLUSIVE锁,两者互斥,所以事务2需要等待。

    MDL锁是在Server中实现。另外,MDL锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁,这是InnoDB存储引擎层不能直接实现的锁。

    总结

    1) 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
    2) 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
    3) 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

    加锁分析

    链接

    简单SQL的加锁分析

    SQL1:

    select * from t1 where id = 10;(RC、RR不加锁。因为MySQL是使用多版本并发控制的,读不加锁。)
    

    SQL2:

    delete from t1 where id = 10;(需根据多种情况进行分析)
    
    RC(Read Committed)级别下的主键索引、唯一索引、非唯一索引、无索引分析
    1. id主键 + RC:id主键,只需要将主键上id = 10的记录加上X锁即可。
    2. id唯一索引 + RC:id不是主键,id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name=’d’,id=10]的记录。
    3. id非唯一索引 + RC:id列上有非唯一索引,对应的所有满足SQL查询条件的记录都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。
    4. id无索引 + RC:没法通过索引进行过滤,那么只能走全表扫描做过滤。聚簇索引上所有的记录,都被加上了X锁。这是由于MySQL的实现决定的。如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行过滤。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。
    RR(Repeatable Read)级别下的主键索引、唯一索引、非唯一索引、无索引分析
    1. id主键 + RR:同id主键 + RC一致
    2. id唯一索引 + RR:同id唯一索引 + RC一致
    3. id非唯一索引 + RR:为了保证两次当前读返回一致的记录,需要保证在第一次当前读与第二次当前读之间其他的事务不会插入新的满足条件的记录并提交。(上面两种情况都是有唯一性的,所以肯定不会不一致)为了实现这个功能,GAP锁应运而生。不仅将满足条件的记录锁上 (X锁),同时还增加n把GAP锁,将可能插入满足条件记录的n个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。步骤:首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录,此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。
    4. id无索引 + RR:如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。
    Serializable隔离级别

    SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。

    复杂SQL的加锁分析

    where条件如何拆分(上一节写过了)
    1. index key
    2. index filter
    3. table filter

    Repeatable Read隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。Index Key确定的范围,需要加上GAP锁;Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则满足Index Filter的记录需要X锁;Table Filter过滤条件,无论是否满足,都需要加X锁。

    死锁原理分析

    两个session的两条SQL产生死锁分析

    每个事务执行两条SQL,分别持有了一把锁,然后加另一把锁,产生死锁

    两个session的一条SQL产生死锁分析

    虽然每个Session都只有一条语句,仍旧会产生死锁。

    针对Session 1,从name索引出发,读到的[hdc, 1],[hdc, 6]均满足条件,不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X锁,加锁顺序为先[1, hdc,100],后[6, hdc,10]。

    而Session 2,从pubtime索引出发,[10, 6], [100, 1]均满足过滤条件,同样也会加聚簇索引上的记录X锁,加锁顺序为[6, hdc,10],后[1, hdc,100]。跟Session 1的加锁顺序正好相反,如果两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。

    结论:死锁的发生与否,并不在于事务中有多少条SQL语句,关键在于两个或以上的Session加锁的顺序不一致。

    事务流程分析

    回滚流程undo

    undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。

    undo是一种逻辑日志,有两个作用:

    • 用于事务的回滚:undo日志用于事务的回滚操作进而保障了事务的原子性。
    • MVCC

    undo log的写入时机:

    • DML操作修改聚簇索引前,记录undo日志
    • 二级索引记录的修改,不记录undo日志

    需要注意的是,undo页面的修改,同样需要记录redo日志。

    在InnoDB存储引擎中,undo log分为:

    • insert undo log:在insert操作中产生的undo log;以在事务提交后直接删除,不需要进行purge操作。
    • update undo log:对delete 和update操作产生的undo log;可能需要提供MVCC机制,因此不能再事务提交时就进行删除。该undo log提交时放入undo log链表,等待purge线程进行最后的删除。

    purge线程两个主要作用是:清理undo页和清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识Delete_Bit,而不删除记录。是一种"假删除",只是做了个标记,真正的删除工作需要后台purge线程去完成。

    重做流程redo

    重做日志(redo log),用于数据库的崩溃恢复,保证事务的持久性,即事务ACID中的D。可以分为以下两种类型:

    • 物理Redo日志
    • 逻辑Redo日志

    在InnoDB存储引擎中,大部分情况下 Redo是物理日志,记录的是数据页的物理变化。而逻辑Redo日志,不是记录页面的实际修改,而是记录修改页面的一类操作,比如新建数据页时,需要记录逻辑日志。逻辑Redo日志涉及更加底层的内容。

    Redo log可以简单分为以下两个部分:

    • 一是重做日志缓冲 (redo log buffer),是易失的,在内存中
    • 二是重做日志文件 (redo log file),是持久的,保存在磁盘中

    在数据页修改完成之后,在脏页刷出磁盘之前,写入redo日志。

    Redo的整体流程

    • 第一步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
    • 第二步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
    • 第三步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
    • 第四步:定期将内存中修改的数据刷新到磁盘中

    总结

    在InnoDB内存中,一般的顺序如下:

    • 写undo的redo
    • 写undo
    • 修改数据页
    • 写Redo

    InnoDB架构分析

    架构图分析

    内存结构分析

    Buffer Pool

    用于在访问时缓存表和索引数据。允许直接在内存中处理经常使用的数据。(在专用MySQL服务器上,通常会将最多80%的物理内存分配给Buffer pool)

    为了提高高容量读取操作的效率,buffer pool被分成可以容纳多行的page。

    Buffer Pool LRU 算法
    为了提高buffer管理的效率,将buffer pool以链表形式进行管理。(链接的页面列表)

    采用中点插入法:当插入一个新page时,移除表尾最近最少使用的page,在中点插入新page。

    这个中点将链表分为两部分:

    • 靠近表头的一部分,为young区,这里的page是最近使用的节点

    • 靠近表尾的一部分,为old区,这里的page是最近少使用的

    中点不是链表的中间点,而是old区的表头节点,即old区与young区的相邻的那个节点。通常取链表的3/8为old区。

    data page
    index page
    Change Buffer / insert buffer

    对不在buffer pool中的辅助索引页的更改将缓存在change buffer中。等辅助索引页被读入buffer pool中时,更改会定期合并。purge操作定期将更新的索引页写到磁盘中。

    在内存中,change buffer会占用buffer pool的空间;在物理磁盘上,change buffer是system tablespace的一部分,所以对索引的修改在数据库重启后仍然存在change buffer中。

    它并不是缓存的一部分,而是物理页。对于非聚集索引的插入或更新操作,不是每一次直接插入索引页。而是先判断插入的非聚集索引页是否在缓冲池中。如果在,则直接插入;如果不在,则先放入插入缓冲区中,然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作。

    Adaptive Hash Index自适应哈希索引

    Adaptive Hash Index使InnoDB能够在系统上执行更多类似于内存数据库的工作,并为缓冲池提供适当的工作负载和足够的内存,而不会牺牲事务功能或可靠性。

    AHI是通过缓冲池的B+树构造而来,因此建立的速度很快。InnoDB引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。

    限制:

    • 只能用于等值比较,例如=, <=>,in
    • 无法用于排序
    • 有冲突可能
    • MySQL自动管理,人为无法干预
    lock info
    data dictionary
    Redo log Buffer

    默认大小为16MB。

    大型日志缓冲区使大型事务能够运行,而无需在事务提交之前将重做日志写入磁盘。

    redo log在下列三种情况会将redo log buffer中的内容刷新到外部磁盘的redo log file中:

    • Master Thread 每秒将redo log buffer刷新到 redo log file中
    • 每个事务提交时会将redo log buffer刷新到 redo log file中
    • 当redo log buffer剩余空间小于1/2时,redo log buffer将刷新到 redo log file中
    double write

    内存中的doublewrite,大小为2MB。

    磁盘文件分析

    系统表空间
    InnoDB Data Dictionary

    InnoDB数据字典由内部系统表组成,其中包含用于跟踪对象的元数据。 元数据实际位于InnoDB系统表空间中。 由于历史原因,数据字典元数据在某种程度上与存储在InnoDB表元数据文件.frm文件中的信息重叠。

    Doublewrite Buffer

    物理磁盘上共享表空间中连续128个页,即2个区(extent),大小同样为2MB,位于系统表空间中。

    只有将页面刷新并写入双写缓冲区后,InnoDB才会将页面写入其正确的位置。如果在页面写入过程中存在操作系统、存储子系统或mysqld进程崩溃,InnoDB稍后可以在崩溃恢复期间从doublewrite缓冲区中找到该页面的良好副本。

    在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy()函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync()函数(不需要两倍的I/O开销或两倍的I/O操作),同步磁盘,避免缓冲写带来的问题。在这个过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不是很大。在完成doublewrite页的写入后,再将doublewrite buffer的页写入各个表空间文件中,此时的写入则是离散的。

    用户表空间文件

    如果设置了参数innodb_file_per_table,则用户可以将每个基于InnoDB存储引擎的表产生一个独立的用户表空间。通过这种方式,用户不用将所有数据都存放于默认的系统表空间中,但是用户表空只存储该表的数据、索引和插入缓冲BITMAP等信息,其余信息还是存放在默认的表空间中。

    重做日志文件和归纳文件
    重做日志文件(redo log file)

    前面已经说过,是持久的,保存在磁盘中。

    重做日志的落盘机制:InnoDB对于数据文件和日志文件的刷盘遵守WAL(Write ahead redo log) 和Force-log-at-commit两种规则,二者保证了事务的持久性。WAL要求数据的变更写入到磁盘前,首先必须将内存中的日志写入到磁盘;Force-log-at-commit要求当一个事务提交时,所有产生的日志都必须刷新到磁盘上,如果日志刷新成功后,缓冲池中的数据刷新到磁盘前数据库发生了宕机,那么重启时,数据库可以从日志中恢复数据。

    归档日志文件(archive log file)

    redo log file由多个group构成。一个group中能包括不止一个log file,日志信息是写到group的每个log file中,所以一个group中的log file存储着一样的信息。当一个group写满之后就转到下一个group中,称之为日志切换。

    当所有group都写满了后,就重头开始从第一个group开始,原来的内容将被覆盖丢失。如果不想被丢失,可以采用归档模式,即将数据保存到archive log file中。归档模式会给系统带来一定的性能问题。

    InnoDB一致性非锁定读

    一致性非锁定读的机制

    一致性的非锁定读(consistent non-locking read)是指InnoDB引擎通过行多版本控制的方法来读取当前执行时间数据库中行的数据。若读取的行正在执行delete/update操作,InnoDB引擎不会等到行上锁的释放,而是去读取行的一个快照数据。快照数据是指该行之前版本的数据,实现是通过undo段来完成,本身没有额外开销。

    MVCC(多版本并发控制)原理

    一个行记录可能有不止一个快照数据,称之为行多版本技术,由此带来的并发控制,称为多版本并发控制(Multi Version Concurrency Control)。

    InnoDB的MVCC实现

    MVCC实现是通过undo日志实现的,当一条记录被修改时,当前的老版本数据会作为一条undo记录保存在undo log中。读快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。

    RR事务隔离级别下,对于快照数据,非锁定一致读总是读取事务开始时的行数据版本。

    RC事务隔离级别下,总是读取最新的快照数据。

    InnoDB事务分析

    事务特性ACID

    • 原子性(Atomicity):即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做;
    • 一致性(Consistency):在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是处于正确的状态,即数据完整性约束没有被破坏。
    • 隔离性(Isolation):并发事务执行之间无影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性;
    • 持久性(Durability):事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失。

    A、C、D三个特性是通过redo log和undo log 实现的。

    I是通过锁和MVCC机制来实现的。

    原子性原理分析

    undo log记录了回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

    一致性原理分析

    数据库必须要实现A、I、D三大特性,才有可能实现一致性(最终目的)。

    持久性原理分析

    当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo logbinlog内容决定回滚数据还是提交数据。

    隔离性原理分析

    InnoDB用MVCC来实现非阻塞的读操作,不同隔离级别下,MVCC通过读取不同版本的数据来解决RR的问题。

    事务并发问题理解
    • 丢失更新:两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的
    • 脏读:一个事务看到了另一个事务未提交的更新数据
    • 不可重复读:在同一事务中,多次读取同一数据却返回不同的结果(也就是有其他事务更改了这些数据)
    • 幻读:一个事务在执行过程中读取到了另一个事务已提交的插入数据(即在第一个事务开始时读取到一批数据,但此后另一个事务又插入了新数据并提交,此时第一个事务又读取这批数据但发现多了一条,即好像发生幻觉一样)
    事务隔离级别
    • 未提交读(Read Uncommitted):最低隔离级别,一个事务能读取到别的事务未提交的更新数据,很不安全,可能出现丢失更新、脏读、不可重复读、幻读;
    • 提交读(Read Committed):一个事务能读取到别的事务提交的更新数据,不能看到未提交的更新数据,不可能可能出现丢失更新、脏读,但可能出现不可重复读、幻读;
    • 可重复读(Repeatable Read):保证同一事务中先后执行的多次查询将返回同一结果,不受其他事务影响,可能可能出现丢失更新、脏读、不可重复读,但可能出现幻读;
    • 序列化(Serializable):最高隔离级别,不允许事务并发执行,而必须串行化执行,最安全,不可能出现更新、脏读、不可重复读、幻读。
  • 相关阅读:
    android 图片特效处理之模糊效果
    android 图片特效处理之怀旧效果
    android图像处理系列之六-- 给图片添加边框(下)-图片叠加
    android图像处理系列之五-- 给图片添加边框(中)
    android图像处理系列之四-- 给图片添加边框(上)
    android图像处理系列之三-- 图片色调饱和度、色相、亮度处理
    Android学习笔记进阶18 之画图并保存图片到本地
    Android学习笔记进阶19 之给图片加边框
    HDU3572_Task Schedule(网络流最大流)
    再淡spring jdbc 连接池断开重连设置
  • 原文地址:https://www.cnblogs.com/angelica-duhurica/p/11303271.html
Copyright © 2011-2022 走看看