概述
定义: 锁是计算机协调多个进程或者线程并发访问某一资源的机制
在数据库中,除传统的计算资源(如CPU,RAM,IO等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发 访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度 来说,锁对数据库而言显得尤其重要,也更加复杂。
实际场景案例:生活购物
打个比方,我们在淘宝上抢购一件商品,商品只有一件库存,这个时候如果有很多人想去买,那么如何解决是你买到还是其他人买到的问题?
这里肯定要用到事务,我们先从库存表中取出物品数量,然后插入订单,付款后插入付款记录表 然后更新商品数量。在这个过程中,使用锁可以对有限资源进行保护,解决隔离和并发的矛盾
锁分类
从数据操作的类型(读、写)分
读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁。
从对数据操作的颗粒度:表锁,行锁
三锁
开销、加锁速度、死锁、粒度、并发性能。只能就具体应用的特点来说哪种锁更合适
表锁(偏读)
特点 偏向MyISAM存储引擎,开销小,加锁快,无死锁,锁定粒度大,发生锁冲突的概率最高,并发最低
案例分析 建表SQL 加读锁 加写锁
【表级锁分析-建表sql】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),其他;lock table mylock read,book write;【查看已添加过的锁】show open tables;【释放表锁】unlock tables;加读锁1.session1 获得表mylock的read锁定 , lock table mylock read; session2连接终端2.session1 可以查询该表的记录,select * from mylock; -->当前session1不能查询其他没有锁定的表,select * from book; session2等其他终端也可以查询该表的记录-->其他session可以查询或者更新未锁定的表,select * from book; update book set card=51 where book_id=1;3. 当前session中插入或者更新 锁定的表 都会提示错误; insert into mylock(name) values('ee'); update mylock set name='a1' where id=1; 其他session插入或者更新锁定的表会一直等待,直到获得锁, insert into mylock(name) values('ee'); update mylock set name='a1' where id=1; 4.释放锁后,其他session获得锁,对应插入或者更新操作完成。 加写锁我们为mylock表加写锁(myISAM存储引擎的写阻塞读的例子)1. session1 获得表的write锁定,unlock tables; lock tables mylock write; 待session1开启写锁后,session2在连接终端2.当前session 对锁定的表 查询+ 更新 + 插入操作都可以执行;select * from mylock where id=1; update mylock set name='a11' where id=1; insert into mylock(name) values('ff');其他session对锁定表的查询被阻塞,需要等待锁被释放; select * from mylock where id=1; 更新与插入操作,会等待释放锁。update mylock set name='a11' where id=1; insert into mylock(name) values('ff');备注:可以尝试用不同的id进行测试,mysql是有缓存的,第二次的条件会从缓存中取得,影响锁效果的演示3.释放锁 unlock tables;其他session获得锁,返回查询结果############################案例结论######################myISAM 在执行查询语句(select)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。mysql的表级锁有两种模式: 表共享读锁(Table read lock) 表独占写锁 ( Table write lock )对比======================锁类型 可否兼容 读锁 写锁读锁 是 是 否写锁 是 否 否结论=====================结合上述对比,所以对myISAM表进行操作,会有以下情况:1.对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一张表的读请求,但是会阻塞对同一张表的写请求。 只有当读锁被释放后,才会执行其他进程的写操作。2.对myISAM表的写操作(加写锁),会阻塞其他进程对同一张表的读和写操作,只有当写操作释放后,才会执行其他 进程的读写操作简而言之,就是读锁会阻塞写,但是不会阻塞读,而写锁则会把读和写都阻塞。###########################表锁分析####################【看看那些表被加锁了】show open tables;【如何分析表锁定】可以通过检查table_locks_waited 和table_locks_immediate状态变量来分析系统上的表锁定;show status like 'table%';这里有两个状态变量记录mysql内部表锁定的情况,两个变量说明如下:table_locks_waited:产生表级锁定的次数,表示可以立即获取的查询次数,每立即获取锁值加1;table_locks_immediate:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1) 此值高则说明存在着比较严重的表级锁争用的情况;此外,myisam的读写锁调度是写优先,这也是mysiam不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永久阻塞;
行锁(偏写):
特点:
偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁
由于行锁支持事务,复习老知识:
事务(Transation)及其ACID属性
事务transation 及其ACID属性
事务是由一组sql组成的逻辑处理单元,事务具有以下四个属性,通常简称事务的ACID属性。
原子性(Atomicity): 事务是一个原子操作单元,其对数据的修改,要么全部执行,要么全部不执行。
一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或者双向链表)也都是必须是正确的。
隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的‘独立’环境执行。这就意味着事务处理过程的中间状态对外部是不可见的,反之亦然。
持久性(Durable):事务完成后,它对数据的修改是永久性的,即使出现系统故障也能够保持。
并发事务处理带来的问题
更新丢失(lost update) 当两个或者多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在(隔离性),就会发生丢失更新问题 -----最后的更新覆盖了由其他事务所做的更新。 例如:两个程序员在修改同一个java文件,每个程序员独立的更改自己的副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其 更改副本的编辑人员覆盖了前一个程序员所做的更改。
脏读(Dirty reads) 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时候,另一个事务也来读取同一条记录, 这时候如果不加以控制,第二个事务读取了这些‘脏数据’,并据此做了进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做‘脏读’
总结:事务A读取到了事务B已修改但是未提交的数据,并在这个数据的基础上做了操作。此时,如果事务B进行了回滚,A读取的数据就无效,违反了一致性的要求
不可重复读(Non-Repeatable Reads)一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或者某些记录已经被删除了。这种现象叫做‘不可重复读’。
总结:事务A读取到了事务B已经提交的修改数据,不符合隔离性。
幻读(Phantom reads) 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就被称为‘幻读’
总结:事务A读取到了事务B提交的新增数据,不符合隔离性
注意:幻读和脏读有点类似 脏读是事务B里面修改了数据 幻读是事务B里面新增了数据
事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
######################建表SQL##################create table test_innodb_lock(a int(11),b varchar(16)) engine=innodb;insert into test_innodb_lock values(1,"aaa");insert into test_innodb_lock values(2,"222");insert into test_innodb_lock values(3,"3333");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");insert into test_innodb_lock values(1,"b1");create index test_innodb_lock_a on test_innodb_lock(a);create index test_innodb_lock_b on test_innodb_lock(b);select * from test_innodb_lock;######################行锁定基本演示##################行锁定基本演示:1.session1 -> set autocommit=0; session2 ->set autocommit=0;2.session1 -> update test_innodb_lock set b='b1' where a=1; #更新但是不提交,没有手写commit session2 -> update test_innodb_lock set b='b2' where a=1; #session2 被阻塞,只能等待3. session1 -> commit; #提交更新 session2 -> 接触阻塞,更新正常进行4. session2 执行命令commit;注意:若果session1更新会话a=1,session2更新会话a=9,互相并不影响。######################无索引行锁升级为表锁##################varchar 不用 ' ' 导致系统自动转换类型, 行锁变表锁session1 -> update test_innodb_lock set a=41 where b=4000;session2 -> update test_innodb_lock set b="9000" where a=9;######################间隙锁危害##################间隙锁的危害案例:间隙锁带来的插入问题1.session1 -> update test_innodb_lock set b=a*20 where a >1 and a <5; session2 ->insert into test_innodb_lock values(2,'200'); #阻塞产生,暂时不能插入2.session1 -> commit; session2 -> 阻塞解除,完成插入【什么是间隙锁】当我们用范围条件而不是相等条件检索数据,并请求共享或排它锁时候,innoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(next-key锁)、【危害】因为Query执行过程中通过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值不存在。间隙锁有同一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的损失。######################面试题:常考如何锁定一行##################面试题:如何锁定一行1.session1 -> begin; select * from test_innodb_lock where a=8 for update;session2 -> update test_innodb_lock set b='8001' where a=8;session1 -> commit;解释:select xxx... from XX... for update;锁定某一行后,其他的操作会被阻塞,直到锁定行的会话提交commit;######################案例结论##################案例总结: innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会更好一些,但是在整体并发处理能力方面要远远优于myISAM的表级锁定。当系统并发量较高的时候,innodb的整体性能和myISAM相比就会有比较显著的优势了。 但是,innodb的行级锁定同样也有其脆弱的一面(行锁变表锁),当我们使用不当的时候,可能会让innodb的整体性能表现不仅不能比myISAM高,甚至会更差#######################行锁分析######################行锁分析【如何分析行锁定】通过InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况mysql> show status like 'innodb_row_lock%';+-------------------------------+-------+| Variable_name | Value |+-------------------------------+-------+| Innodb_row_lock_current_waits | 0 || Innodb_row_lock_time | 20428 || Innodb_row_lock_time_avg | 4085 || Innodb_row_lock_time_max | 5453 || Innodb_row_lock_waits | 5 |+-------------------------------+-------+对各个状态量的说明:Innodb_row_lock_current_waits:当前正在等待锁定的数量Innodb_row_lock_time:从系统启动到现在锁定的总时间长度Innodb_row_lock_time_avg:每次等待所花费的平均时间Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间Innodb_row_lock_waits:系统启动后到现在总共等待的此时对于这五个状态变量,比较重要的是以下三项Innodb_row_lock_time_avg:等待平均时长Innodb_row_lock_waits:等待总次数Innodb_row_lock_time:等待总时长尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
优化建议
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
合理设计索引,尽量缩小锁的范围
页锁:
开销和加锁时间界于表锁和行锁之间:会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 了解一下即可