MySQL 锁机制

    锁,在现实生活中是为我们想要隐藏于外界所使用的一种工具。在计算机中,是协调多个进程或线程并发访问某一资源的一种机制。在数据库当中,除了传统的计算资源(CPU、RAM、I/O等等)的争用之外,数据也是一种供许多用户共享访问的资源。如何保证数据并发访问的一致性、有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素。从这一角度来说,锁对于数据库而言就显得尤为重要。
    锁的理解 : 锁是计算机协调多个进程或线程并发访问某一资源的机制。
    锁的分类 : 乐观锁,悲观锁(行锁,表锁,页锁,共享锁(读锁),排他锁(写锁))。

乐观锁
    乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在真正进行更新数据前,再去判断别的线程在此期间有没有修改数据,如果修改了,会交给业务层去处理。
    常用的实现方式是使用版本戳,例如在一张表中添加一个整型字段version,每更新version++,比如某个时刻version=1,线程A读取了此version=1,线程B也读取了此version=1,当线程A更新数据之前,判断version仍然为1,更新成功,version++变为2,但是当线程B再提交更新时,发现version变为2了,与之前读的version=1不一致,就知道有别的线程更新了数据,这个时候就会进行业务逻辑的处理。
    举例:下单操作包括3步骤:
1.查询出商品信息
     select (status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
     update t_goods
     set status=2,version=version+1
     where id=#{id} and version=#{version};
说明:除了自己手动实现乐观锁之外,现在网上许多框架已经封装好了乐观锁的实现,如hibernate,需要时,可自行尝试。

悲观锁
    顾名思义,很悲观,就是每次拿数据的时候都认为别的线程会修改数据,所以在每次拿的时候都会给数据上锁。上锁之后,当别的线程想要拿数据时,就会阻塞,直到给数据上锁的线程将事务提交或者回滚。传统的关系型数据库里就用到了很多这种锁机制,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
    比如:行锁,表锁,页锁,共享锁(读锁),排他锁(写锁) 等,都是在做操作之前先上锁。

1.表锁
(1) 特点
    偏向MyISAM存储引擎,偏读操作。 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低。
(2) 案例分析
     ① 建表
create table mylock(
id int not null primary key auto_increment,
name varchar(20)
)engine myisam;

insert into mylock(name) values('a');
insert into mylock(name) values('b');
insert into mylock(name) values('c');
insert into mylock(name) values('d');
insert into mylock(name) values('e');

select * from mylock;

【手动增加表锁】:lock table 表名字 read(write),表名字2 read(write),其它;
【查看那些表上加过的锁】:show open tables;
【释放表锁】:unlock tables;
 说明:如果在手动增加写锁之前,有人读取数据,然后我们又手动添加写锁,这个人还可以读取上次相同的记录,因为存在缓存。
(3) 查询表级锁争用情况:show status like 'table%';
    可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定。
     Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1 ;
     Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在着较严重的表级锁争用情况;
(4) MySQL表级锁的锁模式
    MySQL的表级锁有两种模式:共享锁(Table Read Lock)和排他锁(Table Write Lock)。锁模式的兼容性如下表所示。              
请求锁模式
         是否兼容
当前锁模式
None读锁写锁
共享锁(读锁)
排他锁(写锁)
    结合上表,所以对MyISAM表进行操作,会有以下情况:
     ① 对MyISAM表的读操作,不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
     ② 对MyISAM表的写操作,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。
            简而言之,就是读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞。

2.行锁
(1) 特点
     ① 偏向InnoDB存储引擎,偏写操作。 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。
     ② InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。
(2) MySQL InnoDB支持三种行锁定方式:
    ① 行锁(Record Lock):锁直接加在索引记录上面。
    ② 间隙锁(Gap Lock):锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。
    ③ Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock.
(3)  案例分析
     ① 建表
create table test_innodb_lock (a int(11),b varchar(16)) engine=innodb;

insert into test_innodb_lock values(1,'b1');
insert into test_innodb_lock values(2,'b2');
insert into test_innodb_lock values(3,'3000');
insert into test_innodb_lock values(4,'4000');
insert into test_innodb_lock values(5,'5000');
insert into test_innodb_lock values(6,'6000');
insert into test_innodb_lock values(7,'7000');
insert into test_innodb_lock values(8,'8000');
insert into test_innodb_lock values(9,'9000');

create index test_innodb_a_ind on test_innodb_lock(a);
create index test_innodb_lock_b_ind on test_innodb_lock(b);

select * from test_innodb_lock;
    取消自动提交 set autocommit=0。
    如何锁定一行: begin ; 然后在锁定表: select * from 表明 where a=7 for update ; 完成之后一定要提交 commit 
     无索引行锁升级为表锁:例如:update test_innodb_lock set b =9001 where a=9;由于没有索引 所以行锁会变成表锁,应该注意。

(3) 如何分析行锁定     注意:Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,Innodb的整体性能和MyISAM相比就会有比较明显的优势了,但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MyISAM高,甚至可能会更差。
     通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:show status like 'innodb_row_lock%';

    Innodb_row_lock_current_waits:当前正在等待锁定的数量;     参数分析
    Innodb_row_lock_time:从系统启动到现在锁定总时间长度; 
    Innodb_row_lock_time_avg:每次等待所花平均时间;
    Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
    Innodb_row_lock_waits:系统启动后到现在总共等待的次数;
    说明:当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
(4) 间隙锁
     ① 什么是间隙锁:当我们用范围条件(而不是相等条件)检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录做“(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
     ② 间隙锁危害:因为Query执行过程中通过过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。
    默认情况下,InnoDB工作在可重复读隔离级别下,并且以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁与间隙锁的组合,这样,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁(Record Lock),再对索引记录两边的间隙(向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间)加上间隙锁(Gap Lock)。如果一个间隙被事务T1加了锁,其它事务是不能在这个间隙插入记录的。
   ③举个例子:
表task_queue
Id           taskId
1              2
3              9
10            20
40            41

开启一个会话: session 1
sql> set autocommit=0;
   ##
取消自动提交

sql> delete from task_queue where taskId = 20;
sql> insert into task_queue values(20, 20);

在开启一个会话: session 2
sql> set autocommit=0;
   ##
取消自动提交

sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(30, 25);

在没有并发,或是极少并发的情况下, 这样会可能会正常执行,在Mysql中, 事务最终都是穿行执行, 但是在高并发的情况下, 执行的顺序就极有可能发生改变, 变成下面这个样子:
sql> delete from task_queue where taskId = 20;
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(20, 20);
sql> insert into task_queue values(30, 25);
     这个时候最后一条语句:insert into task_queue values(30, 25); 执行时就会爆出死锁错误。因为删除taskId = 20这条记录的时候,20 --  41 都被锁住了, 他们都取得了这一个数据段的共享锁, 所以在获取这个数据段的排它锁时出现死锁。
    间隙锁在InnoDB的唯一作用就是防止其它事务的插入操作,以此来达到防止幻读的发生,所以间隙锁不分什么共享锁与排它锁。另外,在上面的例子中,我们选择的是一个普通(非唯一)索引字段来测试的,这不是随便选的,因为如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁,而不会使用Next-Key Lock的方式,也就是说不会对索引之间的间隙加锁,仔细想想的话,这个并不难理解,大家也可以自己测试一下。
    要禁止间隙锁的话,可以把隔离级别降为读已提交,或者开启参数innodb_locks_unsafe_for_binlog。
(5) InnoDB 优化建议
    ① 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁;
    ② 合理设计索引,尽量缩小锁的范围;
    ③ 尽可能较少检索条件,避免间隙锁;
    ④ 尽量控制事务大小,减少锁定资源量和时间长度;
    ⑤ 尽可能低级别事务隔离。

3.页锁
    行锁锁指定行,表锁锁整张表,页锁是折中实现,即一次锁定相邻的一组记录。
(1) 特点
    偏向BerkeleyDB存储引擎, 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般。
    在生产当中使用表锁和行锁较多,所以页锁在此不详细介绍。

4.小总结
    从上述的特点来看,很难笼统的说哪种锁最好,只能根据具体应用的特点来说哪种锁更加合适。仅仅从锁的角度来说的话:
    表锁更适用于以查询为主,只有少量按索引条件更新数据的应用,在MySQL数据库中,使用表级锁定的主要是MyISAM,Memory,CSV等一些非事务性存储引擎。
    行锁更适用于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,在MySQL数据库中,使用行级锁定的主要是Innodb存储引擎和NDBCluster存储引擎,
    页级介于表锁和行锁之间,在MySQL数据库中,使用页级锁定主要是BerkeleyDB存储引擎的锁定方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员学习圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值