我们知道,数据也是一种供许多用户共享访问的资源。如何保证数据并发访问的一致性、有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素。从这一角度来说,锁对于数据库而言就显得尤为重要。
本文将带领大家一起深入领略Mysql锁的各种风采。
01 表锁
表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。
该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。表锁被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁。
MyISAM只是支持表锁,因此性能相对Innodb来说相对降低,而Innodb也支持表锁,但是默认的行锁,而且只有在查询或者其他SQL语句通过索引才会使用行锁。
02 行锁
行锁的是mysql锁中粒度最小的一种锁,因为锁的粒度很小,所以发生资源争抢的概率也最小,并发性能最大,但是也会造成死锁,每次加锁和释放锁的开销也会变大。
目前主要是Innodb使用行锁,Innodb也是mysql在5.5.5版本之后默认使用的存储引擎。
行锁按照使用方式也分为共享锁(S锁或者读锁)和排它锁(X锁或者写锁)
03 读锁(共享锁,S锁)
使用说明:若事务A对数据对象1加上S锁,则事务A可以读数据对象1但不能修改,其他事务只能再对数据对象1加S锁,而不能加X锁,直到事务A释放数据对象1上的S锁。这保证了其他事务可以读数据对象1,但在事务A释放数据对象1上的S锁之前不能对数据对象1做任何修改。
//用法:
select ... lock in share mode;
----读锁(共享锁)就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
04 写锁(排它锁,X锁)
使用说明 :若事务A对数据对象1加上X锁,事务A可以读数据对象1也可以修改数据对象1,其他事务不能再对数据对象1加任何锁,直到事务A释放数据对象1上的锁。这保证了其他事务在事务A释放数据对象1上的锁之前不能再读取和修改数据对象1。
//用法:
select ... for update
----写锁(排他锁)就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁
05 意向共享锁(IS)和意向排它锁(IX)
释义:
- 意向共享锁(IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
- 意向互斥锁(IX):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
意向共享锁和意向排它锁总称为意向锁。意向锁的出现是为了支持Innodb支持多粒度锁。
首先,意向锁是表级别锁。
理由:当我们需要给一个加表锁的时候,我们需要根据意向锁去判断表中有没有数据行被锁定,以确定是否能加成功。如果意向锁是行锁,那么我们就得遍历表中所有数据行来判断。如果意向锁是表锁,则我们直接判断一次就知道表中是否有数据行被锁定了。所以说将意向锁设置成表级别的锁的性能比行锁高的多。
有了意向锁之后,前面例子中的事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,因为表上有意向排他锁之后事务B申请表的写锁时会被阻塞。
所以,意向锁的作用就是:
当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。
06 乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(跟上一次的版本号比较,如果一样则更新),如果失败则要重复读-比较-写的操作。
JAVA 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
使用实例:
SELECT data AS old_data, version AS old_version FROM …;
// 接着根据获取的数据进行业务操作,得到new_data和new_version。然后:
UPDATE SET data = new_data, version = new_version WHERE version = old_version;
if (updated row > 0) {
// 乐观锁获取成功,操作完成
} else {
// 乐观锁获取失败,回滚并重试
}
优点:从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。
缺点:乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
总结:读用乐观锁,写用悲观锁。
07 悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
java 中的悲观锁就是 Synchronized,AQS 框架下的锁则是先尝试 cas 乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
悲观锁介绍(引自百科):
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)
悲观锁的实现:首先实现悲观锁时,我们必须先使用
//关闭mysql的autoCommit属性。
set autocommit=0;
因为我们查询出数据之后就要将该数据锁定。
关闭自动提交后,我们需要手动开启事务。
//1.开始事务
begin; //或者 start transaction;
//2.查询出商品信息,然后通过for update锁定数据防止其他事务修改
select status from t_goods where id=1 for update;
//3.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//4.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit; --执行完毕,提交事务
上述就实现了悲观锁,悲观锁就是悲观主义者,它会认为我们在事务A中操作数据1的时候,一定会有事务B来修改数据1,所以,在第2步我们将数据查询出来后直接加上排它锁(X)锁,防止别的事务来修改事务1,直到我们commit后,才释放了排它锁。
优点:保证了数据处理时的安全性。
缺点:加锁造成了开销增加,并且增加了死锁的机会。降低了并发性。
因为乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入居多,对吞吐要求不高,可使用悲观锁。
下面三种锁都是innodb的行锁,前面我们说过行锁是基于索引实现的,一旦加锁操作没有操作在索引上,就会退化成表锁。
08 间隙锁(Next-Key锁)
间隙锁,作用于非唯一索引上,主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。
如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
如图:(1,4),(4,7),(7,11),(11,∞)即为间隙锁要锁定的位置。
//举例说明:
SELECT * FROM table WHERE id = 8 FOR UPDATE;
//----此时,(7,11)就会被锁定
SELECT * FROM table WHERE id BETWEN 2 AND 5 FOR UPDATE;
//----此时,(1,4)和(4,7)就会被锁定
09 记录锁
记录锁,它封锁索引记录,作用于唯一索引上,如下图所示:
//例:
select * from t where id=4 for update;
//它会在id=4的索引记录上加锁,以阻止其他事务插入,更新,删除id=1的这一行。
// 需要说明的是:
// select * from t where id=4;
// 则是快照读(SnapShot Read),它并不加锁,不影响其他事务操作该数据。
10 临键锁
临键锁,作用于非唯一索引上,是记录锁与间隙锁的组合,如下图所示:
它的封锁范围,既包含索引记录,又包含索引之前的区间,即(负无穷大,1],(2,4],(5,7],(8,11],(12,无穷大]。
在事务A中执行:
UPDATE table SET name = 'javaHuang' WHERE age = 4;
SELECT * FROM table WHERE age = 4 FOR UPDATE;
这两个语句都会锁定(2,4],(4,7)这两个区间。
即, InnoDB 会获取该记录行的 临键锁 ,并同时获取该记录行下一个区间的间隙锁。
临键锁的出现是为了innodb在rr隔离级别下,解决幻读问题。
如果把事务的隔离级别降级为RC,临键锁则也会失效。
11 死锁
释义:死锁是指两个或两个以上事务在执行过程中因争抢锁资源而造成的互相等待的现象
上图所示,即为死锁产生的常规情景。
那么如何解决死锁?
- 等待事务超时,主动回滚。(事务回滚:将该事务已经完成的对数据库的更新操作撤销)
- 进行死锁检查,主动回滚某条事务,让别的事务能继续走下去。
下面提供一种方法,解决死锁的状态:
//查看正在被锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
//(上图trx_mysql_thread_id列的值)
kill trx_mysql_thread_id;
死锁是一个很复杂的话题,此处只能简而言之
12 总结
通过本文,大致了解了mysql大部分锁的 功能、作用、实现以及解决方法。
P.s. 下篇讲 “MySQL 事务机制”