什么是锁?MySQL 中提供了几类锁?
锁是实现数据库并发控制的重要手段,能够保证数据库在多人同时操做时可以正常运行。MySQL 提供了全局锁、行级锁、表级锁。其中 InnoDB 支持表级锁和行级锁,MyISAM 只支持表级锁。html
详解锁mysql
全局锁:sql
对整个数据库实例加锁,MySQL提供了一个加全局读锁的命令:【Flush tables with read lock】-FTWRL。当须要让整个库处于只读状态,可以使用这个命令。其余线程进行:增删改、数据定义(建表、修改表)和更新类事务的提交语句都会被阻塞。
该锁的典型使用场景是:全库逻辑备份,即把整库全部表都select出来存成文本。
经过FTWRL将整库只读,可能会致使两个问题:数据库
- 主库上备份时,备份期间都不能执行更新,业务会停摆。
- 从库上备份时,备份期间从库不能执行主库同步过来的binlog,会致使主从延迟。
备份加锁是为了不系统备份获得的库与主库不是同一个逻辑时间点,数据不一致。
如何处理逻辑备份时,整个数据库不能插入的状况?
经过MySQL自带的逻辑备份工具mysqldump,使用参数-single-transaction时,导数据以前会启动一个事务(隔离级别是可重复读),来确保拿到一致性视图,因为MVCC的支持,这个过程当中数据是能够正常更新的。可是这种备份方式须要引擎能支持这个隔离级别,如MyISAM不支持事务,则备份须要用FTWRL命令。
FTWRL设置的全库只读与set global readonly = true(设置数据库只读)的区别:安全
- 在有些系统,readonly的值会被用来作其余逻辑,如用来判断一个库是主库仍是备库。所以修改global变量的方式影响更大。
- 异常处理机制上存在差别,若是用FTWRL命令后客户端发生异常断开,那么MySQL会自动释放全局锁,整库回到能够正常更新的状态。而设置readonly后,若是客户端异常断开,数据库将一直保存readonly状态,这样会致使整库长时间处于不可写状态,风险较高。
表级锁:
MySQL里表级锁有两种:表锁和元数据锁(meta data lock,MDL)
表锁:
语法:lock tables ...read/wirte。可用unlock tables主动释放锁,客户端断开时也会主动释放。
注意:lock tables会限制别的线程读写,也会限制本线程接下来的操做对象。若是A线程执行lock table t1 read,t2 write。则其余线程写t1,读t2都会被阻塞,同时,线程A在释放表锁以前,也只能执行读t1,读写t2的操做,写t1也不行,也不能访问其余表。
对于 InnoDB 这种支持行锁的引擎,通常不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面仍是太大。
MDL:
MDL在访问一个表时会被自动加上 ,其做用是保证读写正确性。在并发状况下维护数据的一致性。MySQL5.5引入MDL,当对一个表作增删改查时,加MDL读锁,当对表结构进行变动时,加MDL写锁。
读锁之间不互斥,保证多个线程同时对一张表。写锁之间是互斥的,用来保证变动表结构操做的安全性,所以,两个线程同时给一个表加字段,其中一个要等到另外一个执行完才能开始执行。
注意:事务中MDL锁,在语句执行开始时申请,但在语句结束后不会当即释放,而是等到整个事务提交后再释放。在DML与DDL操做交互时,若是客户端有查询重试机制,就容易产生session爆满,致使内存增高。
例子:sessionA对表t1进行查询,此时表t1会加MDL读锁,此时sessionC发起DDL操做请求,但A的MDL读锁为释放,致使C申请MDL写锁被阻塞,后面若是有其余的session发起查询申请MDL读锁,都会被堵塞,当客户端发起重试时,查询过多。会致使大量session被阻塞,致使内存升高。session
行锁:
行级锁是 MySQL 中粒度最小的一种锁,他能大大减小数据库操做的冲突。InnoDB 的行级锁有共享锁(S LOCK)和排他锁(X LOCK)两种。共享锁容许事物读一行记录,不容许任何线程对该行记录进行修改。排他锁容许当前事物删除或更新一行记录,其余线程不能操做该记录。
共享锁:SELECT ... LOCK IN SHARE MODE,MySQL 会对查询结果集中每行都添加共享锁,前提是当前线程没有对该结果集中的任何行使用排他锁,不然申请会阻塞。
排他锁:select * from t where id=1 for update,其中 id 字段必须有索引,MySQL 会对查询结果集中每行都添加排他锁,在事物操做中,任何对记录的更新与删除操做会自动加上排他锁。前提是当前没有线程对该结果集中的任何行使用排他锁或共享锁,不然申请会阻塞。
针对两阶段协议该注意:
在InnoDB事务中,行锁是在须要的时候才加上,但并非不须要了就马上释放,而是要等到事务结束时才释放。
根据两阶段协议,当遇到事务中须要锁多个行时,要把最可能形成冲突、最可能影响并发度的锁尽可能日后放。架构
死锁:
是指两个或两个以上的进程在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的过程称为死锁。
常见的死锁案例:并发
将投资的钱拆封几份借给借款人,这时处理业务逻辑就要把若干个借款人一块儿锁住 select * from xxx where id in (xx,xx,xx) for update。 批量入库,存在则更新,不存在则插入。 解决方法 insert into tab(xx,xx) on duplicate key update xx='xx'。
如何查看死锁?工具
使用命令 show engine innodb status 查看最近的一次死锁。InnoDB Lock Monitor 打开锁监控,每 15s 输出一第二天志。使用完毕后建议关闭,不然会影响数据库性能。
对待死锁常见的两种策略:
经过 innodblockwait_timeout 来设置超时时间,一直等待直到超时;
发起死锁检测,发现死锁以后,主动回滚死锁中的某一个事务,让其它事务继续执行;
如何避免死锁?性能
- 为了在单个 InnoDB 表上执行多个并发写入操做时避免死锁,能够在事务开始时经过为预期要修改的每一个元祖(行)使用 SELECT ... FOR UPDATE 语句来获取必要的锁,即便这些行的更改语句是在以后才执行的。
- 在事务中,若是要更新记录,应该直接申请足够级别的锁,即排他锁,而不该先申请共享锁、更新时再申请排他锁,由于这时候当用户再申请排他锁时,其余事务可能又已经得到了相同记录的共享锁,从而形成锁冲突,甚至死锁。
- 若是事务须要修改或锁定多个表,则应在每一个事务中以相同的顺序使用加锁语句。在应用中,若是不一样的程序会并发存取多个表,应尽可能约定以相同的顺序来访问表,这样能够大大下降产生死锁的机会
- 经过 SELECT ... LOCK IN SHARE MODE 获取行的读锁后,若是当前事务再须要对该记录进行更新操做,则颇有可能形成死锁。
- 改变事务隔离级别。
InnoDB 默认是如何对待死锁的?
InnoDB 默认是使用设置死锁时间来让死锁超时的策略,默认 innodblockwait_timeout 设置的时长是 50s。
如何开启死锁检测?
设置 innodbdeadlockdetect 设置为 on 能够主动检测死锁,在 Innodb 中这个值默认就是 on 开启的状态。
悲观锁:
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。
乐观锁:
用数据版本(Version)记录机制实现,这是乐观锁最经常使用的一种实现方式。何谓数据版本?即为数据增长一个版本标识,通常是经过为数据库表增长一个数字类型的 version 字段来实现。当读取数据时,将 version 字段的值一同读出,数据每更新一次,对此 version 值加 1。当咱们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,若是数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,不然认为是过时数据。
示例:据库表三个字段,分别是id、value、version
select id,value,version from t where id=#{id}
每次更新表中的value字段时,为了防止发生冲突,须要这样操做
update t set value=2,version=version+1 where id=#{id} and version=#{version}
乐观锁有什么优势和缺点?
由于没有加锁因此乐观锁的优势就是执行性能高。它的缺点就是有可能产生 ABA 的问题,ABA 问题指的是有一个变量 V 初次读取的时候是 A 值,而且在准备赋值的时候检查到它仍然是 A 值,会误觉得没有被修改会正常的执行修改操做,实际上这段时间它的值可能被改了其余值,以后又改回为 A 值,这个问题被称为 ABA 问题。
共享锁:
共享锁又称读锁 (read lock),是读取操做建立的锁。其余用户能够并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放全部共享锁。当若是事务对读锁进行修改操做,极可能会形成死锁。
排它锁:
排他锁 exclusive lock(也叫 writer lock)又称写锁。
若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束以前,其余事务不能对其进行加任何锁,其余进程能够读取,不能进行写操做,需等待其释放。
排它锁是悲观锁的一种实现:若事务 1 对数据对象 A 加上 X 锁,事务 1 能够读 A 也能够修改 A,其余事务不能再对 A 加任何锁,直到事物 1 释放 A 上的锁。这保证了其余事务在事物 1 释放 A 上的锁以前不能再读取和修改 A。排它锁会阻塞全部的排它锁和共享锁。
优化锁的建议:
- 尽可能使用较低的隔离级别。
- 精心设计索引, 并尽可能使用索引访问数据, 使加锁更精确, 从而减小锁冲突的机会。
- 选择合理的事务大小,小事务发生锁冲突的概率也更小。
- 给记录集显示加锁时,最好一次性请求足够级别的锁。好比要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁。
- 不一样的程序访问一组表时,应尽可能约定以相同的顺序访问各表,对一个表而言,尽量以固定的顺序存取表中的行。这样能够大大减小死锁的机会。
- 尽可能用相等条件访问数据,这样能够避免间隙锁对并发插入的影响。
- 不要申请超过实际须要的锁级别。
- 除非必须,查询时不要显示加锁。 MySQL 的 MVCC 能够实现事务中的查询不用加锁,优化事务性能;MVCC 只在 COMMITTED READ(读提交)和 REPEATABLE READ(可重复读)两种隔离级别下工做。
- 对于一些特定的事务,可使用表锁来提升处理速度或减小死锁的可能。
参考文献:
- 高性能MySQL第三版
- 极客时间:MySQL45讲
- 扛得住的MySQL数据库架构:https://coding.imooc.com/class/chapter/49.html#Anchor