荐 Mysql锁与事务隔离级别
1.1 锁的分类
从性能上分为乐观锁和悲观锁
1.1.1 乐观锁
通过版本控制来实现:在表中加个字段,每次操作完成之后对版本进行更新。解决了多个线程同时操作时数据覆盖的问题,在性能上稍快
1.1.2 悲观锁
多个线程同时执行时,实际上是有个等待的过程,后面到的线程需要等待前面的资源执行完成,在性能上稍差
从对数据库操作的类型分:读锁和写锁
从粒度:表锁和行锁
读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁
1.2 三把锁
1.2.1 表锁(偏度)
表锁偏向myIsam存储引擎,开销小,加锁快,发生锁冲突的概率高
Myisam:支持表锁
innodb:支持行锁
1.2.1.1 基本操作
建表并插入数据
DROP TABLE IF EXISTS `locktest`;
CREATE TABLE `locktest` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `locktest` VALUES ('1', 'zhangsan');
INSERT INTO `locktest` VALUES ('2', 'lisi');
INSERT INTO `locktest` VALUES ('3', 'gaowz');
手动增加表锁
lock table 表名称 read(write);
查看表上加过的锁
show open tables;
删除表锁
unlock tables;
1.2.1.2 案例分析(加读锁)
lock table locktest read;
当前session和其他session都可以读该表
当期session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待
1)对Session A 加表锁,在其他Session B中执行更新语法,我们可以看到此时sql出于阻塞状态
2)Session A 中执行更新语法,这里直接给出报错提示(乐观锁)
3)释放Session A中的表锁,Session B的sql语句自动提交(悲观锁)
加写锁
当前session对改表的增删改查都没有问题,其他session对改表所有操作都被阻塞,写锁实际上是排它锁,会将其他session的一切操作排除在外
1) 在Session A对表加写锁,我们会发现在Session B中对该表的操作都会被阻塞
2) 在Session A中释放该表锁,SessionB中的sql才可以执行
结论:
由于MyISam在执行查询语句前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加读锁
1、对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻赛对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
2、对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作
读锁会阻塞写,但是不会阻塞读;写锁会把读和写都堵塞
1.3 行锁(偏写)
行锁偏向Innodb存储引擎,开销大、加锁慢,会出现死锁,锁粒度最小,发生锁冲突的概率最低,并发度也最高。InnoDB与MyISAM的最大的不同有两点:一是支持事务;二是使用了行级锁、
1.3.1 行锁支持事务
-
事务
事务是由一组SQL语句组成的逻辑处理单元,事务具有四个属性(ACID)
原子性(Automicty):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
一致性(Consistent): 在事务开始和完成时,数据都必须保持一致状态。这意味着素有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
隔离性(lsolation): 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的
持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。 -
并发事务处理带来的问题
1) 更新丢失(Lost Update)
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。
2)脏读(Dirty Reads)
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
一句话:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
3)不可重读(Non-Repeatable Reads)
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
一句话:事务A读取到了事务B已经提交的修改数据,不符合隔离性
4)幻读(Phantom Reads)
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
一句话:事务A读取到了事务B提交的新增数据,不符合隔离性
脏读是事务B里面修改了数据
幻读是事务B里面新增了数据 -
事务隔离级别
脏读、不可重复读和幻读,都是数据库读一致性问题,必须由数据库提供一定的事务隔离截止来解决
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
可串行化 | 不可能 | 不可能 | 不可能 |
数据库的事务隔离越严格,并发副作用越小,但付出的代价也越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。
常看当前数据库的事务隔离级别: show variables like ‘tx_isolation’;
**设置事务隔离级别:set tx_isolation=‘REPEATABLE-READ’;
SET session TRANSACTION ISOLATION LEVEL Read uncommitted **
1.3.2 隔离级别案例分析
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`balance` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `account` VALUES ('1', '张三', '100');
INSERT INTO `account` VALUES ('2', 'gaowz', '10000');
INSERT INTO `account` VALUES ('3', 'lisi', '3000');
1) 读未提交
打开一个客户端A,并设置当前事务模式为read uncommited(未提交读),查询表account的初始值:
- 设置隔离级别
set tx_isolation='read-uncommitted';
在两个session中都设置隔离级别为读未提交,我们会发现Session A 中插入的语句事务未提交,在Session B中依然可以看到
-
如果Session A 在回滚之前Session B已经对数据进行修改并进行提交,那么过程中读取的就是脏数据,如下所示,Session A 回滚应该是将数值回滚到100,但实际上是变成了500。
针对这个问题我们可以采用读已提交的事务隔离级别
2) 读已提交
打开一个客户端A,并设置当前事务的模式为read commit (未提交),查询表account所有记录 -
设置隔离级别
set tx_isolation='read-committed';
此时在Session A 中修改balance的值为400,我们发现在Session B中的balance依然是500
此时如果我们仍然要对此数据进行修改,可以看到会将当前的事务阻塞,只有等到Session A 中事务提交之后,Session B中的事务才可以继续向下执行
读已提交虽然解决了脏读的问题,但是没有解决不可重复读的问题——事务A读取到了事务B已经提交的修改的数据
例:事务A 读取了数据500,并进行了一系列的运算操作,在对500更新之前需要重新查询一次500这个数据,如果此时,事务A同样对500这个数据进行了修改,并进行了提交,那事务A中就读到了事务B中已提交修改的数据,那这种情况就不符合正常的逻辑。如下图所示:
3) 可重复读
打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录
- 设置隔离级别
set tx_isolation='repeatable-read';
事务A在事务B提交之后重复读的数据依旧是正常的数据,这个Mysql默认的事务隔离级别就解决了不可重复读的问题
如果,我们在事物A中再次进行更新操作,还是执行在session b中的语句balance的结果应该是多少呢
update account set balance = balance -50 where id =1;
按照正常的逻辑来说此时balance的值应该是200-50 = 150,但实际上是100.
这是因为Mysql可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。这个情况就是幻读的问题,所以我们说,可重复读不可以解决换读的问题
接下来我们再次来验证一下幻读的问题:
首先在事务B中插入一条数据并提交事务,接下来在事务A中查询这条数据,我们发现没有查询到这条数据,接下来再对这条数据进行更新操作,并重新查询,这样我们就可以查询到这条数据
注:这里在对balance字段操作时没有直接给字段赋值,这种方式可以通过数据库自带的锁,来帮助我们拿到的是最新的数据
解决幻读的问题只需要将事务的隔离级别设置为串行化即可
4) 串行化
打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值
因为这种隔离级别并发性非常低,所以一般不会用到,在这里便不再演示
set tx_isolation='serializable';
Mysql默认的是可重复读,如何解决幻读的问题
间隙锁在一定程度上可以解决这个问题:
我们如果要在事务A中更新某条数据,那可以使用间隙锁,那么其他的事务将无法插入这个范围内的数据,所以可以再一定程度上避免幻读。
update account set name = 'ceshi' where id > 1 and id <=6;
1.3.4 总结
优点:
Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一下,但是在整体并发处理能力方面要远远优于MYISAM的表级锁定的。当系统并发量高的时候,Innodb的整体性能和MYISAM相比有明显的的优势
缺点:
由于其粒度小,加锁时间慢,当我们使用不当时可能会让Innodb的整体性能表现不仅不能比MYISAM高,甚至可能会更差
1.3.5 优化小技巧
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
- 合理设计索引,
- 尽量缩小锁的范围 尽可能减少检索条件,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度
- 尽可能低级别事务隔离
本文地址:https://blog.csdn.net/gwz_6903/article/details/107126042