当前位置:网站首页>Mysql锁机制与事务隔离
Mysql锁机制与事务隔离
2022-08-02 14:15:00 【怎么起个名就那么难】
一、事务及其ACID
1.并发带来的问题
脏写
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会产生:就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。发生一致性等问题
脏读
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
重点: 事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
不可重读
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
重点: 事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性
幻读
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
重点: 事务A读取到了事务B提交的新增数据,不符合隔离性
2.事务隔离级别
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。常看当前数据库的事务隔离级别: show variables like ‘tx_isolation’,设置事务隔离级别:set tx_isolation=‘REPEATABLE-READ’;如果Spring设置了就用已经设置的隔离级别
3.锁分类
数据库中lock和latch都可以称为锁,但是有很大的区别。
latch一般称为闩锁,用于保证并发线程操作临界资源的正确性,作用对象是内存数据结构,要求锁定时间非常短,不会检测死锁。在InnoDB引擎中又分为mutex(互斥量)和rwlock(读写锁)。
lock是用来锁定数据库中的对象,如表、页、行,作用对象是事务,在commit/rollback后释放,会检测死锁。分为行锁、表锁、意向锁。
我们下面的锁指的都是lock类锁。
- 分为乐观锁(用版本对比来实现)和悲观锁,乐观锁就是不加锁,类似于CAS,重试机制,根据version 比对,直到和我拿的版本一致才可以修改。悲观锁无论如何我都加锁,加锁所有人都会进入等待,类似于上厕所排队
- 从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁) 不管如何加锁
读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁,X锁(eXclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁 - 从对数据操作的粒度分,分为表锁和行锁,锁住整张表,锁一行一行的记录
表锁
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。例如我们要迁移表,先让表锁住,不让别人在插入数据不许修改数据
基本操作
--建表SQL
CREATE TABLE `mylock` (
`id` INT (11) NOT NULL AUTO_INCREMENT,
`NAME` VARCHAR (20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = MyISAM DEFAULT CHARSET = utf8;
--插入数据
INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('1', 'a');
INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('2', 'b');
INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('3', 'c');
INSERT INTO`test`.`mylock` (`id`, `NAME`) VALUES ('4', 'd');
案例结论
1、对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻赛对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
2、对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作
行锁
死锁
每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。两个人相互等待谁都没有释放,导致死锁
InnoDB与MYISAM的最大不同有两点:
- InnoDB支持事务(TRANSACTION)
- InnoDB支持行级锁
行锁演示
一个session开启事务更新不提交,另一个session更新同一条记录会阻塞,更新不同记录不会阻塞
总结:
MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,可以读,但是写操作会阻塞,在执行update、insert、delete操作会自动给涉及的表加写锁。读写都不允许
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一下,但是在整体并发处理能力方面要远远优于MYISAM的表级锁定的。当系统并发量高的时候,Innodb的整体性能和MYISAM相比就会有比较明显的优势了。
但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MYISAM高,甚至可能会更差。
行锁与事务隔离级别案例分析
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lilei', '450');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('hanmei', '16000');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lucy', '2400');
读未提交:
(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
set tx_isolation=‘read-uncommitted’;
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
(4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
(5)在客户端A执行更新语句 update account set balance = balance - 50 where id =1 ,lilei的balance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别
3、读已提交
(1)打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的所有记录:
set tx_isolation=‘read-committed’;
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
(4)客户端B的事务提交
(5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
4、可重复读
(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录
set tx_isolation=‘repeatable-read’;
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交
(3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题!!!
(4)在客户端A,接着执行update account set balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤2中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作不会更新版本号, **是快照读(**历史版本) ;insert、update和delete会更新版本号,是当前读(当前版本)。
(5)重新打开客户端B,插入一条新数据后提交!!!
(6)在客户端A查询表account的所有记录,没有查出新增数据,所以没有出现幻读!!
(7)验证幻读
在客户端A执行update account set balance=888 where id = 4;能更新成功,再次查询能查到客户端B新增的数据!!
5、串行化
(1)打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值:
set tx_isolation=‘serializable’;
(2)打开一个客户端B,并设置当前事务模式为serializable,更新相同的id为1的记录会被阻塞等待,更新id为2的记录可以成功,说明在串行模式下innodb的查询也会被加上行锁。
如果客户端A执行的是一个范围查询,那么该范围内的所有行包括每行记录所在的间隙区间范围(就算该行数据还未被插入也会加锁,这种是间隙锁)都会被加锁。此时如果客户端B在该范围内插入数据都会被阻塞,所以就避免了幻读。
这种隔离级别并发性极低,开发中很少会用到。性能及其的慢
间隙锁(Gap Lock)
间隙锁,锁的就是两个值之间的空隙。Mysql默认级别是repeatable-read,有办法解决幻读问题吗?间隙锁在某些情况下可以解决幻读问题。
假设account表里数据如下:
那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,
在Session_1下面执行 update account set name = ‘zhuge’ where id > 8 and id <18;,则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,20]区间都无法修改数据,注意最后那个20也是包含在内的。
间隙锁是在可重复读隔离级别下才会生效。
临键锁(Next-key Locks)
Next-Key Locks是行锁与间隙锁的组合。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁。
无索引行锁会升级为表锁(RR级别会升级为表锁,RC级别不会升级为表锁)
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁
session1 执行:update account set balance = 800 where name = ‘lilei’;
session2 对该表任一行操作都会阻塞住
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。
锁定某一行还可以用lock in share mode(共享锁) 和for update(排它锁),例如:select * from test_innodb_lock where a = 2 for update; 这样其他session只能读这行数据,修改则会被阻塞,直到锁定行的session提交
边栏推荐
猜你喜欢
随机推荐
代码细节带来的极致体验,ShardingSphere 5.1.0 性能提升密钥
【线程网络】了解线程属性(fork/interview question)
Litestar 4D – WebCatalog 7:全自动数据管理
implement tcp copa on ns3
【无标题】
mininet multihomed topology
Oauth2.0 authentication server adds verification code login method
OpenPose 命令行说明
How does ns3 solve cross reference issue
超简单了解三次握手与四次挥手
udp transparent proxy
固态硬盘到底要不要分区
C语言函数调用过程-汇编分析
Optisystem应用:光电检测器灵敏度建模
【网络安全】学习笔记 --02 安全通信协议
为什么Volatile能保证双重检查锁的线程安全
MySQL协议长什么样子
PostgreSQL 协议数据样例
Apache ShardingSphere 5.1.1 正式发布
【线程安全】用户级,内核级,组合级线程|线程同步的处理(条件变量)|strtok_r(可冲入函数)