关于mysql中的锁
在并发环境下,有可能会出现脏读(Dirty Read)、不可重复读(Unrepeatable Read)、 幻读(Phantom Read)、更新丢失(Lost update)等情况,所以mysql引入了很多锁的概念
MySQL InnoDB对数据行的锁定类型一共有四种:共享锁(读锁,S锁)、排他锁(写锁,X锁)、意向共享锁(IS锁)和意向排他锁(IX锁),支持三种行锁定方式:
- 行锁(Record Lock):锁直接加在索引记录上面。
- 间隙锁(Gap Lock):锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。
- Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock。
默认情况下,InnoDB工作在可重复读隔离级别下,并且以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁与间隙锁的组合,这样,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。如果一个间隙被事务T1加了锁,其它事务是不能在这个间隙插入记录的。
- 在可重复读级别下,InnoDB以Next-Key Lock的方式对索引加锁;在读已提交级别下,InnoDB以Index-Record Lock的方式对索引加锁。
- 被加锁的索引如果不是聚族索引,那被锁的索引所指向的聚族索引以及其它指向相同聚族索引的索引也会被加锁。
- SELECT * FROM ... LOCK IN SHARE MODE对索引加共享锁;SELECT * FROM ... FOR UPDATE对索引加排他锁。
- SELECT * FROM ... 是非阻塞式读,(除Serializable级别)不会对索引加锁。在读已提交级别下,总是查询记录的最新、有效的版本;在可重复读级别下,会记住第一次查询时的版本,之后的查询会基于该版本。例外的情况是在串行化级别,这时会以Next-Key Lock的方式对索引加共享锁。
- UPDATE ... WHERE 与DELETE ... WHERE对索引加排他锁。
- INSERT INTO ... 以Index-Record Lock的方式对索引加排他锁
什么是悲观锁
悲观锁是指对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态,在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
悲观锁(pessimistic locking)体现了一种谨慎的处事态度。其流程如下:
- 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了
- 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常
悲观锁确实很严谨,有效保证了数据的一致性,在C/S应用上有诸多成熟方案。 但是他的缺点与优点一样的明显
- 悲观锁适用于可靠的持续性连接,诸如C/S应用。 对于Web应用的HTTP连接,先天不适用
- 锁的使用意味着性能的损耗,在高并发、锁定持续时间长的情况下,尤其严重。 Web应用的性能瓶颈多在数据库处,使用悲观锁,进一步收紧了瓶颈
- 非正常中止情况下的解锁机制,设计和实现起来很麻烦,成本还很高
- 不够严谨的设计下,可能产生莫名其妙的,不易被发现的, 的死锁问题
自动提交
MySQL 采用 autocommit 模式运行。这意味着,当执行一个用于更新(修改)表的语句之后,MySQL立刻把更新到buffer中,同时记录锁也会被释放。因此如果事务要执行多条更新(修改)语句,那么从第2条更新语句开始就是在无锁条件下执行了,这样会导致事务失效,破坏数据一致性
mysql> select @@autocommit; +--------------+ | @@autocommit | +--------------+ | 1 | +--------------+
关闭自动提交
set autocommit=0; or [mysqld] init_connect=‘SET autocommit=0‘ ;
避免此问题的方法就是关闭 autocommit,然后通过执行 commit 语句来提交事务
$db->begin(); $db->query("SET autocommit=0"); ... ... $db->commit();
注意:
1、不能将"关闭autocommit"作为缺省设置,否则在 innodb 表上执行的查询操作也将因为没有执行 commit 或者 rollback 而一直锁表!因此只能在需要时局部关闭 autocommit,并在操作完成后开启 autocommit
2、连接mysql用户的权限不能大于启动mysql的用户的权限,不然init_connect=‘SET autocommit=0‘根本不会启作用,也不会报任何错误
If a user has SUPER privilege, init_connect will not execute,(otherwise if init_connect will a wrong query no one can connect to server).
Note, if init_connect is a wrong query, the connection is closing without any errors and next command will clause ‘lost connection‘ error.
使用举例
要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 sec) mysql> select @@autocommit; +--------------+ | @@autocommit | +--------------+ | 0 | +--------------+ 1 row in set (0.00 sec)
Connect a:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select age from users where id =1 for update; +-----+ | age | +-----+ | 24 | +-----+ 1 row in set (0.00 sec) mysql> update users set age = 25 where id =1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.01 sec)
Connect b: 在a长时间未提交的时候
mysql> select * from users where id =1 for update; ERROR 1205 : Lock wait timeout exceeded; try restarting transaction mysql> update users set age = 22 where id =1; ERROR 1205 : Lock wait timeout exceeded; try restarting transaction
connect b 只有在 connect a 提交之后才会执行,否则会一直等待
在事务中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 会等待其它事务结束后才执行,一般SELECT ... 则不受此影响
MySQL select…for update 的 Row Lock 与 Table Lock
上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键/索引,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)
如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤,但在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是MySQL,为了效率也是会违反规范的
这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象
优缺点
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;
另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据
悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好
乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能
参考文章
http://tech.meituan.com/innodb-lock.html
http://hedengcheng.com/?p=771
http://ouyanggod.iteye.com/blog/2166384