锁是计算机协调多个进程或线程并发访问某一资源的机制。
在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供需要用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
锁分类
从性能上分为乐观锁(用版本对比或CAS机制)和悲观锁,乐观锁适合读操作较多的场景,悲观
锁适合写操作较多的场景,如果在写操作较多的场景使用乐观锁会导致比对次数过多,影响性能。
从对数据操作的粒度分,分为表锁、页锁、行锁。
从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁),还有意向锁。
读锁
又称(共享锁,S锁(shared))。若事务T对数据A(某一资源)加上S锁,则事务T可以读A但不能修改A。其它事务也只能读A但不能修改A。并且,其他事务只能再对A加S锁,不能加X锁,除非T释放A上的S 锁。简单的说,自己只能读,别人也只能读。
写锁
又称(排它锁、独占锁,X锁(exclusive))。若事务T对数据A(某一资源)加上X锁,事务T可以读A也可以修改A。其他事务不能再对A加任何锁(共享锁或排他锁),直到T释放A上的锁。简单的说,自己可读可写,别人可以无锁读但是不能加读锁,不可写,因为写操作默认加排它锁。
意向锁(Intention Lock)
又称I锁,针对表锁,主要是为了提高加表锁的效率,是mysql数据库自己加的。当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,表示已经有行锁了,这时如果其他事务要想对表再加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁。特别是表中的记录很多时,逐行判断加表锁的方式效率很低,而这个标识就是意向锁。
意向锁主要分为:
意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁。
意向排他锁,IX锁,对整个表加排他锁之前,需要先获取到意向排他锁。
表锁
每次操作锁住整张表,开销小,加锁快。不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低,一般用在整表数据迁移的场景。
准备测试数据
测试表锁
给表加读锁
lock table test read;
然后插入一条数据发现会阻塞,无法插入。
删除表锁
unlock tables;
查看表上加过的锁
show open tables;
页锁
只有BDB存储引擎支持页锁,页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
行锁
每次操作锁住一行数据。开销大,加锁慢(因为要扫描数据),会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度最高。
InnoDB相对于MYISAM的最大不同有两点:
InnoDB支持事务(TRANSACTION)
InnoDB支持行级锁
注意,InnoDB的行锁实际上是针对索引加的锁(在索引对应的索引项上做标记),不是针对整个行记录加的锁。并且该索引不能失效,否则会从行锁升级为表锁。(RR(REPEATABLE READ)级别会升级为表锁,RC(READ COMMITTED)级别不会升级为表锁)。
比如我们在RR级别执行如下sql:
select * from test where age= 18 for update;
where条件里的name字段无索引
行级锁操作
加共享锁:select ... lock in share mode
加排它锁:select ...for update
几个注意的点
mysql InnoDB引擎的数据库,增删改操作默认都会加排他锁,而查询不会加任何锁。
自己对某一资源加共享锁,别人也可以再继续加共享锁,即多个共享锁可以共存。
数据库同一资源上,共享锁和排他锁不能共存,排他锁和排他锁不能共存。
同一资源上,要么不存在任何锁,要么存在单个或多个共享锁,要么存在单个排它锁。
测试LOCK IN SHARE MODE 共享锁
现在我们对id=1的数据行进行共享锁查询,这里会使用BEGIN开启事务,而不去COMMIT提交事务,这样做是为了保证锁不被释放,因为提交事务或回滚事务都会释放锁。
测试一个事务中加了共享锁,在其他事务查询中直接查询、共享锁、排它锁的情况。
窗口1中共享锁的事务查询,且没有提交事务
窗口2中测试无锁查询:
结果:可以查询到数据,不受影响。
窗口2中测试共享锁查询:
结果:可以查询到数据,不受影响。
窗口2中测试排他锁查询:
结果:一直处于阻塞状态,没有返回查询结果集。
分析:一个事务中的查询加了共享锁,且没有提交事务,另一个事务中的无锁查询和共享锁查询是可以查到数据的,但排他锁查询不可以,因为排他锁与共享锁不能共存于同一数据上。可以这么理解,排它锁是一种独占的锁,数据只能被自己独占,所以,也就不能存在其他锁了。
测试for update 排他锁
begin;
select * from test where id=1 for update;
-- commit;
窗口1
窗口2中测试无锁查询:
结果:可以查询到数据,不受影响。
窗口2中测试共享锁查询:
结果:一直处于阻塞状态,没有返回查询结果集。
窗口2中测试排他锁查询:
结果:一直处于阻塞状态,没有返回查询结果集。
分析:一个事务中的查询加了排它锁,且事务没有提交。另一个事务中的排他锁查询和共享锁查询都会处于阻塞状态。这是因为id=1的数据已经被加上了排他锁,并且该锁还未释放,阻塞表明是在等待排他锁释放。另外,说明一下,第二个窗口下的查询操作并没有开启一个事务,但属于其它事务中的查询。因为在默认的自动事务提交设置下 ,select 同Update、Insert、Delete一样都会启动一个隐式的事务。窗口1中的事务是显示事务的使用。
InnoDb 锁机制
最后我们验证一下上面所说的mysql InnoDb引擎中update、delete、insert语句会自动加排他锁的问题。
窗口1,事务中进行更新操作,不提交事务
结果:数据并没有更新。
窗口2中无锁查询:
结果:数据并没有更新。
窗口2中共享锁查询:
结果:查询处于阻塞。
窗口2中排他锁查询:
结果:查询处于阻塞。
分析:一个事务中进行了增删改操作(insert、delete、update),且没有提交事务。另一个事务中的无锁查询可以查到旧数据,共享锁查询和排他锁查询处于阻塞状态。这说明mysql数据库对修改操作自动加上排他锁了。
最后小结:
1)一个事务中的查询加了共享锁,且没有提交事务。另一个事务中的无锁查询和共享锁查询可以查到数据,但排他锁查询不可以。
2)一个事务中的查询加了排它锁,且没有提交事务。另一个事务中的无锁查询可以查到数据,共享锁查询和排他锁查询都会处于阻塞状态。
3)mysql InnoDB引擎的数据库,insert、delete、update都会自动给涉及到的数据加上排他锁,select 语句默认不会加任何锁。