zoukankan      html  css  js  c++  java
  • 20.Mysql锁机制

    20.锁问题
    锁是计算机协调多个进程或线程并发访问某一资源的机制。

    20.1 Mysql锁概述
    锁类型分为表级锁、页面锁、行级锁。
    表级锁:一个线程对表进行DML时会锁住整张表,其它线程只能读该表,如果要写该表时将产生锁等待。
    优点:开销少、加锁快、不会产生死锁。缺点:锁粒度大、容易产生锁等待、并发低。
    行级锁:一个线程对表进行DML时会锁住该表影响的行,其它线程可以读该表,也可以DML该表其它的行,如果要DML已被锁定的行时将产生锁等待。
    缺点:开销大、加锁慢、会产生死锁。优点:锁粒度小、不容易产生锁等待、并发高。
    页面锁:一个线程对表进行DML时会锁住该表影响行的所在页面(dbblock),其它线程可以读该表,也可以DML该表其它的页面(dbblock),如果要DML已被锁定的页面(dbblock)时将产生锁等待。
    页面锁介于表级锁和行级锁中间,会产生死锁,并发一般,表只有一个页面(dbblock)时等同于表级锁。
    Mysql不同的存储引擎支持不同的锁机制。
    MyISAM和MEMORY存储引擎支持表级锁;
    BDB存储引擎支持页面锁;
    INNODB存储引擎支持行级锁。

    20.2 MyISAM表锁
    20.2.1 查询表级锁争用情况
    状态变量table_locks_immediate:立即加锁的次数
    状态变量table_locks_waited:产生锁等待的次数

    20.2.2 Mysql表级锁的锁模式
    共享读锁lock table 表名 read:一个进程的读操作,不阻断其它进程的读操作,但阻断其它进程的写操作。
    独占写锁lock table 表名 write:一个进程的写操作,阻断其它进程的读操作和写操作。
    读操作之间的并行的,读写操作之间是串行的,写操作之间是串行的。
    释放锁unlock tables:释放所有锁。

    20.2.3 如何加表锁
    select 语句会自动给涉及的表加读锁;
    insertupdatedelete 语句会自动给涉及的表加写锁;
    也可通过命令 lock table 表名 read/write 显示的加读/写锁。
    lock table 表名 read local; --增加local选项指读操作只锁定表中现有记录,其它进程可以增加新的记录,但不能删除、修改现有记录。
    读写操作加锁时会给涉及的所有表加锁,如果其中哪个表已经被其它进程锁定(且非共享读锁)时,将产生锁等待。
    Mysql不支持锁升级,使用lock table 表名 read加锁后,进程只能访问被自己加锁的表(不能访问未加锁的表),且必须按已加锁的锁模式来访问(加的读锁时不能进行写操作)。
    使用lock table 表名 read加锁后,不能对表起别名。

    20.2.4 并发插入(Concurrent Inserts)
    系统变量concurrent_insert=[0|1|2] 控制并发插入行为。指表被读锁锁定时,是否允许其它进程在表的末尾进行插入数据。
    0 不允许并发插入;1 表没有空洞(中间被删除的行)时允许在表尾进行并发插入;2 允许在表尾进行并发插入。
    optimize table命令可以整理空间碎片,回收因删除操作产生的空洞。

    20.2.5 MyISAM的锁调度
    MyISAM的锁调度策略:读写互斥,读写串行,写锁优先。
    问题1:存在大量DML操作时,DQL操作很难获得读锁,长时间被阻塞。
    启动参数low-priority-updates,系统层面降低写请求优先级
    set low-priority-updates=1 会话层面降低写请求优先级
    在insert/update/delete语句后增加low-priority属性 语句层面降低写请求优先级
    系统参数max_write_lock_count=x, 当一个表的读锁达到阈值x时,Mysql暂时降低写请求优先级,给读进程一个获得锁的机会。
    问题2:耗时长的读进程,也会导致写进程所等待并超时。
    应尽量避免耗时长的查询,通过SQL优化或将复杂SQL拆分为多条SQL语句。

    20.3 InnoDB锁问题
    InnoDB支持事务采用了行级锁。
    20.3.1 背景知识
    1.事务及其ACID属性
    事务是一组SQL语句组成的逻辑处理单元,事务具有原子性(Atomicity)、一致性(Consistent)、隔离性(Isolation)、持久性(Durable)4个特性。
    原子性:事务中的一组SQL要么全部执行,要么全部回退,不能部分执行。
    一致性:事务开始和完成时数据必须保持一致状态。
    隔离性:事务在一个session中运行,不受其它session影响,事务的中间状态对外不可见。
    持久性:事务完成后数据的修改是永久性的。

    2.并发事务处理带来的问题
    更新丢失:多个事务基于同一行的更新操作可能相互覆盖。通过锁机制消除。
    脏读:一个事务修改了数据,尚未提交完成时,另一个事务读取了修改后的数据,称为脏读。通过快照提供读一致性。
    不可重复读:一个事务中两次读取同一个数据,读取结果不同时,称为不可重复读。
    幻读:一个事务中两次读取同一个数据,读取结果数量不同时,称为幻读。

    3.事务的隔离级别
    脏读、不可重复读、幻读都是读一致性问题,可以通过事务的隔离机制里解决。
    数据库实现事务隔离的方式分为:锁和快照。
    锁方式事务隔离:在读取数据前对其加锁,防止其它事务对数据进行修改。
    快照方式事务隔离:生成一个数据请求时间点的一致性数据快照,使用快照来提供一定级别的读一致性。
    隔离性越严格,并发的问题越少,并发量越低。事务隔离实质就是将并发事务串行化执行。
    ISO SQL92定义了4个事务隔离级别:
    读未提交Read uncommitted:存在脏读、不可重复读、幻读
    读已提交Read committed :不存在脏读,存在不可重复读、幻读
    可重复读Repeatable read :不存在脏读、不可重复读,存在幻读
    可序列化Serializable :不存在脏读、不可重复读、幻读
    Mysql提供参数tx_isolation设置事务隔离级别,默认为可重复读Repeatable-read。

    20.3.2 获取InnoDB行锁争用情况
    状态变量:
    InnoDB_row_lock_waits 锁等待次数
    InnoDB_row_lock_time 锁等待时间
    InnoDB_row_lock_time_avg 锁等待平均时间
    InnoDB_row_lock_time_max 锁等待单次最大时间
    InnoDB_row_lock_current_waits 当前的锁等待次数
    如果锁等待次数和锁等待平均时间的值比较高,说明系统存在严重的锁争用。
    可以通过information_schema模式下的innodb_locks和innodb_lock_waits表查看。
    也可以创建innodb_monitor表监控发生锁冲突的表和数据行:
    create table innodb_monitor(a int)engine=innodb;
    打开监控后通过show engine innodb status;命令,会输出详细的锁等待信息,包括:表名、锁类型、锁定的记录等。
    打开监控后15秒后系统向.err文件记录监控内容,.err文件会急剧增加,确定问题后需要停止监控,即删除innodb_monitor表。
    停止监控时需要drop table innodb_monitor;

    20.3.3 InnoDB的行锁模式及加锁方法
    InnoDB的行锁模式分为共享锁、排他锁、意向共享锁、意向排他锁。
    共享锁(S): 允许一个事务读一行,阻止其它事务写这一行
    排他锁(X): 允许一个事务写一行,阻止其它事务读这一行或写这一行
    意向共享表锁(IS): 事务在加S锁之前,应先取得表的IS锁
    意向排他表锁(IX): 事务在加X锁之前,应先取得表的IX锁
    锁兼容:如果一个事务请求的锁模式与当前的锁模式兼容,InnoDB就将请求的锁授予该事务。
    锁等待:如果一个事务请求的锁模式与当前的锁模式不兼容,InnoDB就将该请求挂起,等待当前的锁释放,产生锁等待。
    IS S IX X
    IS ok ok ok no
    S ok ok no no
    IX ok no ok no
    X no no no no
    意向锁是InnoDB自动添加的,不需要用户干预。
    insert/update/delete/select ... for update 会自动添加X锁。
    select 不会添加任何锁。
    手动加S锁方法:select * from 表名 where ... lock in share mode;
    手动加X锁方法:select * from 表名 where ... for update;

    20.3.4 InnoDB行锁实现方式
    InnoDB行锁是给索引上的索引项加锁实现的。
    如果没有索引,InnoDB将通过隐藏的聚簇索引来对记录加锁。
    Record lock:对索引项加锁
    Gap lock:对索引项之间的间隙(包括:第一条记录前的间隙和最后一条记录后的间隙)加锁。
    Next-key lock:对记录及其前面的间隙加锁。
    在不通过索引条件检索数据时,则对表中所有记录加锁。
    select * from emp where empno=7788 for update; --empno列有索引,所以只对emp表中7788一行加锁
    select * from emp where ename='scott' for update; --ename列没有索引,所以对emp表中所有记录加锁
    对唯一索引的某个键值加锁时只锁定一行,对非唯一索引的某个键值加锁时将锁定这个键值对应的多行记录。
    当表中有多个索引时,不同的事务可以使用不同索引锁定不同的行,但不能是同一行。无论使用的索引的类型。
    即便在SQL的where条件中使用了索引列,但是否使用索引由Mysql执行计划决定,如果索引失效将对表中全部记录加锁。

    20.3.5 Next-Key锁
    在使用范围条件并请求共享锁或排他锁时,InnoDB会给符合条件的已有记录的索引项加锁,
    对于符合条件但并不存在的记录称为“间隙”,InnoDB会给间隙也加锁,这种机制称为Next-Key锁。
    Next-Key锁的作用在于防止幻读,并满足Mysql恢复和复制的需要。
    业务中应尽量使用等值条件,避免使用范围条件,以降低Next-Key锁等待。
    在等值条件中请求给一个不存在的记录加锁,InnoDB也会使用Next-Key锁。

    20.3.6 恢复和复制的需要,对InnoDB锁机制的影响
    Mysql通过binlog记录执行成功的DML操作,并由此实现Mysql的数据库恢复和主从复制。
    binlog日志格式分为:基于语句的日志格式SBL、基于行的日志格式RBL和混合格式。
    Mysql 4中复制模式:基于SQL语句的复制SBR、基行数据的复制RBR、混合复制模式和使用全局事务ID(GTIDs)的复制。
    基于SQL语句的复制SBR要求:一个事务未提交前,其它并发事务不能插入满足其锁定条件的任何记录,即不允许出现幻读。
    即使在其它隔离级别下,InnoDB均会使用Next-Key锁保证复制的串行化。
    对复制语句create table tab_name select ...和insert into table_name select ... Mysql为保证恢复和复制的正确性增加了共享锁。
    系统参数innodb_locks_unsafe_for_binlog=[off|on] 默认未off。
    当设置未on时,给复制语句不加锁,但有可能导致复制不一致。

    20.3.7 InnoDB在不同隔离级别下的一致性读及锁的差异
    锁和快照是Innodb实现一致性读和SQL92隔离级别的方法,
    不同隔离级别下Innodb处理SQL时采用的一致性读策略和需要的锁是不同的。
    读未提交 读已提交 可重复度 可序列化
    select 相等 None locks None locks None locks None locks
    select 范围 None locks None locks None locks S Next-Key
    insert X X X X
    update 相等 X X X X
    update 范围 X Next-Key X Next-Key X Next-Key X Next-Key
    delete 相等 X X X X
    delete 范围 X Next-Key X Next-Key X Next-Key X Next-Key
    replace 无键冲突 X X X X
    replace 键冲突 X Next-Key X Next-Key X Next-Key X Next-Key
    lock in S 相等 S S S S
    lock in S 范围 S S S Next-Key S Next-Key
    for update 相等 X X X X
    for update 范围 X S(X) X Next-Key X Next-Key
    复制数据 off S Next-Key S Next-Key S Next-Key S Next-Key
    复制数据 on None locks None locks None locks S Next-Key
    复制表 off S Next-Key S Next-Key S Next-Key S Next-Key
    复制表 on None locks None locks None locks S Next-Key
    说明:
    lock in S 指 手动添加S锁 select * from 表名 where ... lock in share mode;
    for update 指 手动添加X锁 select * from 表名 where ... for update;
    复制数据 指 insert into 表名 select ...
    复制表 指 create table 表名 select ...
    on 指 系统参数innodb_locks_unsafe_for_binlog=on
    off 指 系统参数innodb_locks_unsafe_for_binlog=off
    None locks 指 没有加任何锁
    S 指 Share lock 行级共享锁
    X 指 Exclusive lock 行级排他锁
    S Next-Key 指 Share Next-Key 行级共享间隙锁
    X Next-Key 指 Exclusive Next-Key 行级排他间隙锁
    相等 指 条件中使用主键列的等值比较,即只处理一条记录
    范围 指 条件中使用主键列的范围比较或非主键列比较或未使用索引,即处理多条记录
    注意:隔离级别越高,加锁越严格,锁冲突的可能性就越高,并发性越低。
    设置隔离级别的方法:
    1.修改my.cnf文件中tx_isolation系统参数
    tx_isolation=READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE;
    2.动态修改参数
    set [session|gloabl] tx_isolation=READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE;
    3.在程序中执行
    set session transactionn isolation READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE;

    20.3.8 什么时候使用表锁
    事务和行锁是Innodb被选择的原因,个别特殊事务中可以考虑使用表级锁。
    场景1:事务需要更新大部分或全部数据(大量数据)时,使用表级锁可以提升性能。
    场景2:事务涉及多个表且逻辑复杂,很可能一起死锁而造成大量事务回滚时,使用表级锁可以避免死锁,减少回滚。
    使用表级的方法:
    set autocommit=0; -- 关闭自动提交功能,否则Mysql不会给表加锁
    lock tables 表名1 write|read,...; -- 添加表级锁,可以一次给多个表添加
    要处理的事务SQL
    commit; -- 提交或回退事务,commit或rollback不能释放表级锁
    unlock tables; -- 释放全部表级锁,unlock tables可以隐式提交事务
    说明:
    表锁不是InnoDB存储引擎管理的,而是由Mysql Server管理,
    仅当autocommit=0(默认为1)、innodb_table_locks=1(默认为1)时,
    InnoDB引擎才能感知Mysql server是否添加了表锁,Mysql server才能感知InnoDB引擎是否添加了行锁。
    否则,InnoDB将无法自动检测和处理因Mysql server添加了表锁而与已有行锁的冲突以及死锁。

    20.3.9 关于死锁
    MyIASM引擎只有表级锁,且需要一次获取所有的锁,要么全部满足,要么等待,不会产生死锁。
    InnoDB引擎是行级锁,锁是由事务中的单条SQL逐步获取的,所以可能会产生死锁。
    死锁是指AB两个事务各有一个排他锁,且需要获得对方已占有排他锁,才能结束自身的事务,此时产生的相互的锁等待称为死锁。
    InnoDB可以自动检测死锁,并回退其中一个事务,释放该事务占有的锁,使另一个事务获取要请求的锁,继续完成该事务。
    在涉及外部锁和表锁时,InnoDB并不能检测到死锁,而是通过锁等待超时参数innodb_lock_wait_timeout来释放等待超时的事务的锁来解决死锁问题。
    大量事务的锁等待会占用大量计算机资源,引起性能问题,所以要设置合适的锁等待超时参数innodb_lock_wait_timeout阈值。
    死锁一般都是应用设计的问题,可以通过调整业务流、数据库对象、事务大小、以及SQl语句来解决,从而从根本上避免死锁。
    减少死锁方法:
    1.程序并发存取多个表时,应尽量约定以相同的顺序来访问表。
    2.在批量处理数据时,应对数据排序,以固定的顺序来处理记录。
    3.在事务中应该直接申请足够级别的锁,避免逐步的锁升级。
    4.将默认隔离级别由REPEATABLE READ降低至READ COMMITTED。
    5.事务报错后应使用rollback语句释放已获得的排他锁。
    6.可以通过show innodb status;命令分析最后一个死锁产生的原因,
    包括:引发死锁的SQL语句,事务已获得的锁,正在等待的锁,以及被回滚的事务等。
    依此分析死锁产生的原因和改进措施。

    20.4 小结
    减少锁等待和死锁次数的措施:
    1.尽量使用较低的隔离级别;
    2.精心设计索引,并尽量使用索引访问数据,使加锁更加准确,从而减少锁冲突的机会;
    3.选择合理的事务大小,小事务发生锁冲突的几率更小;
    4.给记录集显示加锁时,最好一次性请求足够级别的锁,而不是逐步进行锁升级;
    5.不同程序访问一组表时,应尽量约定以相同的顺序访问各个表,对一个表而言,尽可能以固定的顺序存取表中的行;
    6.尽量使用相等条件访问数据,避免Next-Key锁的影响;
    7.不要申请超过实际需要的锁级别,除非必须,否则查询时不要显示加锁;
    8.对特定事务,可以使用表锁来提高处理速度或减少发生死锁的几率。

  • 相关阅读:
    LinkedList源码分析
    Hashtable源码分析
    String源码分析
    记一次ArrayList产生的线上OOM问题
    【spring源码分析】IOC容器初始化——查漏补缺(五)
    前端面试的那些事儿(1)~JavaScript 原始数据类型
    前端面试的那些事儿(2)~ 不再害怕被问 JavaScript 对象
    第二周技术周报-前端的自我修养
    第一周技术周报-前端框架演变
    JavaScript数据类型检测 数组(Array)检测方式
  • 原文地址:https://www.cnblogs.com/BradMiller/p/10123404.html
Copyright © 2011-2022 走看看